import DataLoader from 'dataloader';
import { Injectable, Scope } from 'graphql-modules';
import { sql } from '@pgtyped/runtime';
import { reassureOwnerIdExists } from '../../../shared/helpers/index.js';
import { AdminContextProvider } from '../../admin-context/providers/admin-context.provider.js';
import { TenantAwareDBClient } from '../../app-providers/tenant-db-client.js';
import type {
  IDeleteExpenseParams,
  IDeleteExpenseQuery,
  IGetExpensesByChargeIdsQuery,
  IGetExpensesByChargeIdsResult,
  IGetExpensesByFinancialEntityIdsQuery,
  IGetExpensesByIdsQuery,
  IInsertExpenseParams,
  IInsertExpenseQuery,
  IInsertExpensesParams,
  IInsertExpensesQuery,
  IReplaceMiscExpensesChargeIdParams,
  IReplaceMiscExpensesChargeIdQuery,
  IUpdateExpenseParams,
  IUpdateExpenseQuery,
} from '../types.js';

const getExpensesByChargeIds = sql<IGetExpensesByChargeIdsQuery>`
  SELECT *
  FROM accounter_schema.misc_expenses
  WHERE charge_id IN $$chargeIds;`;

const getExpensesByIds = sql<IGetExpensesByIdsQuery>`
  SELECT *
  FROM accounter_schema.misc_expenses
  WHERE id IN $$ids;`;

const getExpensesByFinancialEntityIds = sql<IGetExpensesByFinancialEntityIdsQuery>`
SELECT *
FROM accounter_schema.misc_expenses
WHERE creditor_id IN $$financialEntityIds OR debtor_id IN $$financialEntityIds;`;

const updateExpense = sql<IUpdateExpenseQuery>`
  UPDATE accounter_schema.misc_expenses
  SET
  charge_id = COALESCE(
    $chargeId,
    charge_id
  ),
  creditor_id = COALESCE(
    $creditorId,
    creditor_id
  ),
  debtor_id = COALESCE(
    $debtorId,
    debtor_id
  ),
  amount = COALESCE(
    $amount,
    amount
  ),
  currency = COALESCE(
    $currency,
    currency
  ),
  description = COALESCE(
    $description,
    description
  ),
  invoice_date = COALESCE(
    $invoiceDate,
    invoice_date
  ),
  value_date = COALESCE(
    $valueDate,
    value_date
  )
  WHERE
    id = $miscExpenseId
  RETURNING *;`;

const replaceMiscExpensesChargeId = sql<IReplaceMiscExpensesChargeIdQuery>`
    UPDATE accounter_schema.misc_expenses
      SET
      charge_id = $assertChargeID
    WHERE
      charge_id = $replaceChargeID
    RETURNING *
  `;

const insertExpense = sql<IInsertExpenseQuery>`
  INSERT INTO accounter_schema.misc_expenses (charge_id, creditor_id, debtor_id, amount, currency, description, invoice_date, value_date, owner_id)
  VALUES ($chargeId, $creditorId, $debtorId, $amount, $currency, $description, $invoiceDate, $valueDate, $ownerId)
  RETURNING *;`;

const insertExpenses = sql<IInsertExpensesQuery>`
  INSERT INTO accounter_schema.misc_expenses (charge_id, creditor_id, debtor_id, amount, currency, description, invoice_date, value_date, owner_id)
  VALUES $$miscExpenses(chargeId, creditorId, debtorId, amount, currency, description, invoiceDate, valueDate, ownerId)
  RETURNING *;`;

const deleteExpense = sql<IDeleteExpenseQuery>`
  DELETE FROM accounter_schema.misc_expenses
  WHERE id = $id
  RETURNING id;`;

@Injectable({
  scope: Scope.Operation,
  global: true,
})
export class MiscExpensesProvider {
  constructor(
    private db: TenantAwareDBClient,
    private adminContextProvider: AdminContextProvider,
  ) {}

  private async batchExpensesByChargeIds(chargeIds: readonly string[]) {
    const expenses = await getExpensesByChargeIds.run({ chargeIds }, this.db);
    const expensesByChargeId = new Map<string, IGetExpensesByChargeIdsResult[]>();
    expenses.map(expense => {
      if (expensesByChargeId.has(expense.charge_id)) {
        expensesByChargeId.get(expense.charge_id)?.push(expense);
      } else {
        expensesByChargeId.set(expense.charge_id, [expense]);
      }
    });
    return chargeIds.map(id => expensesByChargeId.get(id) ?? []);
  }

