import DataLoader from 'dataloader';
import { CONTEXT, Inject, Injectable, Scope } from 'graphql-modules';
import { sql } from '@pgtyped/runtime';
import { dateToTimelessDateString, reassureOwnerIdExists } from '../../../shared/helpers/index.js';
import { TenantAwareDBClient } from '../../app-providers/tenant-db-client.js';
import { identifyInterestTransactionIds } from '../../ledger/helpers/bank-deposit-ledger-generation.helper.js';
import { TransactionsProvider } from '../../transactions/providers/transactions.provider.js';
import type {
  IDeleteBankDepositChargesByChargeIdsQuery,
  IGetAllDepositsWithTransactionsQuery,
  IGetDepositTransactionsByChargeIdQuery,
  IGetTransactionsByBankDepositsQuery,
  IInsertOrUpdateBankDepositChargeParams,
  IInsertOrUpdateBankDepositChargeQuery,
} from '../types.js';

const getTransactionsByBankDeposits = sql<IGetTransactionsByBankDepositsQuery>`
    SELECT 
      cbd.deposit_id,
      cbd.account_id as deposit_account_id,
      t.*
    FROM accounter_schema.charges_bank_deposits cbd
    INNER JOIN accounter_schema.transactions t
      ON cbd.id = t.charge_id
    WHERE deposit_id IN $$depositIds;`;

const getDepositTransactionsByChargeId = sql<IGetDepositTransactionsByChargeIdQuery>`
    SELECT
      cbd.deposit_id,
      cbd.account_id as deposit_account_id,
      t.*
    FROM accounter_schema.charges_bank_deposits cbd
    LEFT JOIN accounter_schema.transactions t
      ON cbd.id = t.charge_id
    WHERE deposit_id IN (
      SELECT cbd2.deposit_id
      FROM accounter_schema.charges_bank_deposits cbd2
      WHERE cbd2.id = $chargeId
    )
    AND ($includeCharge OR t.charge_id <> $chargeId);`;

const insertOrUpdateBankDepositCharge = sql<IInsertOrUpdateBankDepositChargeQuery>`
  INSERT INTO accounter_schema.charges_bank_deposits (id, deposit_id, account_id, owner_id)
  VALUES ($chargeId, $depositId, $accountId, $ownerId)
  ON CONFLICT (id) DO UPDATE SET deposit_id = EXCLUDED.deposit_id, account_id = EXCLUDED.account_id, owner_id = EXCLUDED.owner_id;
`;

const deleteBankDepositChargesByChargeIds = sql<IDeleteBankDepositChargesByChargeIdsQuery>`
  DELETE FROM accounter_schema.charges_bank_deposits
  WHERE id IN $$chargeIds;
`;

const getAllDepositsWithTransactions = sql<IGetAllDepositsWithTransactionsQuery>`
    SELECT 
      cbd.deposit_id,
      t.id,
      t.currency,
      t.debit_date,
      t.event_date,
      t.amount,
      t.current_balance,
      t.charge_id
    FROM accounter_schema.charges_bank_deposits cbd
    LEFT JOIN accounter_schema.transactions t
        ON cbd.id = t.charge_id
    WHERE cbd.deposit_id IS NOT NULL
    ORDER BY cbd.deposit_id, COALESCE(t.debit_date, t.event_date);`;

@Injectable({
  scope: Scope.Operation,
  global: true,
})
export class BankDepositChargesProvider {
  constructor(
    private db: TenantAwareDBClient,
    private transactionsProvider: TransactionsProvider,
    @Inject(CONTEXT) private context: GraphQLModules.GlobalContext,
  ) {}

  private async batchTransactionsByBankDeposits(depositIds: readonly string[]) {
    const transactions = await getTransactionsByBankDeposits.run(
      {
        depositIds,
      },
      this.db,
    );
    return depositIds.map(id => transactions.filter(t => t.deposit_id === id));
  }

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

  public getDepositTransactionsByChargeId(chargeId: string, includeCharge = false) {
    return getDepositTransactionsByChargeId.run({ chargeId, includeCharge }, this.db);
  }

  public insertOrUpdateBankDepositCharge(params: IInsertOrUpdateBankDepositChargeParams) {
    return insertOrUpdateBankDepositCharge.run(
      reassureOwnerIdExists(params, this.context),
      this.db,
    );
  }

