import { GraphQLError } from 'graphql';
import { type Injector } from 'graphql-modules';
import { ChargeTypeEnum } from '../../../shared/enums.js';
import { errorSimplifier } from '../../../shared/errors.js';
import { dateToTimelessDateString } from '../../../shared/helpers/index.js';
import { AdminContextProvider } from '../../admin-context/providers/admin-context.provider.js';
import { ScopeProvider } from '../../auth/providers/scope.provider.js';
import { getMinDate } from '../../ledger/helpers/ledger-lock.js';
import { generateLedgerRecordsForFinancialCharge } from '../../ledger/resolvers/ledger-generation/financial-ledger-generation.resolver.js';
import { MiscExpensesProvider } from '../../misc-expenses/providers/misc-expenses.provider.js';
import { getChargeType } from '../helpers/charge-type.js';
import { getChargeLedgerMeta } from '../helpers/common.helper.js';
import { generateAndTagCharge } from '../helpers/financial-charge.helper.js';
import { ChargesProvider } from '../providers/charges.provider.js';
import type { ChargesModule } from '../types.js';
import { commonChargeFields } from './common.js';

// Validate an explicit write-target business against the caller's memberships,
// then load that business's admin context (not the request's primary), so
// charge generation uses the correct per-business tax configuration.
async function getWriteTargetAdminContext(injector: Injector, ownerId: string) {
  const target = await injector.get(ScopeProvider).resolveWriteTarget(ownerId);
  const targetAdminContext = await injector
    .get(AdminContextProvider)
    .adminContextByOwnerIdLoader.load(target);
  if (!targetAdminContext) {
    throw new GraphQLError('Admin context not found for the target business', {
      extensions: { code: 'NOT_FOUND' },
    });
  }
  return targetAdminContext;
}

