import { Injectable } from '@nestjs/common';
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';

import isEmpty from 'lodash.isempty';
import { v4 } from 'uuid';
import { DataSource, EntityManager, Repository } from 'typeorm';

import { CreateRemoteServerInput } from 'src/engine/metadata-modules/remote-server/dtos/create-remote-server.input';
import {
  RemoteServerEntity,
  RemoteServerType,
} from 'src/engine/metadata-modules/remote-server/remote-server.entity';
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
import { encryptText } from 'src/engine/core-modules/auth/auth.util';
import {
  validateObjectAgainstInjections,
  validateStringAgainstInjections,
} from 'src/engine/metadata-modules/remote-server/utils/validate-remote-server-input.utils';
import { ForeignDataWrapperServerQueryFactory } from 'src/engine/api/graphql/workspace-query-builder/factories/foreign-data-wrapper-server-query.factory';
import { RemoteTableService } from 'src/engine/metadata-modules/remote-server/remote-table/remote-table.service';
import { UpdateRemoteServerInput } from 'src/engine/metadata-modules/remote-server/dtos/update-remote-server.input';
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
import { buildUpdateRemoteServerRawQuery } from 'src/engine/metadata-modules/remote-server/utils/build-update-remote-server-raw-query.utils';
import { validateRemoteServerType } from 'src/engine/metadata-modules/remote-server/utils/validate-remote-server-type.util';
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import {
  RemoteServerException,
  RemoteServerExceptionCode,
} from 'src/engine/metadata-modules/remote-server/remote-server.exception';

@Injectable()
export class RemoteServerService<T extends RemoteServerType> {
  constructor(
    @InjectRepository(RemoteServerEntity, 'metadata')
    private readonly remoteServerRepository: Repository<
      RemoteServerEntity<RemoteServerType>
    >,
    @InjectDataSource('metadata')
    private readonly metadataDataSource: DataSource,
    private readonly environmentService: EnvironmentService,
    private readonly foreignDataWrapperServerQueryFactory: ForeignDataWrapperServerQueryFactory,
    private readonly remoteTableService: RemoteTableService,
    private readonly workspaceDataSourceService: WorkspaceDataSourceService,
    @InjectRepository(FeatureFlagEntity, 'core')
    private readonly featureFlagRepository: Repository<FeatureFlagEntity>,
  ) {}

  public async createOneRemoteServer(
    remoteServerInput: CreateRemoteServerInput<T>,
    workspaceId: string,
  ): Promise<RemoteServerEntity<RemoteServerType>> {
    this.validateRemoteServerInputAgainstInjections(remoteServerInput);

    validateRemoteServerType(
      remoteServerInput.foreignDataWrapperType,
      this.featureFlagRepository,
      workspaceId,
    );

    const foreignDataWrapperId = v4();

    let remoteServerToCreate = {
      ...remoteServerInput,
      workspaceId,
      foreignDataWrapperId,
    };

    if (remoteServerInput.userMappingOptions) {
      remoteServerToCreate = {
        ...remoteServerToCreate,
        userMappingOptions: {
          ...remoteServerInput.userMappingOptions,
          password: this.encryptPassword(
            remoteServerInput.userMappingOptions.password,
          ),
        },
      };
    }

    return this.metadataDataSource.transaction(
      async (entityManager: EntityManager) => {
        const createdRemoteServer = entityManager.create(
          RemoteServerEntity,
          remoteServerToCreate,
        );

        const foreignDataWrapperQuery =
          this.foreignDataWrapperServerQueryFactory.createForeignDataWrapperServer(
            createdRemoteServer.foreignDataWrapperId,
            remoteServerInput.foreignDataWrapperType,
            remoteServerInput.foreignDataWrapperOptions,
          );

        await entityManager.query(foreignDataWrapperQuery);

        if (remoteServerInput.userMappingOptions) {
          const userMappingQuery =
            this.foreignDataWrapperServerQueryFactory.createUserMapping(
              createdRemoteServer.foreignDataWrapperId,
              remoteServerInput.userMappingOptions,
            );

          await entityManager.query(userMappingQuery);
        }

        await entityManager.save(RemoteServerEntity, createdRemoteServer);

        return createdRemoteServer;
      },
    );
  }