  public async getAllDepositsWithMetadata() {
    const rows = await getAllDepositsWithTransactions.run(undefined, this.db);

    // Group transactions by deposit_id
    const depositMap = new Map<
      string,
      {
        id: string;
        currency: string | null;
        openDate: Date | null;
        closeDate: Date | null;
        currentBalance: number;
        totalInterest: number;
        totalDeposit: number;
        currencyError: string[];
        transactionIds: string[];
      }
    >();

    // Identify interest transactions using shared helper
    const interestTransactionIds = identifyInterestTransactionIds(rows, {
      getId: r => r.id,
      getChargeId: r => r.charge_id,
      getAmount: r => Number(r.amount ?? 0),
    });

    for (const row of rows) {
      if (!row.deposit_id) continue;

      if (!depositMap.has(row.deposit_id)) {
        depositMap.set(row.deposit_id, {
          id: row.deposit_id,
          currency: row.currency,
          openDate: row.debit_date ?? row.event_date,
          closeDate: null,
          currentBalance: 0,
          totalInterest: 0,
          totalDeposit: 0,
          currencyError: [],
          transactionIds: [],
        });
      }

      const deposit = depositMap.get(row.deposit_id)!;
      deposit.transactionIds.push(row.id);

      // Validate single currency
      if (deposit.currency && row.currency && deposit.currency !== row.currency) {
        const alreadyInErrors = deposit.currencyError.includes(row.id);
        if (!alreadyInErrors) {
          deposit.currencyError.push(row.id);
        }
      }

      // Update openDate (earliest transaction)
      const txDate = row.debit_date ?? row.event_date;
      if (!deposit.openDate || (txDate && txDate < deposit.openDate)) {
        deposit.openDate = txDate;
      }

      // Track balance and interest separately
      if (row.amount) {
        const amount = Number(row.amount);
        if (interestTransactionIds.has(row.id)) {
          deposit.totalInterest += amount;
        } else {
          deposit.currentBalance += amount;
          if (amount > 0) {
            deposit.totalDeposit += amount;
          }
        }
      }

      // If balance reaches zero, update closeDate (interest doesn't affect closure)
      if (Math.abs(deposit.currentBalance) < 0.005 && txDate) {
        deposit.closeDate = txDate;
      }
    }

    // Ensure closeDate reflects final state: if not closed, closeDate must be null
    for (const d of depositMap.values()) {
      if (Math.abs(d.currentBalance) >= 0.005) {
        d.closeDate = null;
      }
    }

    return Array.from(depositMap.values()).map(deposit => ({
      id: deposit.id,
      currency: deposit.currency,
      openDate: deposit.openDate ? dateToTimelessDateString(deposit.openDate) : null,
      closeDate: deposit.closeDate ? dateToTimelessDateString(deposit.closeDate) : null,
      currentBalance: deposit.currentBalance,
      totalInterest: deposit.totalInterest,
      totalDeposit: deposit.totalDeposit,
      currencyError: deposit.currencyError,
      transactionIds: deposit.transactionIds,
    }));
  }

  public async createDeposit(currency: string) {
    // Generate unique deposit ID using timestamp and random suffix
    const depositId = `${currency}-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;

    return {
      id: depositId,
      currency,
      openDate: null,
      closeDate: null,
      currentBalance: 0,
      totalInterest: 0,
      currencyError: [],
      transactionIds: [],
    };
  }

  public async deleteChargeDepositsByChargeIds(chargeIds: string[]) {
    return deleteBankDepositChargesByChargeIds.run({ chargeIds }, this.db);
  }

  public async assignChargeToDeposit(chargeId: string, depositId: string) {
    const [depositTransactions, transactions] = await Promise.all([
      // Get target deposit transactions to validate currency
      this.getTransactionsByBankDepositLoader.load(depositId),
      this.transactionsProvider.transactionsByChargeIDLoader.load(chargeId),
    ]);

    let accountId: string | null = null;

    // Check currency conflict
    if (depositTransactions.length > 0) {
      const depositCurrency = depositTransactions[0].currency;
      const depositAccountId = depositTransactions[0].deposit_account_id;
      for (const transaction of transactions) {
        const transactionCurrency = transaction.currency;
        if (depositCurrency && transactionCurrency !== depositCurrency) {
          throw new Error(
            `Currency conflict: Transaction currency (${transactionCurrency}) does not match deposit currency (${depositCurrency})`,
          );
        }
        if (depositAccountId !== transaction.account_id) {
          throw new Error(
            `Account conflict: Transaction account (${transaction.account_id}) does not match deposit account (${depositAccountId})`,
          );
        }
      }
      accountId = depositAccountId;
    } else {
      for (const transaction of transactions) {
        accountId ??= transaction.account_id;
        if (accountId !== transaction.account_id) {
          throw new Error(`Account conflict: Transactions accounts are inconsistent`);
        }
      }
    }

    await this.insertOrUpdateBankDepositCharge({ chargeId, depositId, accountId });

    // Return updated deposit metadata
    const allDeposits = await this.getAllDepositsWithMetadata();
    const updatedDeposit = allDeposits.find(d => d.id === depositId);

    if (!updatedDeposit) {
      throw new Error('Deposit not found after assignment');
    }

    return updatedDeposit;
  }
}
