import { Injectable } from '@nestjs/common';

import { DataSourceService } from 'src/metadata/data-source/data-source.service';
import { FieldMetadataService } from 'src/metadata/field-metadata/field-metadata.service';
import { ObjectMetadataService } from 'src/metadata/object-metadata/object-metadata.service';
import { TenantMigrationRunnerService } from 'src/tenant-migration-runner/tenant-migration-runner.service';
import { TenantMigrationService } from 'src/metadata/tenant-migration/tenant-migration.service';
import { standardObjectsPrefillData } from 'src/tenant-manager/standard-objects-prefill-data/standard-objects-prefill-data';
import { TenantDataSourceService } from 'src/tenant-datasource/tenant-datasource.service';
import { DataSourceEntity } from 'src/metadata/data-source/data-source.entity';
import { RelationMetadataService } from 'src/metadata/relation-metadata/relation-metadata.service';
import { standardObjectRelationMetadata } from 'src/tenant-manager/standard-objects/standard-object-relation-metadata';
import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity';
import {
  FieldMetadataEntity,
  FieldMetadataType,
} from 'src/metadata/field-metadata/field-metadata.entity';

import {
  basicFieldsMetadata,
  standardObjectsMetadata,
} from './standard-objects/standard-object-metadata';

@Injectable()
export class TenantManagerService {
  constructor(
    private readonly tenantDataSourceService: TenantDataSourceService,
    private readonly tenantMigrationService: TenantMigrationService,
    private readonly migrationRunnerService: TenantMigrationRunnerService,
    private readonly objectMetadataService: ObjectMetadataService,
    private readonly fieldMetadataService: FieldMetadataService,
    private readonly dataSourceService: DataSourceService,
    private readonly relationMetadataService: RelationMetadataService,
  ) {}

  /**
   * Init a workspace by creating a new data source and running all migrations
   * @param workspaceId
   * @returns Promise<void>
   */
  public async init(workspaceId: string): Promise<void> {
    const schemaName =
      await this.tenantDataSourceService.createWorkspaceDBSchema(workspaceId);

    const dataSourceMetadata =
      await this.dataSourceService.createDataSourceMetadata(
        workspaceId,
        schemaName,
      );

    await this.tenantMigrationService.insertStandardMigrations(workspaceId);

    await this.migrationRunnerService.executeMigrationFromPendingMigrations(
      workspaceId,
    );

    const createdObjectMetadata =
      await this.createStandardObjectsAndFieldsMetadata(
        dataSourceMetadata.id,
        workspaceId,
      );

    await this.prefillWorkspaceWithStandardObjects(
      dataSourceMetadata,
      workspaceId,
      createdObjectMetadata,
    );
  }

  /**
   *
   * Create all standard objects and fields metadata for a given workspace
   *
   * @param dataSourceId
   * @param workspaceId
   */
  public async createStandardObjectsAndFieldsMetadata(
    dataSourceId: string,
    workspaceId: string,
  ): Promise<ObjectMetadataEntity[]> {
    const createdObjectMetadata = await this.objectMetadataService.createMany(
      Object.values(standardObjectsMetadata).map(
        (objectMetadata: ObjectMetadataEntity) => ({
          ...objectMetadata,
          dataSourceId,
          workspaceId,
          isCustom: false,
          isActive: true,
          fields: [...basicFieldsMetadata, ...objectMetadata.fields].map(
            (field) => ({
              ...field,
              workspaceId,
              isCustom: false,
              isActive: true,
            }),
          ),
        }),
      ),
    );

    await this.relationMetadataService.createMany(
      Object.values(standardObjectRelationMetadata).map((relationMetadata) =>
        this.createStandardObjectRelations(
          workspaceId,
          createdObjectMetadata,
          relationMetadata,
        ),
      ),
    );

    return createdObjectMetadata;
  }

