import { Injector } from 'graphql-modules';
import type { FinancialAmount } from '../../../__generated__/types.js';
import { Currency, DocumentType } from '../../../shared/enums.js';
import { formatFinancialAmount } from '../../../shared/helpers/index.js';
import { isInvoice, isReceipt } from '../../documents/helpers/common.helper.js';
import { basicDocumentValidation } from '../../documents/helpers/validate-document.helper.js';
import { DocumentsProvider } from '../../documents/providers/documents.provider.js';
import { TaxCategoriesProvider } from '../../financial-entities/providers/tax-categories.provider.js';
import { getLedgerMeta } from '../../ledger/helpers/common.helper.js';
import { LedgerProvider } from '../../ledger/providers/ledger.provider.js';
import { MiscExpensesProvider } from '../../misc-expenses/providers/misc-expenses.provider.js';
import { getTransactionsMeta } from '../../transactions/helpers/common.helper.js';
import { TransactionsProvider } from '../../transactions/providers/transactions.provider.js';
import { ChargesProvider } from '../providers/charges.provider.js';

export async function calculateTotalAmount(
  chargeId: string,
  injector: Injector,
  defaultLocalCurrency: Currency,
): Promise<FinancialAmount | null> {
  try {
    const [
      charge,
      { transactionsAmount, transactionsCurrency },
      { documentsCurrency, documentsAmount },
    ] = await Promise.all([
      injector.get(ChargesProvider).getChargeByIdLoader.load(chargeId),
      getChargeTransactionsMeta(chargeId, injector),
      getChargeDocumentsMeta(chargeId, injector),
    ]);

    if (!charge) {
      throw new Error(`Charge ID="${chargeId}" not found`);
    }

    if (charge.type === 'PAYROLL' && transactionsAmount) {
      return formatFinancialAmount(transactionsAmount, defaultLocalCurrency);
    }
    if (documentsAmount && documentsCurrency) {
      return formatFinancialAmount(documentsAmount, documentsCurrency);
    }
    if (transactionsAmount && transactionsCurrency) {
      return formatFinancialAmount(transactionsAmount, transactionsCurrency);
    }
    return null;
  } catch (error) {
    console.error('Error calculating total amount for charge:', error);
    throw new Error('Failed to calculate total amount for charge', { cause: error });
  }
}

export async function getChargeBusinesses(chargeId: string, injector: Injector) {
  const [charge, transactions, documents, ledgerRecords, miscExpenses] = await Promise.all([
    injector.get(ChargesProvider).getChargeByIdLoader.load(chargeId),
    injector.get(TransactionsProvider).transactionsByChargeIDLoader.load(chargeId),
    injector.get(DocumentsProvider).getDocumentsByChargeIdLoader.load(chargeId),
    injector.get(LedgerProvider).getLedgerRecordsByChargesIdLoader.load(chargeId),
    injector.get(MiscExpensesProvider).getExpensesByChargeIdLoader.load(chargeId),
  ]);

  if (!charge) {
    throw new Error(`Charge ID="${chargeId}" not found`);
  }

  const allBusinessIdsSet = new Set<string>();
  const mainBusinessIdsSet = new Set<string>();

  transactions.map(t => {
    if (t.business_id) {
      allBusinessIdsSet.add(t.business_id);
      if (!t.is_fee) {
        mainBusinessIdsSet.add(t.business_id);
      }
    }
  });

  documents.map(d => {
    [d.creditor_id, d.debtor_id].map(id => {
      if (id && id !== charge.owner_id) {
        allBusinessIdsSet.add(id);
        mainBusinessIdsSet.add(id);
      }
    });
  });

  ledgerRecords.map(lr => {
    [lr.credit_entity1, lr.credit_entity2, lr.debit_entity1, lr.debit_entity2].map(id => {
      if (id && id !== charge.owner_id) {
        allBusinessIdsSet.add(id);
      }
    });
  });

  miscExpenses.map(me => {
    [me.creditor_id, me.debtor_id].map(id => {
      if (id && id !== charge.owner_id) {
        allBusinessIdsSet.add(id);
      }
    });
  });

  const allBusinessIds = Array.from(allBusinessIdsSet);
  const mainBusinessIds = Array.from(mainBusinessIdsSet);

  let mainBusinessId: string | null = null;

  if (mainBusinessIds.length === 1) {
    mainBusinessId = mainBusinessIds[0];
  }

  return {
    allBusinessIds,
    mainBusinessId,
  };
}