  public getExpensesByChargeIdLoader = new DataLoader((chargeIds: readonly string[]) =>
    this.batchExpensesByChargeIds(chargeIds),
  );

  private async batchExpensesByFinancialEntityIds(financialEntityIds: readonly string[]) {
    const expenses = await getExpensesByFinancialEntityIds.run({ financialEntityIds }, this.db);
    const expensesByFinancialEntityId = new Map<string, IGetExpensesByChargeIdsResult[]>();
    expenses.map(expense => {
      if (expensesByFinancialEntityId.has(expense.creditor_id)) {
        expensesByFinancialEntityId.get(expense.creditor_id)?.push(expense);
      } else {
        expensesByFinancialEntityId.set(expense.creditor_id, [expense]);
      }
      if (expensesByFinancialEntityId.has(expense.debtor_id)) {
        expensesByFinancialEntityId.get(expense.debtor_id)?.push(expense);
      } else {
        expensesByFinancialEntityId.set(expense.debtor_id, [expense]);
      }
    });
    return financialEntityIds.map(id => expensesByFinancialEntityId.get(id) ?? []);
  }

  public getExpensesByFinancialEntityIdLoader = new DataLoader(
    (financialEntityIds: readonly string[]) =>
      this.batchExpensesByFinancialEntityIds(financialEntityIds),
  );

  private async batchExpensesByIds(ids: readonly string[]) {
    const expenses = await getExpensesByIds.run({ ids }, this.db);
    return ids.map(id => expenses.find(expense => expense.id === id));
  }

  public getExpensesByIdLoader = new DataLoader((ids: readonly string[]) =>
    this.batchExpensesByIds(ids),
  );

  public async updateExpense(params: IUpdateExpenseParams) {
    if (params.miscExpenseId) await this.invalidateById(params.miscExpenseId);
    return updateExpense.run(params, this.db);
  }

  public async replaceMiscExpensesChargeId(params: IReplaceMiscExpensesChargeIdParams) {
    if (params.assertChargeID) {
      await this.invalidateByChargeId(params.assertChargeID);
    }
    if (params.replaceChargeID) {
      await this.invalidateByChargeId(params.replaceChargeID);
    }
    return replaceMiscExpensesChargeId.run(params, this.db);
  }

  public async insertExpense(params: IInsertExpenseParams) {
    if (params.chargeId) await this.invalidateByChargeId(params.chargeId);
    const { ownerId } = await this.adminContextProvider.getVerifiedAdminContext();
    return insertExpense.run(reassureOwnerIdExists(params, ownerId), this.db);
  }

  public async insertExpenses(params: IInsertExpensesParams) {
    const chargeIds = Array.from(
      new Set(params.miscExpenses.map(({ chargeId }) => chargeId)),
    ).filter(id => !!id) as string[];
    if (chargeIds.length) {
      await Promise.all(chargeIds.map(chargeId => this.invalidateByChargeId(chargeId)));
    }

    const { ownerId } = await this.adminContextProvider.getVerifiedAdminContext();
    return insertExpenses.run(
      {
        miscExpenses: params.miscExpenses.map(expense => ({
          ...expense,
          ownerId: expense.ownerId || ownerId,
        })),
      },
      this.db,
    );
  }

  public async deleteMiscExpense(params: IDeleteExpenseParams) {
    if (params.id) await this.invalidateById(params.id);
    return deleteExpense.run(params, this.db);
  }

  public async invalidateByChargeId(chargeId: string) {
    this.getExpensesByChargeIdLoader.clear(chargeId);
  }

  public async invalidateByFinancialEntityId(financialEntityId: string) {
    this.getExpensesByFinancialEntityIdLoader.clear(financialEntityId);
  }

  public async invalidateById(id: string) {
    const expense = await this.getExpensesByIdLoader.load(id);
    if (expense) {
      this.getExpensesByChargeIdLoader.clear(expense.charge_id);
      if (expense.creditor_id) {
        this.getExpensesByFinancialEntityIdLoader.clear(expense.creditor_id);
      }
      if (expense.debtor_id) {
        this.getExpensesByFinancialEntityIdLoader.clear(expense.debtor_id);
      }
    }
    this.getExpensesByIdLoader.clear(id);
  }

  public clearCache() {
    this.getExpensesByIdLoader.clearAll();
    this.getExpensesByChargeIdLoader.clearAll();
    this.getExpensesByFinancialEntityIdLoader.clearAll();
  }
}