export const financialChargesResolvers: ChargesModule.Resolvers = {
  Query: {
    annualFinancialCharges: async (_, { ownerId, year }, { injector }) => {
      // This report is shaped around a single business's tax categories. When a
      // business is requested explicitly, validate it is within the read scope
      // and use its admin context; otherwise default to the request's primary.
      let targetAdminContext;
      if (ownerId) {
        const [scopedId] = await injector.get(ScopeProvider).getReadScope([ownerId]);
        targetAdminContext = await injector
          .get(AdminContextProvider)
          .adminContextByOwnerIdLoader.load(scopedId);
        if (!targetAdminContext) {
          throw new GraphQLError('Admin context not found for the requested business', {
            extensions: { code: 'NOT_FOUND' },
          });
        }
      } else {
        targetAdminContext = await injector.get(AdminContextProvider).getVerifiedAdminContext();
      }

      const charges = await injector.get(ChargesProvider).getChargesByFilters({
        ownerIds: [targetAdminContext.ownerId],
        type: 'FINANCIAL',
        fromAnyDate: year,
        toAnyDate: year,
      });

      return {
        id: `${year}-financial-charges`,
        revaluationCharge: charges.find(c =>
          c.business_array?.includes(
            targetAdminContext.general.taxCategories.exchangeRevaluationTaxCategoryId,
          ),
        ),
        taxExpensesCharge: charges.find(c =>
          c.business_array?.includes(targetAdminContext.authorities.taxExpensesTaxCategoryId),
        ),
        depreciationCharge: targetAdminContext.depreciation.accumulatedDepreciationTaxCategoryId
          ? charges.find(c =>
              c.business_array?.includes(
                targetAdminContext.depreciation.accumulatedDepreciationTaxCategoryId!,
              ),
            )
          : null,
        recoveryReserveCharge: targetAdminContext.salaries.recoveryReserveTaxCategoryId
          ? charges.find(c =>
              c.business_array?.includes(targetAdminContext.salaries.recoveryReserveTaxCategoryId!),
            )
          : null,
        vacationReserveCharge: targetAdminContext.salaries.vacationReserveTaxCategoryId
          ? charges.find(c =>
              c.business_array?.includes(targetAdminContext.salaries.vacationReserveTaxCategoryId!),
            )
          : null,
        bankDepositsRevaluationCharge: targetAdminContext.bankDeposits
          .bankDepositInterestIncomeTaxCategoryId
          ? charges.find(c =>
              c.business_array?.includes(targetAdminContext.bankDeposits.bankDepositBusinessId!),
            )
          : null,
      };
    },
  },
  Mutation: {
    generateRevaluationCharge: async (_, { date, ownerId }, context, info) => {
      const { injector } = context;
      const {
        ledgerLock,
        general: {
          taxCategories: { exchangeRevaluationTaxCategoryId },
        },
      } = await getWriteTargetAdminContext(injector, ownerId);
      if (ledgerLock && date <= ledgerLock) {
        throw new GraphQLError('Cannot generate revaluation charge for locked period');
      }
      try {
        const charge = await generateAndTagCharge(
          injector,
          ownerId,
          exchangeRevaluationTaxCategoryId,
          `Revaluation charge for ${date}`,
        );

        await generateLedgerRecordsForFinancialCharge(
          charge,
          { insertLedgerRecordsIfNotExists: true },
          context,
          info,
        );

        return charge;
      } catch (e) {
        throw errorSimplifier('Error generating revaluation charge', e);
      }
    },
    generateBankDepositsRevaluationCharge: async (_, { date, ownerId }, context, info) => {
      const { injector } = context;
      const {
        ledgerLock,
        bankDeposits: { bankDepositInterestIncomeTaxCategoryId },
      } = await getWriteTargetAdminContext(injector, ownerId);
      if (ledgerLock && date <= ledgerLock) {
        throw new GraphQLError('Cannot generate revaluation charge for locked period');
      }
      try {
        if (!bankDepositInterestIncomeTaxCategoryId) {
          throw new GraphQLError('Bank deposit interest income tax category missing');
        }
        const charge = await generateAndTagCharge(
          injector,
          ownerId,
          bankDepositInterestIncomeTaxCategoryId,
          `Bank deposits revaluation charge for ${date}`,
        );

        await generateLedgerRecordsForFinancialCharge(
          charge,
          { insertLedgerRecordsIfNotExists: true },
          context,
          info,
        );

        return charge;
      } catch (e) {
        throw errorSimplifier('Error generating bank deposits revaluation charge', e);
      }
    },
    generateTaxExpensesCharge: async (_, { year, ownerId }, context, info) => {
      const { injector } = context;
      const {
        ledgerLock,
        authorities: { taxExpensesTaxCategoryId },
      } = await getWriteTargetAdminContext(injector, ownerId);
      if (ledgerLock && `${year}-12-31` <= ledgerLock) {
        throw new GraphQLError('Cannot generate revaluation charge for locked period');
      }
      try {
        const charge = await generateAndTagCharge(
          injector,
          ownerId,
          taxExpensesTaxCategoryId,
          `Tax expenses charge for ${year.substring(0, 4)}`,
        );

        await generateLedgerRecordsForFinancialCharge(
          charge,
          { insertLedgerRecordsIfNotExists: true },
          context,
          info,
        );

        return charge;
      } catch (e) {
        throw errorSimplifier('Error generating tax expenses charge', e);
      }
    },
    generateDepreciationCharge: async (_, { year, ownerId }, context, info) => {
      const { injector } = context;
      const {
        ledgerLock,
        depreciation: { accumulatedDepreciationTaxCategoryId },
      } = await getWriteTargetAdminContext(injector, ownerId);
      if (ledgerLock && `${year}-12-31` <= ledgerLock) {
        throw new GraphQLError('Cannot generate revaluation charge for locked period');
      }
      if (!accumulatedDepreciationTaxCategoryId) {
        throw new GraphQLError('Accumulated depreciation tax category missing');
      }
      try {
        const charge = await generateAndTagCharge(
          injector,
          ownerId,
          accumulatedDepreciationTaxCategoryId,
          `Depreciation charge for ${year.substring(0, 4)}`,
        );

        await generateLedgerRecordsForFinancialCharge(
          charge,
          { insertLedgerRecordsIfNotExists: true },
          context,
          info,
        );

        return charge;
      } catch (e) {
        throw errorSimplifier('Error generating depreciation charge', e);
      }
    },
    generateRecoveryReserveCharge: async (_, { year, ownerId }, context, info) => {
      const { injector } = context;
      const {
        ledgerLock,
        salaries: { recoveryReserveTaxCategoryId },
      } = await getWriteTargetAdminContext(injector, ownerId);
      if (ledgerLock && `${year}-12-31` <= ledgerLock) {
        throw new GraphQLError('Cannot generate revaluation charge for locked period');
      }
      if (!recoveryReserveTaxCategoryId) {
        throw new GraphQLError('Recovery reserve tax category missing');
      }
      try {
        const charge = await generateAndTagCharge(
          injector,
          ownerId,
          recoveryReserveTaxCategoryId,
          `Recovery reserve charge for ${year.substring(0, 4)}`,
        );

        await generateLedgerRecordsForFinancialCharge(
          charge,
          { insertLedgerRecordsIfNotExists: true },
          context,
          info,
        );

        return charge;
      } catch (e) {
        throw errorSimplifier('Error generating recovery reserve charge', e);
      }
    },
    generateVacationReserveCharge: async (_, { year, ownerId }, context, info) => {
      const { injector } = context;
      const {
        ledgerLock,
        salaries: { vacationReserveTaxCategoryId },
      } = await getWriteTargetAdminContext(injector, ownerId);
      if (ledgerLock && `${year}-12-31` <= ledgerLock) {
        throw new GraphQLError('Cannot generate revaluation charge for locked period');
      }
      if (!vacationReserveTaxCategoryId) {
        throw new GraphQLError('Vacation reserve tax category missing');
      }

      try {
        const charge = await generateAndTagCharge(
          injector,
          ownerId,
          vacationReserveTaxCategoryId,
          `Vacation reserves charge for ${year.substring(0, 4)}`,
        );

        await generateLedgerRecordsForFinancialCharge(
          charge,
          { insertLedgerRecordsIfNotExists: true },
          context,
          info,
        );

        return charge;
      } catch (e) {
        throw errorSimplifier('Error generating vacation reserves charge', e);
      }
    },
    generateBalanceCharge: async (_, { description, balanceRecords }, context, info) => {
      const { injector } = context;

      if (!balanceRecords.length) {
        throw new GraphQLError('Balance charge must include balance records');
      }
      const { defaultTaxCategoryId, ownerId, ledgerLock } = await injector
        .get(AdminContextProvider)
        .getVerifiedAdminContext();

      if (!defaultTaxCategoryId) {
        throw new GraphQLError('Default tax category missing');
      }

      try {
        if (ledgerLock) {
          const minDate = getMinDate(
            balanceRecords.map(record => [new Date(record.invoiceDate), record.valueDate]).flat(),
          );
          if (minDate && dateToTimelessDateString(minDate) <= ledgerLock) {
            throw new GraphQLError('Cannot generate balance charge for locked period');
          }
        }

        const charge = await generateAndTagCharge(
          injector,
          ownerId,
          defaultTaxCategoryId,
          description,
        );

        await Promise.all(
          balanceRecords.map(async record => {
            if (!record.debtorId && !record.creditorId) {
              throw new GraphQLError(
                'Balance record must include at least one counterparty (debtor or creditor)',
              );
            }
            await injector
              .get(MiscExpensesProvider)
              .insertExpense({ ...record, chargeId: charge.id })
              .catch(() => {
                throw new GraphQLError('Error adding balance records');
              });
          }),
        );

        await generateLedgerRecordsForFinancialCharge(
          charge,
          { insertLedgerRecordsIfNotExists: true },
          context,
          info,
        );

        return charge;
      } catch (e) {
        throw errorSimplifier('Error generating balance charge', e);
      }
    },
  },
  FinancialCharge: {
    __isTypeOf: async (DbCharge, { injector }) =>
      (await getChargeType(DbCharge, injector).catch(error => {
        throw errorSimplifier('Failed to determine charge type', error);
      })) === ChargeTypeEnum.Financial,
    ...commonChargeFields,
    type: () => 'FINANCIAL',
    vat: () => null,
    totalAmount: () => null,
    property: () => false,
    isInvoicePaymentDifferentCurrency: () => false,
    minEventDate: async (DbCharge, _, { injector }) => {
      try {
        const { ledgerMinInvoiceDate } = await getChargeLedgerMeta(DbCharge.id, injector);
        return ledgerMinInvoiceDate;
      } catch (error) {
        throw errorSimplifier('Failed to fetch min event date', error);
      }
    },
    minDebitDate: async (DbCharge, _, { injector }) => {
      try {
        const { ledgerMinValueDate } = await getChargeLedgerMeta(DbCharge.id, injector);
        return ledgerMinValueDate;
      } catch (error) {
        throw errorSimplifier('Failed to fetch min debit date', error);
      }
    },
    maxEventDate: async (DbCharge, _, { injector }) => {
      try {
        const { ledgerMaxInvoiceDate } = await getChargeLedgerMeta(DbCharge.id, injector);
        return ledgerMaxInvoiceDate;
      } catch (error) {
        throw errorSimplifier('Failed to fetch max event date', error);
      }
    },
    maxDebitDate: async (DbCharge, _, { injector }) => {
      try {
        const { ledgerMaxValueDate } = await getChargeLedgerMeta(DbCharge.id, injector);
        return ledgerMaxValueDate;
      } catch (error) {
        throw errorSimplifier('Failed to fetch max debit date', error);
      }
    },
    // minDocumentsDate:
    // validationData:
    // metadata:
    yearsOfRelevance: () => null,
  },
};