export async function getChargeDocumentsMeta(chargeId: string, injector: Injector) {
  const [charge, documents] = await Promise.all([
    injector.get(ChargesProvider).getChargeByIdLoader.load(chargeId),
    injector.get(DocumentsProvider).getDocumentsByChargeIdLoader.load(chargeId),
  ]);

  if (!charge) {
    throw new Error(`Charge ID="${chargeId}" not found`);
  }

  let invalidDocuments = false;
  let receiptAmount = 0;
  let receiptVatAmount: number | null = null;
  let receiptCount = 0;
  let invoiceAmount = 0;
  let invoiceVatAmount: number | null = null;
  let invoiceCount = 0;
  let proformaAmount: number | null = null;
  const currenciesSet = new Set<Currency>();
  const proformaCurrencySet = new Set<Currency>();
  let documentsMinAccountancyDate: Date | null = null;
  let documentsMinAnyDate: Date | null = null;
  let documentsMaxAccountancyDate: Date | null = null;
  let documentsMaxDate: Date | null = null;

  documents.map(d => {
    const amount = d.total_amount ?? 0;
    let factor = 1;
    if (d.debtor_id === charge.owner_id) {
      factor *= -1;
    }

    if (d.date) {
      documentsMinAnyDate ??= d.date;
      if (documentsMinAnyDate > d.date) {
        documentsMinAnyDate = d.date;
      }

      documentsMaxDate ??= d.date;
      if (documentsMaxDate < d.date) {
        documentsMaxDate = d.date;
      }
    }

    if (isInvoice(d.type)) {
      invoiceCount++;
      invoiceAmount += amount * factor;
      if (d.vat_amount != null) {
        invoiceVatAmount ??= 0;
        invoiceVatAmount += (d.vat_amount ?? 0) * factor;
      }
      if (d.date) {
        documentsMinAccountancyDate ??= d.date;
        if (documentsMinAccountancyDate > d.date) {
          documentsMinAccountancyDate = d.date;
        }

        documentsMaxAccountancyDate ??= d.date;
        if (documentsMaxAccountancyDate < d.date) {
          documentsMaxAccountancyDate = d.date;
        }
      }
      if (d.currency_code) {
        currenciesSet.add(d.currency_code as Currency);
      }
    }
    if (isReceipt(d.type)) {
      receiptCount++;
      receiptAmount += amount * factor;
      if (d.vat_amount != null) {
        receiptVatAmount ??= 0;
        receiptVatAmount += (d.vat_amount ?? 0) * factor;
      }
      if (d.date) {
        documentsMinAccountancyDate ??= d.date;
        if (documentsMinAccountancyDate > d.date) {
          documentsMinAccountancyDate = d.date;
        }

        documentsMaxAccountancyDate ??= d.date;
        if (documentsMaxAccountancyDate < d.date) {
          documentsMaxAccountancyDate = d.date;
        }
      }
      if (d.currency_code) {
        currenciesSet.add(d.currency_code as Currency);
      }
    }
    if (d.type === DocumentType.Proforma) {
      proformaAmount ??= 0;
      proformaAmount += amount * factor;

      if (d.currency_code) {
        proformaCurrencySet.add(d.currency_code as Currency);
      }
    }

    if (!basicDocumentValidation(d)) {
      invalidDocuments = true;
    }
  });

  const currencies =
    currenciesSet.size > 0 ? Array.from(currenciesSet) : Array.from(proformaCurrencySet);
  const documentsAmount =
    invoiceCount > 0 ? invoiceAmount : receiptCount > 0 ? receiptAmount : proformaAmount;

  return {
    receiptAmount,
    receiptCount,
    invoiceAmount,
    invoiceCount,
    documentsAmount,
    documentsVatAmount: invoiceVatAmount == null ? receiptVatAmount : invoiceVatAmount,
    documentsCount: documents.length,
    documentsCurrency: currencies.length === 1 ? currencies[0] : null,
    invalidDocuments,
    documentsMinDate: documentsMinAccountancyDate ?? (documentsMinAnyDate as Date | null),
    documentsMaxDate: documentsMaxAccountancyDate ?? (documentsMaxDate as Date | null),
  };
}

export async function getChargeTransactionsMeta(chargeId: string, injector: Injector) {
  const transactions = await injector
    .get(TransactionsProvider)
    .transactionsByChargeIDLoader.load(chargeId);

  return getTransactionsMeta(transactions);
}

export async function getChargeLedgerMeta(chargeId: string, injector: Injector) {
  const ledgerRecords = await injector
    .get(LedgerProvider)
    .getLedgerRecordsByChargesIdLoader.load(chargeId);

  return getLedgerMeta(ledgerRecords);
}

export async function getChargeTaxCategoryId(
  chargeId: string,
  injector: Injector,
): Promise<string | null> {
  const charge = await injector.get(ChargesProvider).getChargeByIdLoader.load(chargeId);
  if (!charge) {
    throw new Error(`Charge ID="${chargeId}" not found`);
  }
  if (charge.tax_category_id) {
    return charge.tax_category_id;
  }

  const { mainBusinessId } = await getChargeBusinesses(chargeId, injector);

  if (!mainBusinessId) {
    return null;
  }

  const taxCategory = await injector
    .get(TaxCategoriesProvider)
    .taxCategoryByBusinessIDsLoader.load(mainBusinessId);

  return taxCategory?.id ?? null;
}
