import { AdminContextProvider } from '../../../admin-context/providers/admin-context.provider.js';
import { BankDepositChargesProvider } from '../../../bank-deposits/providers/bank-deposit-charges.provider.js';
import { getChargeBusinesses } from '../../../charges/helpers/common.helper.js';
import { ExchangeProvider } from '../../../exchange-rates/providers/exchange.provider.js';
import { TaxCategoriesProvider } from '../../../financial-entities/providers/tax-categories.provider.js';
import { TransactionsProvider } from '../../../transactions/providers/transactions.provider.js';
import { identifyInterestTransactionIds } from '../../helpers/bank-deposit-ledger-generation.helper.js';
import { ledgerEntryFromMainTransaction } from '../../helpers/common-charge-ledger.helper.js';
import { calculateExchangeRate } from '../../helpers/exchange-ledger.helper.js';
import { convertLedgerRecordToProto, storeInitialGeneratedRecords, } from '../../helpers/ledgrer-storage.helper.js';
import { generateMiscExpensesLedger } from '../../helpers/misc-expenses-ledger.helper.js';
import { getFinancialAccountTaxCategoryId, getLedgerBalanceInfo, LedgerError, ledgerProtoToRecordsConverter, updateLedgerBalanceByEntry, validateTransactionBasicVariables, } from '../../helpers/utils.helper.js';
import { LedgerProvider } from '../../providers/ledger.provider.js';
export const generateLedgerRecordsForBankDeposit = async (charge, { insertLedgerRecordsIfNotExists }, { injector }) => {
    const chargeId = charge.id;
    const { ownerId, defaultLocalCurrency, general: { taxCategories: { exchangeRateTaxCategoryId }, }, } = await injector.get(AdminContextProvider).getVerifiedAdminContext();
    const errors = new Set();
    try {
        // validate ledger records are balanced
        const ledgerBalance = new Map();
        const transactionsPromise = injector
            .get(TransactionsProvider)
            .transactionsByChargeIDLoader.load(chargeId);
        const bankDepositTransactionsPromise = injector
            .get(BankDepositChargesProvider)
            .getDepositTransactionsByChargeId(chargeId);
        const [transactions, bankDepositTransactions, { mainBusinessId }] = await Promise.all([
            transactionsPromise,
            bankDepositTransactionsPromise,
            getChargeBusinesses(chargeId, injector),
        ]);
        const entriesPromises = [];
        const financialAccountLedgerEntries = [];
        const interestLedgerEntries = [];
        const feeFinancialAccountLedgerEntries = [];
        let isWithdrawal = false;
        let isBreathingDeposit = false;
        // generate ledger from transactions
        const [mainTransaction, interestTransactions] = transactions.reduce(([mainTransaction, interestTransactions], current) => {
            if (mainTransaction.id === current.id) {
                return [mainTransaction, interestTransactions];
            }
            return Number(current.amount) > Number(mainTransaction.amount)
                ? [current, [mainTransaction, ...interestTransactions]]
                : [mainTransaction, [current, ...interestTransactions]];
        }, [transactions[0], []]);
        if (Number(mainTransaction.amount) > 0) {
            isWithdrawal = true;
        }
        const mainTransactionPromise = async () => ledgerEntryFromMainTransaction(mainTransaction, injector, chargeId, charge.owner_id, mainBusinessId ?? undefined)
            .then(ledgerEntry => {
            financialAccountLedgerEntries.push(ledgerEntry);
            updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance, defaultLocalCurrency);
        })
            .catch(e => {
            if (e instanceof LedgerError) {
                errors.add(e.message);
            }
            else {
                throw e;
            }
        });
        // create a ledger record for fee transactions
        const interestTransactionsPromises = interestTransactions.map(async (transaction) => {
            // for each transaction, create a ledger record
            const { currency, valueDate, transactionBusinessId } = validateTransactionBasicVariables(transaction);
            const businessTaxCategory = await injector
                .get(TaxCategoriesProvider)
                .taxCategoryByBusinessIDsLoader.load(transactionBusinessId);
            if (!businessTaxCategory) {
                errors.add(`Business ID="${transactionBusinessId}" is missing tax category`);
                return;
            }
            let amount = Number(transaction.amount);
            let foreignAmount = 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 = {
                id: transaction.id,
                invoiceDate: transaction.event_date,
                valueDate,
                currency,
                creditAccountID1: isCreditorCounterparty ? businessTaxCategory.id : accountTaxCategoryId,
                creditAmount1: foreignAmount ? Math.abs(foreignAmount) : undefined,
                localCurrencyCreditAmount1: Math.abs(amount),
                debitAccountID1: isCreditorCounterparty ? accountTaxCategoryId : businessTaxCategory.id,
                debitAmount1: foreignAmount ? Math.abs(foreignAmount) : undefined,
                localCurrencyDebitAmount1: Math.abs(amount),
                description: transaction.source_description ?? undefined,
                reference: transaction.origin_key,
                isCreditorCounterparty,
                ownerId: charge.owner_id,
                currencyRate: transaction.currency_rate ? Number(transaction.currency_rate) : undefined,
                chargeId,
            };
            interestLedgerEntries.push(ledgerEntry);
            updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance, defaultLocalCurrency);
        });
        // generate ledger from misc expenses
        const expensesLedgerPromise = generateMiscExpensesLedger(charge, injector).then(entries => {
            entries.map(entry => {
                entry.ownerId = charge.owner_id;
                feeFinancialAccountLedgerEntries.push(entry);
                updateLedgerBalanceByEntry(entry, ledgerBalance, defaultLocalCurrency);
            });
        });
        entriesPromises.push(mainTransactionPromise(), ...interestTransactionsPromises, expensesLedgerPromise);
        await Promise.all(entriesPromises);
        const miscLedgerEntries = [];
        const miscLedgerEntriesPromise = async () => {
            const mainLedgerEntry = financialAccountLedgerEntries[0];
            if (!mainLedgerEntry) {
                throw new LedgerError('Main ledger entry not found');
            }
            const depositTransactions = bankDepositTransactions.filter(t => Number(t.amount) < 0);
            if (depositTransactions.length === 0 && isWithdrawal) {
                throw new LedgerError('Deposit transaction not found');
            }
            if (depositTransactions.length > 1) {
                isBreathingDeposit = true;
                // Validate consistent currency
                const depositCurrency = depositTransactions[0].currency;
                for (const tx of depositTransactions) {
                    if (tx.currency !== depositCurrency) {
                        throw new LedgerError(`Deposit has inconsistent currencies: found ${tx.currency} and ${depositCurrency}`);
                    }
                }
                // If currency is local, no revaluation needed
                if (depositCurrency === defaultLocalCurrency) {
                    return;
                }
                // Filter out interest transactions via shared helper
                const interestTransactionIds = identifyInterestTransactionIds(depositTransactions, {
                    getId: tx => tx.id,
                    getChargeId: tx => tx.charge_id,
                    getAmount: tx => Number(tx.amount),
                });
                const nonInterestTransactions = depositTransactions.filter(tx => !interestTransactionIds.has(tx.id));
                // Find latest deposit/withdrawal transaction prior to current
                const mainTxDate = mainTransaction.debit_date ?? mainTransaction.event_date;
                const prevTransactions = nonInterestTransactions.filter(tx => {
                    const txDate = tx.debit_date ?? tx.event_date;
                    if (txDate < mainTxDate) {
                        return true;
                    }
                    if (txDate.getTime() === mainTxDate.getTime()) {
                        // Same date: use ID ordering
                        return tx.id < mainTransaction.id;
                    }
                    return false;
                });
                if (prevTransactions.length === 0) {
                    throw new LedgerError('No previous deposit transaction found for revaluation');
                }
                // Sort by date desc, then by id desc to get the latest
                prevTransactions.sort((a, b) => {
                    const dateA = a.debit_date ?? a.event_date;
                    const dateB = b.debit_date ?? b.event_date;
                    if (dateB.getTime() !== dateA.getTime()) {
                        return dateB.getTime() - dateA.getTime();
                    }
                    return b.id.localeCompare(a.id);
                });
                const prevAction = prevTransactions[0];
                const prevActionDate = prevAction.debit_date ?? prevAction.event_date;
                // Calculate origin balance at prev action (sum of all non-interest up to and including prev action)
                let originBalance = 0;
                for (const tx of nonInterestTransactions) {
                    const txDate = tx.debit_date ?? tx.event_date;
                    if (txDate < prevActionDate ||
                        (txDate.getTime() === prevActionDate.getTime() && tx.id <= prevAction.id)) {
                        originBalance += Number(tx.amount);
                    }
                }
                // Get exchange rate for prev action and calculate prev action local balance
                const prevExchangeRate = await injector
                    .get(ExchangeProvider)
                    .getExchangeRates(depositCurrency, defaultLocalCurrency, prevActionDate);
                const prevLocalBalance = originBalance * prevExchangeRate;
                // Get current exchange rate
                const currentExchangeRate = await injector
                    .get(ExchangeProvider)
                    .getExchangeRates(depositCurrency, defaultLocalCurrency, mainLedgerEntry.valueDate);
                // Calculate revaluation: (origin balance * current rate) - prev local balance
                const rawRevaluationAmount = originBalance * currentExchangeRate - prevLocalBalance;
                const revaluationAmount = Math.abs(rawRevaluationAmount);
                if (revaluationAmount > 0) {
                    const isCreditorCounterparty = rawRevaluationAmount > 0;
                    const mainAccountId = mainLedgerEntry.creditAccountID1;
                    const revaluationLedgerEntry = {
                        id: mainTransaction.id + '|revaluation',
                        creditAccountID1: isCreditorCounterparty ? mainAccountId : exchangeRateTaxCategoryId,
                        localCurrencyCreditAmount1: revaluationAmount,
                        debitAccountID1: isCreditorCounterparty ? exchangeRateTaxCategoryId : mainAccountId,
                        localCurrencyDebitAmount1: revaluationAmount,
                        description: 'Revaluation ledger record',
                        isCreditorCounterparty,
                        invoiceDate: mainLedgerEntry.valueDate,
                        valueDate: mainLedgerEntry.valueDate,
                        currency: defaultLocalCurrency,
                        ownerId: mainLedgerEntry.ownerId,
                        chargeId,
                    };
                    miscLedgerEntries.push(revaluationLedgerEntry);
                    updateLedgerBalanceByEntry(revaluationLedgerEntry, ledgerBalance, defaultLocalCurrency);
                }
            }
            else if (isWithdrawal) {
                const mainBusinessBalance = mainTransaction.business_id
                    ? ledgerBalance.get(mainTransaction.business_id)
                    : undefined;
                const mainBusinessBalanceByCurrency = mainTransaction.currency === defaultLocalCurrency
                    ? mainBusinessBalance?.amount
                    : mainBusinessBalance?.foreignAmounts?.[mainTransaction.currency]?.foreign;
                const mainBusinessAbsBalance = mainBusinessBalanceByCurrency
                    ? Math.abs(mainBusinessBalanceByCurrency)
                    : undefined;
                const initialDepositTransaction = depositTransactions[0];
                const depositTransactionAmount = Math.abs(Number(initialDepositTransaction.amount));
                if (mainBusinessAbsBalance !== depositTransactionAmount ||
                    mainTransaction.currency !== initialDepositTransaction.currency) {
                    throw new LedgerError('Deposit transaction does not match the withdrawal transaction');
                }
                const depositLedgerRecords = await injector
                    .get(LedgerProvider)
                    .getLedgerRecordsByChargesIdLoader.load(initialDepositTransaction.charge_id);
                if (depositLedgerRecords.length !== 1) {
                    if (depositLedgerRecords.length === 0) {
                        throw new LedgerError('Deposit ledger record not found');
                    }
                    throw new LedgerError('Multiple deposit ledger records found');
                }
                const depositLedgerRecord = depositLedgerRecords[0];
                if (Number.isNaN(depositLedgerRecord.credit_local_amount1)) {
                    throw new LedgerError('Deposit ledger record has invalid local amount');
                }
                if (depositLedgerRecord.debit_entity1) {
                    updateLedgerBalanceByEntry(convertLedgerRecordToProto(depositLedgerRecord, ownerId), ledgerBalance, defaultLocalCurrency);
                }
                if (mainTransaction.currency !== defaultLocalCurrency) {
                    const rawAmount = Number(depositLedgerRecord.credit_local_amount1) -
                        mainLedgerEntry.localCurrencyCreditAmount1;
                    const amount = Math.abs(rawAmount);
                    const isCreditorCounterparty = rawAmount > 0;
                    const mainAccountId = mainLedgerEntry.creditAccountID1;
                    // validate exchange rate
                    try {
                        const exchangeAmount = calculateExchangeRate(mainLedgerEntry.creditAccountID1, [
                            ...financialAccountLedgerEntries,
                            convertLedgerRecordToProto(depositLedgerRecord, ownerId),
                        ], defaultLocalCurrency);
                        if ((!exchangeAmount && !!amount) ||
                            (exchangeAmount && Math.abs(exchangeAmount) !== amount)) {
                            throw new LedgerError('Exchange rate error');
                        }
                    }
                    catch (e) {
                        errors.add(e.message);
                    }
                    const exchangeLedgerEntry = {
                        id: mainTransaction.id + '|fee', // NOTE: this field is dummy
                        creditAccountID1: isCreditorCounterparty ? mainAccountId : exchangeRateTaxCategoryId,
                        localCurrencyCreditAmount1: amount,
                        debitAccountID1: isCreditorCounterparty ? exchangeRateTaxCategoryId : mainAccountId,
                        localCurrencyDebitAmount1: amount,
                        description: 'Exchange ledger record',
                        isCreditorCounterparty,
                        invoiceDate: mainLedgerEntry.valueDate,
                        valueDate: mainLedgerEntry.valueDate,
                        currency: defaultLocalCurrency,
                        ownerId: mainLedgerEntry.ownerId,
                        chargeId,
                    };
                    miscLedgerEntries.push(exchangeLedgerEntry);
                    updateLedgerBalanceByEntry(exchangeLedgerEntry, ledgerBalance, defaultLocalCurrency);
                }
            }
            return;
        };
        await miscLedgerEntriesPromise().catch(e => {
            if (e instanceof LedgerError) {
                errors.add(e.message);
            }
            else {
                throw e;
            }
        });
        const records = [
            ...financialAccountLedgerEntries,
            ...interestLedgerEntries,
            ...feeFinancialAccountLedgerEntries,
            ...miscLedgerEntries,
        ];
        if (insertLedgerRecordsIfNotExists) {
            await storeInitialGeneratedRecords(charge.id, records, injector);
        }
        const allowedUnbalancedBusinesses = new Set();
        const mainLedgerEntry = financialAccountLedgerEntries[0];
        if (!mainLedgerEntry.isCreditorCounterparty) {
            allowedUnbalancedBusinesses.add(mainLedgerEntry.debitAccountID1);
        }
        if (mainBusinessId && isBreathingDeposit) {
            allowedUnbalancedBusinesses.add(mainBusinessId);
        }
        const ledgerBalanceInfo = await getLedgerBalanceInfo(injector, ledgerBalance, errors, allowedUnbalancedBusinesses);
        return {
            records: ledgerProtoToRecordsConverter(records),
            charge,
            balance: ledgerBalanceInfo,
            errors: Array.from(errors),
        };
    }
    catch (e) {
        return {
            __typename: 'CommonError',
            message: `Failed to generate ledger records for charge ID="${chargeId}"\n${e}`,
        };
    }
};
//# sourceMappingURL=bank-deposit-ledger-generation.resolver.js.map