import type { Injector } from 'graphql-modules';
import { DECREASED_VAT_RATIO } from '../../../shared/constants.js';
import { dateToTimelessDateString, formatCurrency } from '../../../shared/helpers/index.js';
import type { LedgerProto, StrictLedgerProto } from '../../../shared/types/index.js';
import { AdminContextProvider } from '../../admin-context/providers/admin-context.provider.js';
import type { IGetChargesByIdsResult } from '../../charges/types.js';
import {
  validateDocumentAllocation,
  validateDocumentVat,
} from '../../documents/helpers/validate-document.helper.js';
import type { IGetDocumentsByChargeIdResult } from '../../documents/types.js';
import { ExchangeProvider } from '../../exchange-rates/providers/exchange.provider.js';
import { FinancialAccountsProvider } from '../../financial-accounts/providers/financial-accounts.provider.js';
import type { IGetTransactionsByChargeIdsResult } from '../../transactions/types.js';
import { VatProvider } from '../../vat/providers/vat.provider.js';
import { IGetBalanceCancellationByChargesIdsResult } from '../types.js';
import {
  getFinancialAccountTaxCategoryId,
  LedgerError,
  validateTransactionBasicVariables,
} from './utils.helper.js';

export async function ledgerEntryFromDocument(
  injector: Injector,
  document: IGetDocumentsByChargeIdResult,
  charge: IGetChargesByIdsResult,
  ownerId: string,
  taxCategoryId: string,
): Promise<StrictLedgerProto> {
  if (!document.date) {
    throw new LedgerError(`Document serial "${document.serial_number}" is missing the date`);
  }

  if (!document.debtor_id) {
    throw new LedgerError(`Document serial "${document.serial_number}" is missing the debtor`);
  }
  if (!document.creditor_id) {
    throw new LedgerError(`Document serial "${document.serial_number}" is missing the creditor`);
  }
  if (!document.total_amount) {
    throw new LedgerError(`Document serial "${document.serial_number}" is missing amount`);
  }
  if (!document.currency_code) {
    throw new LedgerError(`Document serial "${document.serial_number}" is missing currency code`);
  }

  const {
    defaultLocalCurrency,
    authorities: { inputVatTaxCategoryId, outputVatTaxCategoryId, propertyOutputVatTaxCategoryId },
  } = await injector.get(AdminContextProvider).getVerifiedAdminContext();

  let totalAmount = Math.abs(document.total_amount);

  const isCreditorCounterparty = document.debtor_id === ownerId;
  const counterpartyId = isCreditorCounterparty ? document.creditor_id : document.debtor_id;

  const debitAccountID1 = isCreditorCounterparty ? taxCategoryId : counterpartyId;
  const creditAccountID1 = isCreditorCounterparty ? counterpartyId : taxCategoryId;

  const currency = formatCurrency(document.currency_code);
  let foreignTotalAmount: number | null = null;
  let amountWithoutVat = totalAmount;
  let foreignAmountWithoutVat: number | null = null;
  let vatAmount = document.vat_amount == null ? null : Math.abs(document.vat_amount);
  let foreignVatAmount: number | null = null;
  let vatTaxCategory: string | null = null;

  if (vatAmount != null && charge.is_property) {
    vatAmount = vatAmount * DECREASED_VAT_RATIO; // Adjust VAT for property charges
  }

  const isCreditInvoice = document.type === 'CREDIT_INVOICE';
  if (vatAmount) {
    amountWithoutVat = amountWithoutVat - vatAmount;
    const adjustedOutputVatTaxCategoryId = charge.is_property
      ? propertyOutputVatTaxCategoryId
      : outputVatTaxCategoryId;
    vatTaxCategory =
      isCreditorCounterparty === isCreditInvoice
        ? inputVatTaxCategoryId
        : adjustedOutputVatTaxCategoryId;

    const vatValue = await injector
      .get(VatProvider)
      .getVatValueByDateLoader.load(dateToTimelessDateString(document.date));

    if (!vatValue) {
      throw new LedgerError(`VAT value is missing for document ID=${document.id}`);
    }

    validateDocumentVat(document, vatValue, message => {
      throw new LedgerError(message);
    });
    await validateDocumentAllocation(document, injector).then(valid => {
      if (!valid) {
        throw new LedgerError(`Allocation number missing for document [${document.serial_number}]`);
      }
    });
  }

  // handle non-local currencies
  if (document.currency_code !== defaultLocalCurrency) {
    let exchangeRate: number;
    if (document.exchange_rate_override) {
      exchangeRate = Number(document.exchange_rate_override);
    } else {
      exchangeRate = await injector
        .get(ExchangeProvider)
        .getExchangeRates(currency, defaultLocalCurrency, document.date);
    }

    // Set foreign amounts
    foreignTotalAmount = totalAmount;
    foreignAmountWithoutVat = amountWithoutVat;

    // calculate amounts in ILS
    totalAmount = exchangeRate * totalAmount;
    amountWithoutVat = exchangeRate * amountWithoutVat;
    if (vatAmount && vatAmount > 0) {
      foreignVatAmount = vatAmount;
      vatAmount = exchangeRate * vatAmount;
    }
  }

  let creditAccountID2: string | null = null;
  let debitAccountID2: string | null = null;
  let creditAmount1: number | null;
  let localCurrencyCreditAmount1: number;
  let debitAmount1: number | null;
  let localCurrencyDebitAmount1: number;
  let creditAmount2: number | null = null;
  let localCurrencyCreditAmount2: number | null = null;
  let debitAmount2: number | null = null;
  let localCurrencyDebitAmount2: number | null = null;
  if (isCreditorCounterparty) {
    localCurrencyCreditAmount1 = totalAmount;
    creditAmount1 = foreignTotalAmount;
    localCurrencyDebitAmount1 = amountWithoutVat;
    debitAmount1 = foreignAmountWithoutVat;

    if (vatAmount && vatAmount > 0) {
      // add vat to debtor2
      debitAmount2 = foreignVatAmount;
      localCurrencyDebitAmount2 = vatAmount;
      debitAccountID2 = vatTaxCategory;
    }
  } else {
    localCurrencyDebitAmount1 = totalAmount;
    debitAmount1 = foreignTotalAmount;
    localCurrencyCreditAmount1 = amountWithoutVat;
    creditAmount1 = foreignAmountWithoutVat;

    if (vatAmount && vatAmount > 0) {
      // add vat to creditor2
      creditAmount2 = foreignVatAmount;
      localCurrencyCreditAmount2 = vatAmount;
      creditAccountID2 = vatTaxCategory;
    }
  }

  const ledgerEntry: StrictLedgerProto = {
    id: document.id,
    invoiceDate: document.date,
    valueDate: document.date,
    currency,
    creditAccountID1,
    creditAmount1: creditAmount1 ?? undefined,
    localCurrencyCreditAmount1,
    debitAccountID1,
    debitAmount1: debitAmount1 ?? undefined,
    localCurrencyDebitAmount1,
    creditAccountID2: creditAccountID2 ?? undefined,
    creditAmount2: creditAmount2 ?? undefined,
    localCurrencyCreditAmount2: localCurrencyCreditAmount2 ?? undefined,
    debitAccountID2: debitAccountID2 ?? undefined,
    debitAmount2: debitAmount2 ?? undefined,
    localCurrencyDebitAmount2: localCurrencyDebitAmount2 ?? undefined,
    description: undefined,
    reference: document.serial_number ?? undefined,
    isCreditorCounterparty,
    isCreditInvoice,
    ownerId,
    chargeId: charge.id,
  };

  return ledgerEntry;
}

