import type {
  Maybe,
  ResolverFn,
  ResolversParentTypes,
  ResolversTypes,
} from '../../../../../__generated__/types.js';
import { EMPTY_UUID } from '../../../../../shared/constants.js';
import type { LedgerProto } from '../../../../../shared/types/index.js';
import { AdminContextProvider } from '../../../../admin-context/providers/admin-context.provider.js';
import { FinancialEntitiesProvider } from '../../../../financial-entities/providers/financial-entities.provider.js';
import {
  decorateLedgerRecords,
  getProfitLossReportAmounts,
  type DecoratedLedgerRecord,
} from '../../../../reports/helpers/profit-and-loss.helper.js';
import {
  calculateCumulativeRnDExpenses,
  calculateTaxAmounts,
} from '../../../../reports/helpers/tax.helper.js';
import { storeInitialGeneratedRecords } from '../../../helpers/ledgrer-storage.helper.js';
import { generateMiscExpensesLedger } from '../../../helpers/misc-expenses-ledger.helper.js';
import { ledgerProtoToRecordsConverter } from '../../../helpers/utils.helper.js';
import { LedgerProvider } from '../../../providers/ledger.provider.js';

export const generateLedgerRecordsForTaxExpenses: ResolverFn<
  Maybe<ResolversTypes['GeneratedLedgerRecords']>,
  ResolversParentTypes['Charge'],
  GraphQLModules.Context,
  { insertLedgerRecordsIfNotExists: boolean }
> = async (charge, { insertLedgerRecordsIfNotExists }, { injector }) => {
  try {
    const {
      defaultLocalCurrency,
      authorities: { taxBusinessId, taxExpensesTaxCategoryId },
    } = await injector.get(AdminContextProvider).getVerifiedAdminContext();
    if (!charge.user_description) {
      return {
        __typename: 'CommonError',
        message: `Tax expenses charge must include user description with designated year`,
      };
    }

    // get revaluation date - search for "yyyy-mm-dd" in description
    const dateRegex = /\b(\d{4})\b/;
    const matches = charge.user_description.match(dateRegex);
    if (!matches?.length) {
      return {
        __typename: 'CommonError',
        message: `Tax expenses charge description must include full year`,
      };
    }

    const stringYear = matches[0];
    const year = Number(stringYear);
    if (Number.isNaN(year) || year < 2000 || year > new Date().getFullYear()) {
      return {
        __typename: 'CommonError',
        message: `Tax expenses charge description must include valid year (2000 - current year)`,
      };
    }

    const from = new Date(year - 2, 0, 1, 0, 0, 1);
    const to = new Date(year + 1, 0, 0);
    const ledgerRecords = await injector
      .get(LedgerProvider)
      .getLedgerRecordsByDates({ fromDate: from, toDate: to });

    const financialEntities = await injector
      .get(FinancialEntitiesProvider)
      .getAllFinancialEntities();

    const financialEntitiesDict = new Map(financialEntities.map(entity => [entity.id, entity]));

    const decoratedLedgerByYear = new Map<number, DecoratedLedgerRecord[]>();
    for (let year = from.getFullYear(); year <= to.getFullYear(); year++) {
      if (from.getFullYear() > to.getFullYear()) {
        break;
      }

      decoratedLedgerByYear.set(year, []);
    }

    ledgerRecords.map(record => {
      const year = record.invoice_date.getFullYear();
      const [decoratedRecord] = decorateLedgerRecords([record], financialEntitiesDict);
      decoratedLedgerByYear.get(year)?.push(decoratedRecord);
    });

    const profitLossByYear = new Map<number, ReturnType<typeof getProfitLossReportAmounts>>();

    function getProfitLossReportAmountsByYear(year: number) {
      let amounts = profitLossByYear.get(year);
      if (!amounts) {
        const decoratedLedgerRecords = decoratedLedgerByYear.get(year) ?? [];
        amounts = getProfitLossReportAmounts(decoratedLedgerRecords);
        profitLossByYear.set(year, amounts);
      }
      return amounts;
    }

    const cumulativeResearchAndDevelopmentExpensesAmount = calculateCumulativeRnDExpenses(
      year,
      decoratedLedgerByYear,
      profitLossByYear,
    );

    const taxableCumulativeResearchAndDevelopmentExpensesAmount =
      cumulativeResearchAndDevelopmentExpensesAmount / 3;

    const { researchAndDevelopmentExpenses, profitBeforeTax } =
      getProfitLossReportAmountsByYear(year);

    const { annualTaxExpenseAmount } = await calculateTaxAmounts(
      injector,
      year,
      decoratedLedgerByYear.get(year) ?? [],
      researchAndDevelopmentExpenses.amount * -1,
      taxableCumulativeResearchAndDevelopmentExpensesAmount,
      profitBeforeTax.amount,
    );

    const ledgerEntry: LedgerProto = {
      id: EMPTY_UUID,
      invoiceDate: new Date(year, 11, 31),
      valueDate: new Date(year, 11, 31),
      currency: defaultLocalCurrency,
      isCreditorCounterparty: true,
      creditAccountID1: taxBusinessId,
      debitAccountID1: taxExpensesTaxCategoryId,
      localCurrencyCreditAmount1: Math.abs(annualTaxExpenseAmount),
      localCurrencyDebitAmount1: Math.abs(annualTaxExpenseAmount),
      description: `Tax expenses for ${year}`,
      ownerId: charge.owner_id,
      chargeId: charge.id,
      currencyRate: 1,
    };

    const ledgerEntries = [ledgerEntry];

    // generate ledger from misc expenses
    await generateMiscExpensesLedger(charge, injector).then(entries => {
      entries.map(entry => {
        entry.ownerId = charge.owner_id;
        ledgerEntries.push(entry);
      });
    });

    if (insertLedgerRecordsIfNotExists) {
      await storeInitialGeneratedRecords(charge.id, ledgerEntries, injector);
    }

    return {
      records: ledgerProtoToRecordsConverter(ledgerEntries),
      charge,
      balance: {
        isBalanced: true,
        unbalancedEntities: [],
        balanceSum: 0,
        financialEntities: [],
      },
      errors: [],
    };
  } catch (e) {
    return {
      __typename: 'CommonError',
      message: `Failed to generate ledger records for charge ID="${charge.id}"\n${e}`,
    };
  }
};