  public async updateOneRemoteServer(
    remoteServerInput: UpdateRemoteServerInput<T>,
    workspaceId: string,
  ): Promise<RemoteServerEntity<RemoteServerType>> {
    this.validateRemoteServerInputAgainstInjections(remoteServerInput);

    const remoteServer = await this.findOneByIdWithinWorkspace(
      remoteServerInput.id,
      workspaceId,
    );

    if (!remoteServer) {
      throw new RemoteServerException(
        'Remote server does not exist',
        RemoteServerExceptionCode.REMOTE_SERVER_NOT_FOUND,
      );
    }

    const currentRemoteTablesForServer =
      await this.remoteTableService.findRemoteTablesByServerId({
        remoteServerId: remoteServer.id,
        workspaceId,
      });

    if (currentRemoteTablesForServer.length > 0) {
      throw new RemoteServerException(
        'Cannot update remote server with synchronized tables',
        RemoteServerExceptionCode.REMOTE_SERVER_MUTATION_NOT_ALLOWED,
      );
    }

    const foreignDataWrapperId = remoteServer.foreignDataWrapperId;

    let partialRemoteServerWithUpdates = {
      ...remoteServerInput,
      workspaceId,
      foreignDataWrapperId,
    };

    if (partialRemoteServerWithUpdates?.userMappingOptions?.password) {
      partialRemoteServerWithUpdates = {
        ...partialRemoteServerWithUpdates,
        userMappingOptions: {
          ...partialRemoteServerWithUpdates.userMappingOptions,
          password: this.encryptPassword(
            partialRemoteServerWithUpdates.userMappingOptions.password,
          ),
        },
      };
    }

    return this.metadataDataSource.transaction(
      async (entityManager: EntityManager) => {
        const updatedRemoteServer = await this.updateRemoteServer(
          partialRemoteServerWithUpdates,
        );

        if (
          !isEmpty(partialRemoteServerWithUpdates.foreignDataWrapperOptions)
        ) {
          const foreignDataWrapperQuery =
            this.foreignDataWrapperServerQueryFactory.updateForeignDataWrapperServer(
              {
                foreignDataWrapperId,
                foreignDataWrapperOptions:
                  partialRemoteServerWithUpdates.foreignDataWrapperOptions,
              },
            );

          await entityManager.query(foreignDataWrapperQuery);
        }

        if (!isEmpty(partialRemoteServerWithUpdates.userMappingOptions)) {
          const userMappingQuery =
            this.foreignDataWrapperServerQueryFactory.updateUserMapping(
              foreignDataWrapperId,
              partialRemoteServerWithUpdates.userMappingOptions,
            );

          await entityManager.query(userMappingQuery);
        }

        return updatedRemoteServer;
      },
    );
  }

  public async deleteOneRemoteServer(
    id: string,
    workspaceId: string,
  ): Promise<RemoteServerEntity<RemoteServerType>> {
    validateStringAgainstInjections(id);

    const remoteServer = await this.remoteServerRepository.findOne({
      where: {
        id,
        workspaceId,
      },
    });

    if (!remoteServer) {
      throw new RemoteServerException(
        'Remote server does not exist',
        RemoteServerExceptionCode.REMOTE_SERVER_NOT_FOUND,
      );
    }

    await this.remoteTableService.unsyncAll(workspaceId, remoteServer);

    return this.metadataDataSource.transaction(
      async (entityManager: EntityManager) => {
        await entityManager.query(
          `DROP SERVER "${remoteServer.foreignDataWrapperId}" CASCADE`,
        );
        await entityManager.delete(RemoteServerEntity, id);

        return remoteServer;
      },
    );
  }

  public async findOneByIdWithinWorkspace(id: string, workspaceId: string) {
    return this.remoteServerRepository.findOne({
      where: {
        id,
        workspaceId,
      },
    });
  }

  public async findManyByTypeWithinWorkspace<T extends RemoteServerType>(
    foreignDataWrapperType: T,
    workspaceId: string,
  ) {
    return this.remoteServerRepository.find({
      where: {
        foreignDataWrapperType,
        workspaceId,
      },
    });
  }

  private encryptPassword(password: string) {
    const key = this.environmentService.get('LOGIN_TOKEN_SECRET');

    return encryptText(password, key);
  }

  private async updateRemoteServer(
    remoteServerToUpdate: DeepPartial<RemoteServerEntity<RemoteServerType>> &
      Pick<RemoteServerEntity<RemoteServerType>, 'workspaceId' | 'id'>,
  ): Promise<RemoteServerEntity<RemoteServerType>> {
    const [parameters, rawQuery] =
      buildUpdateRemoteServerRawQuery(remoteServerToUpdate);

    const updateResult = await this.workspaceDataSourceService.executeRawQuery(
      rawQuery,
      parameters,
      remoteServerToUpdate.workspaceId,
    );

    return updateResult[0][0];
  }

  private validateRemoteServerInputAgainstInjections(
    remoteServerInput: CreateRemoteServerInput<T> | UpdateRemoteServerInput<T>,
  ) {
    if (remoteServerInput.foreignDataWrapperOptions) {
      validateObjectAgainstInjections(
        remoteServerInput.foreignDataWrapperOptions,
      );
    }

    if (remoteServerInput.userMappingOptions) {
      validateObjectAgainstInjections(remoteServerInput.userMappingOptions);
    }
  }
}