function getCreditCardAccountByTransactionReferenceAndDescription(
  sourceReference: string,
  sourceDescription: string | null,
): string | null {
  if (sourceReference === '8547') {
    return '6fed9073-f571-402e-9da0-ac0669f658d2'; // 9200
  }
  if (sourceReference === '13795') {
    if (sourceDescription?.includes('2260')) {
      return '0e86514c-9ddb-4c82-b731-6107f7293b1f'; //2260
    }
    if (sourceDescription?.includes('4333')) {
      return '782070ce-ec7e-4be0-ab69-46c89528bf04'; //4333
    }
  }
  if (sourceReference === '22200' && sourceDescription?.includes('מסטרקארד')) {
    return '782070ce-ec7e-4be0-ab69-46c89528bf04'; //4333
  }
  if (sourceReference === '11200' && sourceDescription?.includes('כ.א.ל')) {
    return '6fed9073-f571-402e-9da0-ac0669f658d2'; // 9200
  }
  return null;
}

export async function ledgerEntryFromMainTransaction(
  transaction: IGetTransactionsByChargeIdsResult,
  injector: Injector,
  chargeId: string,
  ownerId: string,
  businessId?: string,
  gotRelevantDocuments = false,
): Promise<StrictLedgerProto> {
  const { currency, valueDate, transactionBusinessId } =
    validateTransactionBasicVariables(transaction);

  let mainAccountId: string = transactionBusinessId;

  const {
    defaultLocalCurrency,
    financialAccounts: { internalWalletsIds },
  } = await injector.get(AdminContextProvider).getVerifiedAdminContext();

  if (
    !gotRelevantDocuments &&
    transaction.source_reference &&
    businessId &&
    internalWalletsIds.includes(businessId)
  ) {
    const account = await injector
      .get(FinancialAccountsProvider)
      .getFinancialAccountByAccountNumberLoader.load(transaction.source_reference);
    let accountId = account?.id ?? null;
    accountId ??= getCreditCardAccountByTransactionReferenceAndDescription(
      transaction.source_reference,
      transaction.source_description,
    );
    if (!accountId) {
      throw new LedgerError(
        `Transaction reference "${transaction.source_reference}" is missing account`,
      );
    }
    mainAccountId = await getFinancialAccountTaxCategoryId(injector, {
      ...transaction,
      account_id: accountId,
    });
  }

  let amount = Number(transaction.amount);
  let foreignAmount: number | undefined = undefined;

  if (currency !== defaultLocalCurrency) {
    // get exchange rate for currency
    const exchangeRate = await injector
      .get(ExchangeProvider)
      .getExchangeRates(currency, defaultLocalCurrency, valueDate);

    foreignAmount = amount;
    // calculate amounts in ILS
    amount = exchangeRate * amount;
  }

  const accountTaxCategoryId = await getFinancialAccountTaxCategoryId(injector, transaction);

  const isCreditorCounterparty = amount > 0;

  const ledgerEntry: StrictLedgerProto = {
    id: transaction.id,
    invoiceDate: transaction.event_date,
    valueDate,
    currency,
    creditAccountID1: isCreditorCounterparty ? mainAccountId : accountTaxCategoryId,
    creditAmount1: foreignAmount ? Math.abs(foreignAmount) : undefined,
    localCurrencyCreditAmount1: Math.abs(amount),
    debitAccountID1: isCreditorCounterparty ? accountTaxCategoryId : mainAccountId,
    debitAmount1: foreignAmount ? Math.abs(foreignAmount) : undefined,
    localCurrencyDebitAmount1: Math.abs(amount),
    description: transaction.source_description ?? undefined,
    reference: transaction.origin_key,
    isCreditorCounterparty,
    ownerId,
    currencyRate: transaction.currency_rate ? Number(transaction.currency_rate) : undefined,
    chargeId,
  };

  return ledgerEntry;
}