  /**
   *
   * @param workspaceId
   * @param createdObjectMetadata
   * @param relationMetadata
   * @returns Partial<RelationMetadataEntity>
   */
  private createStandardObjectRelations(
    workspaceId: string,
    createdObjectMetadata: ObjectMetadataEntity[],
    relationMetadata: any,
  ) {
    const createdObjectMetadataByNameSingular = createdObjectMetadata.reduce(
      (acc, curr) => {
        acc[curr.nameSingular] = curr;
        return acc;
      },
      {},
    );

    const fromObjectMetadata =
      createdObjectMetadataByNameSingular[
        relationMetadata.fromObjectNameSingular
      ];
    const toObjectMetadata =
      createdObjectMetadataByNameSingular[
        relationMetadata.toObjectNameSingular
      ];

    if (!fromObjectMetadata) {
      throw new Error(
        `Could not find created object metadata with 
          fromObjectNameSingular: ${relationMetadata.fromObjectNameSingular}`,
      );
    }

    if (!toObjectMetadata) {
      throw new Error(
        `Could not find created object metadata with
          toObjectNameSingular: ${relationMetadata.toObjectNameSingular}`,
      );
    }

    const fromFieldMetadata = createdObjectMetadataByNameSingular[
      relationMetadata.fromObjectNameSingular
    ]?.fields.find(
      (field: FieldMetadataEntity) =>
        field.type === FieldMetadataType.RELATION &&
        field.name === relationMetadata.fromFieldMetadataName,
    );

    const toFieldMetadata = createdObjectMetadataByNameSingular[
      relationMetadata.toObjectNameSingular
    ]?.fields.find(
      (field: FieldMetadataEntity) =>
        field.type === FieldMetadataType.RELATION &&
        field.name === relationMetadata.toFieldMetadataName,
    );

    if (!fromFieldMetadata) {
      throw new Error(
        `Could not find created field metadata with 
          fromFieldMetadataName: ${relationMetadata.fromFieldMetadataName}
          for object: ${relationMetadata.fromObjectNameSingular}`,
      );
    }

    if (!toFieldMetadata) {
      throw new Error(
        `Could not find created field metadata with 
          toFieldMetadataName: ${relationMetadata.toFieldMetadataName}
          for object: ${relationMetadata.toObjectNameSingular}`,
      );
    }

    return {
      fromObjectMetadataId: fromObjectMetadata.id,
      toObjectMetadataId: toObjectMetadata.id,
      workspaceId,
      relationType: relationMetadata.type,
      fromFieldMetadataId: fromFieldMetadata.id,
      toFieldMetadataId: toFieldMetadata.id,
    };
  }

  /**
   *
   * Reset all standard objects and fields metadata for a given workspace
   *
   * @param dataSourceId
   * @param workspaceId
   */
  public async resetStandardObjectsAndFieldsMetadata(
    dataSourceId: string,
    workspaceId: string,
  ) {
    await this.objectMetadataService.deleteMany({
      workspaceId: { eq: workspaceId },
    });

    await this.createStandardObjectsAndFieldsMetadata(
      dataSourceId,
      workspaceId,
    );
  }

  /**
   *
   * We are prefilling a few standard objects with data to make it easier for the user to get started.
   *
   * @param dataSourceMetadata
   * @param workspaceId
   */
  private async prefillWorkspaceWithStandardObjects(
    dataSourceMetadata: DataSourceEntity,
    workspaceId: string,
    createdObjectMetadata: ObjectMetadataEntity[],
  ) {
    const workspaceDataSource =
      await this.tenantDataSourceService.connectToWorkspaceDataSource(
        workspaceId,
      );

    if (!workspaceDataSource) {
      throw new Error('Could not connect to workspace data source');
    }

    standardObjectsPrefillData(
      workspaceDataSource,
      dataSourceMetadata.schema,
      createdObjectMetadata,
    );
  }

  /**
   *
   * Delete a workspace by deleting all metadata and the schema
   *
   * @param workspaceId
   */
  public async delete(workspaceId: string): Promise<void> {
    // Delete data from metadata tables
    await this.fieldMetadataService.deleteFieldsMetadata(workspaceId);
    await this.objectMetadataService.deleteObjectsMetadata(workspaceId);
    await this.tenantMigrationService.delete(workspaceId);
    await this.dataSourceService.delete(workspaceId);
    // Delete schema
    await this.tenantDataSourceService.deleteWorkspaceDBSchema(workspaceId);
  }
}
