import DataLoader from 'dataloader';
import { Injectable, Scope } from 'graphql-modules';
// eslint-disable-next-line no-restricted-imports
import type { PoolClient } from 'pg';
import { sql } from '@pgtyped/runtime';
import { TenantAwareDBClient } from '../../app-providers/tenant-db-client.js';
import type {
  IDeleteFinancialEntityQuery,
  IGetAllFinancialEntitiesQuery,
  IGetAllFinancialEntitiesResult,
  IGetFinancialEntitiesByIdsQuery,
  IInsertFinancialEntitiesParams,
  IInsertFinancialEntitiesQuery,
  IReplaceFinancialEntitiesQuery,
  IUpdateFinancialEntityParams,
  IUpdateFinancialEntityQuery,
} from '../types.js';
import { BusinessesOperationProvider } from './businesses-operation.provider.js';
import { BusinessesProvider } from './businesses.provider.js';
import { TaxCategoriesProvider } from './tax-categories.provider.js';

const getFinancialEntitiesByIds = sql<IGetFinancialEntitiesByIdsQuery>`
    SELECT *
    FROM accounter_schema.financial_entities
    WHERE id IN $$ids;`;

const getAllFinancialEntities = sql<IGetAllFinancialEntitiesQuery>`
    SELECT *
    FROM accounter_schema.financial_entities;`;

const updateFinancialEntity = sql<IUpdateFinancialEntityQuery>`
  UPDATE accounter_schema.financial_entities
  SET
  name = COALESCE(
    $name,
    name
  ),
  sort_code = COALESCE(
    $sortCode,
    sort_code
  ),
  type = COALESCE(
    $type,
    type
  ),
  irs_code = COALESCE(
    $irsCode,
    irs_code
  ),
  is_active = COALESCE(
    $isActive,
    is_active
  )
  WHERE
    id = $financialEntityId
  RETURNING *;
`;

const insertFinancialEntities = sql<IInsertFinancialEntitiesQuery>`
  INSERT INTO accounter_schema.financial_entities (type, owner_id, name, sort_code, irs_code, is_active)
  VALUES $$financialEntities(type, ownerId, name, sortCode, irsCode, isActive)
  RETURNING *;`;

const deleteFinancialEntity = sql<IDeleteFinancialEntityQuery>`
    DELETE FROM accounter_schema.financial_entities
    WHERE id = $financialEntityId
    RETURNING id;
  `;

const replaceFinancialEntities = sql<IReplaceFinancialEntitiesQuery>`
  WITH ledger_debit1 AS (
    UPDATE accounter_schema.ledger_records
    SET debit_entity1 = $targetEntityId
    WHERE debit_entity1 = $entityIdToReplace
    RETURNING id
  ),
  ledger_debit2 AS (
    UPDATE accounter_schema.ledger_records
    SET debit_entity2 = $targetEntityId
    WHERE debit_entity2 = $entityIdToReplace
    RETURNING id
  ),
  ledger_credit1 AS (
    UPDATE accounter_schema.ledger_records
    SET credit_entity1 = $targetEntityId
    WHERE credit_entity1 = $entityIdToReplace
    RETURNING id
  ),
  ledger_credit2 AS (
    UPDATE accounter_schema.ledger_records
    SET credit_entity2 = $targetEntityId
    WHERE credit_entity2 = $entityIdToReplace
    RETURNING id
  ),
  misc_expenses_creditor AS (
    UPDATE accounter_schema.misc_expenses
    SET creditor_id = $targetEntityId
    WHERE creditor_id = $entityIdToReplace
    RETURNING id
  )
  UPDATE accounter_schema.misc_expenses
    SET debtor_id = $targetEntityId
    WHERE debtor_id = $entityIdToReplace
  RETURNING id;
`;

@Injectable({
  scope: Scope.Operation,
  global: true,
})
export class FinancialEntitiesProvider {
  constructor(
    private db: TenantAwareDBClient,
    private businessesProvider: BusinessesProvider,
    private businessesOperationProvider: BusinessesOperationProvider,
    private taxCategoriesProvider: TaxCategoriesProvider,
  ) {}

  private async batchFinancialEntitiesByIds(ids: readonly string[]) {
    const financialEntities = await getFinancialEntitiesByIds.run(
      {
        ids,
      },
      this.db,
    );
    return ids.map(id => financialEntities.find(fe => fe.id === id));
  }

  public getFinancialEntityByIdLoader = new DataLoader((keys: readonly string[]) =>
    this.batchFinancialEntitiesByIds(keys),
  );

  private allFinancialEntitiesCache: Promise<IGetAllFinancialEntitiesResult[]> | null = null;
  public getAllFinancialEntities() {
    if (this.allFinancialEntitiesCache) {
      return this.allFinancialEntitiesCache;
    }
    const result = getAllFinancialEntities.run(undefined, this.db).then(entities => {
      entities.map(entity => {
        this.getFinancialEntityByIdLoader.prime(entity.id, entity);
      });
      return entities;
    });
    this.allFinancialEntitiesCache = result;
    return result;
  }