export async function ledgerEntryFromBalanceCancellation(
  balanceCancellation: IGetBalanceCancellationByChargesIdsResult,
  ledgerBalance: Map<string, { amount: number; entityId: string }>,
  financialAccountLedgerEntries: StrictLedgerProto[],
  chargeId: string,
  ownerId: string,
  injector: Injector,
): Promise<LedgerProto> {
  const {
    defaultLocalCurrency,
    general: {
      taxCategories: { balanceCancellationTaxCategoryId },
    },
  } = await injector.get(AdminContextProvider).getVerifiedAdminContext();
  const entityBalance = ledgerBalance.get(balanceCancellation.business_id);
  if (!entityBalance) {
    throw new LedgerError(
      `Balance cancellation for business ${balanceCancellation.business_id} redundant - already balanced`,
    );
  }

  const { amount, entityId } = entityBalance;

  const financialAccountEntry = financialAccountLedgerEntries.find(entry =>
    [
      entry.creditAccountID1,
      entry.creditAccountID2,
      entry.debitAccountID1,
      entry.debitAccountID2,
    ].includes(balanceCancellation.business_id),
  );
  if (!financialAccountEntry) {
    throw new LedgerError(
      `Balance cancellation for business ${balanceCancellation.business_id} failed - no financial account entry found`,
    );
  }

  let foreignAmount: number | undefined = undefined;

  if (
    financialAccountEntry.currency !== defaultLocalCurrency &&
    financialAccountEntry.currencyRate
  ) {
    foreignAmount = financialAccountEntry.currencyRate * amount;
  }

  const isCreditorCounterparty = amount > 0;

  const ledgerEntry: LedgerProto = {
    id: balanceCancellation.charge_id,
    invoiceDate: financialAccountEntry.invoiceDate,
    valueDate: financialAccountEntry.valueDate,
    currency: financialAccountEntry.currency,
    creditAccountID1: isCreditorCounterparty ? balanceCancellationTaxCategoryId : entityId,
    creditAmount1: foreignAmount ? Math.abs(foreignAmount) : undefined,
    localCurrencyCreditAmount1: Math.abs(amount),
    debitAccountID1: isCreditorCounterparty ? entityId : balanceCancellationTaxCategoryId,
    debitAmount1: foreignAmount ? Math.abs(foreignAmount) : undefined,
    localCurrencyDebitAmount1: Math.abs(amount),
    description: balanceCancellation.description ?? undefined,
    reference: financialAccountEntry.reference,
    isCreditorCounterparty,
    ownerId,
    currencyRate: financialAccountEntry.currencyRate,
    chargeId,
  };

  return ledgerEntry;
}

export function isRefundCharge(description?: string | null): boolean {
  if (!description) {
    return false;
  }
  const normalizedDescription = description.toLocaleLowerCase();
  return (
    normalizedDescription.includes('refund') || normalizedDescription.includes('reimbursement')
  );
}

export function getExchangeDates(financialAccountLedgerEntries: LedgerProto[]): Date {
  const timeValue = Math.max(
    ...financialAccountLedgerEntries.map(entry => entry.valueDate.getTime()),
  );
  const date = new Date(timeValue);

  return date;
}