  /** Returns a map of sort_code → entity ids, built once per request from the cached entity list. */
  private entityBySortCodeMapCache: Promise<Map<number, string[]>> | null = null;
  public getEntityBySortCodeMap(): Promise<Map<number, string[]>> {
    if (this.entityBySortCodeMapCache) return this.entityBySortCodeMapCache;
    this.entityBySortCodeMapCache = this.getAllFinancialEntities().then(entities => {
      const map = new Map<number, string[]>();
      for (const entity of entities) {
        if (entity.sort_code == null) continue;
        const bucket = map.get(entity.sort_code);
        if (bucket) {
          bucket.push(entity.id);
        } else {
          map.set(entity.sort_code, [entity.id]);
        }
      }
      return map;
    });
    return this.entityBySortCodeMapCache;
  }

  public updateFinancialEntity(params: IUpdateFinancialEntityParams) {
    if (params.financialEntityId) {
      this.invalidateFinancialEntityById(params.financialEntityId);
    }
    return updateFinancialEntity.run(params, this.db);
  }

  public insertFinancialEntity(
    params: IInsertFinancialEntitiesParams['financialEntities'][number],
    client?: PoolClient,
  ) {
    this.allFinancialEntitiesCache = null;
    return insertFinancialEntities.run({ financialEntities: [params] }, client ?? this.db);
  }

  private async batchInsertFinancialEntities(
    newFinancialEntities: readonly IInsertFinancialEntitiesParams['financialEntities'][number][],
  ) {
    const financialEntities = await insertFinancialEntities.run(
      {
        financialEntities: newFinancialEntities,
      },
      this.db,
    );
    return newFinancialEntities.map(fe => financialEntities.find(f => f.name === fe.name) ?? null);
  }

  public insertFinancialEntitiesLoader = new DataLoader(
    (financialEntities: readonly IInsertFinancialEntitiesParams['financialEntities'][number][]) =>
      this.batchInsertFinancialEntities(financialEntities),
    {
      cache: false,
    },
  );

  public async deleteFinancialEntityById(financialEntityId: string) {
    const entity = await this.getFinancialEntityByIdLoader.load(financialEntityId);
    if (!entity) {
      throw new Error(`Financial entity with id ${financialEntityId} not found`);
    }
    if (entity.id === entity.owner_id) {
      throw new Error('Cannot delete owner entity');
    }

    this.invalidateFinancialEntityById(financialEntityId);

    // remove business
    const deleteBusiness =
      entity.type === 'business'
        ? this.businessesOperationProvider.deleteBusinessById(financialEntityId)
        : Promise.resolve();

    // remove tax category
    const deleteTaxCategory =
      entity.type === 'tax_category'
        ? this.taxCategoriesProvider.deleteTaxCategoryById(financialEntityId)
        : Promise.resolve();

    await Promise.all([deleteBusiness, deleteTaxCategory]);

    // TODO: should remove ledger, misc expenses?

    // delete entity
    deleteFinancialEntity.run({ financialEntityId }, this.db);
  }

  public async replaceFinancialEntity(
    targetEntityId: string,
    entityIdToReplace: string,
    deleteEntity: boolean = false,
  ) {
    const [entityToReplace, entity] = await Promise.all([
      this.getFinancialEntityByIdLoader.load(entityIdToReplace),
      this.getFinancialEntityByIdLoader.load(targetEntityId),
    ]);
    if (!entityToReplace) {
      throw new Error(`Financial entity with id ${entityIdToReplace} not found`);
    }
    if (!entity) {
      throw new Error(`Financial entity with id ${targetEntityId} not found`);
    }
    if (entityToReplace.type !== entity.type) {
      throw new Error('Cannot replace entities of different types');
    }
    if (entityToReplace.id === entityToReplace.owner_id) {
      throw new Error('Cannot replace owner entity');
    }
    this.invalidateFinancialEntityById(entityIdToReplace);
    this.invalidateFinancialEntityById(targetEntityId);

    // convert ledger, misc expenses
    await replaceFinancialEntities.run({ targetEntityId, entityIdToReplace }, this.db);

    // convert business
    const businessReplacementPromise =
      entity.type === 'business'
        ? this.businessesProvider.replaceBusiness(targetEntityId, entityIdToReplace).then(() => {
            if (deleteEntity)
              this.businessesOperationProvider.deleteBusinessById(entityIdToReplace);
          })
        : Promise.resolve();

    // convert tax category
    const taxCategoryReplacementPromise =
      entity.type === 'tax_category'
        ? this.taxCategoriesProvider.replaceTaxCategory(
            targetEntityId,
            entityIdToReplace,
            deleteEntity,
          )
        : Promise.resolve();

    await Promise.all([businessReplacementPromise, taxCategoryReplacementPromise]);

    if (deleteEntity) {
      await this.deleteFinancialEntityById(entityIdToReplace);
    }
  }

  public invalidateFinancialEntityById(financialEntityId: string) {
    this.businessesProvider.invalidateBusinessById(financialEntityId);
    this.taxCategoriesProvider.invalidateTaxCategoryById(financialEntityId);
    this.allFinancialEntitiesCache = null;
    this.getFinancialEntityByIdLoader.clear(financialEntityId);
  }

  public clearCache() {
    this.taxCategoriesProvider.clearCache();
    this.businessesProvider.clearCache();
    this.getFinancialEntityByIdLoader.clearAll();
    this.allFinancialEntitiesCache = null;
  }
}
