import { formatStringifyAmount } from '../../../../shared/helpers/index.js';
import { AdminContextProvider } from '../../../admin-context/providers/admin-context.provider.js';
import { calculateTotalAmount, getChargeBusinesses, getChargeDocumentsMeta, getChargeTaxCategoryId, } from '../../../charges/helpers/common.helper.js';
import { getDeelEmployeeId, isDeelDocument } from '../../../deel/helpers/deel.helper.js';
import { DocumentsProvider } from '../../../documents/providers/documents.provider.js';
import { BusinessesProvider } from '../../../financial-entities/providers/businesses.provider.js';
import { TransactionsProvider } from '../../../transactions/providers/transactions.provider.js';
import { getExchangeDates, isRefundCharge, ledgerEntryFromBalanceCancellation, ledgerEntryFromDocument, ledgerEntryFromMainTransaction, } from '../../helpers/common-charge-ledger.helper.js';
import { handleCrossYearLedgerEntries } from '../../helpers/cross-year-ledger.helper.js';
import { validateExchangeRate } from '../../helpers/exchange-ledger.helper.js';
import { getEntriesFromFeeTransaction, splitFeeTransactions, } from '../../helpers/fee-transactions.js';
import { storeInitialGeneratedRecords } from '../../helpers/ledgrer-storage.helper.js';
import { generateMiscExpensesLedger } from '../../helpers/misc-expenses-ledger.helper.js';
import { getLedgerBalanceInfo, LedgerError, ledgerProtoToRecordsConverter, multipleForeignCurrenciesBalanceEntries, updateLedgerBalanceByEntry, } from '../../helpers/utils.helper.js';
import { BalanceCancellationProvider } from '../../providers/balance-cancellation.provider.js';
import { UnbalancedBusinessesProvider } from '../../providers/unbalanced-businesses.provider.js';
export const generateLedgerRecordsForCommonCharge = async (charge, { insertLedgerRecordsIfNotExists }, { injector }) => {
    const { defaultLocalCurrency, defaultTaxCategoryId, general: { taxCategories: { incomeExchangeRateTaxCategoryId, exchangeRateTaxCategoryId }, }, } = await injector.get(AdminContextProvider).getVerifiedAdminContext();
    const chargeId = charge.id;
    const errors = new Set();
    try {
        // validate ledger records are balanced
        const ledgerBalance = new Map();
        function updateLedgerBalance(entry) {
            updateLedgerBalanceByEntry(entry, ledgerBalance, defaultLocalCurrency);
        }
        const dates = new Set();
        const currencies = new Set();
        const [{ invoiceCount, receiptCount }, formattedChargeAmount] = await Promise.all([
            getChargeDocumentsMeta(chargeId, injector),
            calculateTotalAmount(chargeId, injector, defaultLocalCurrency),
        ]);
        const gotRelevantDocuments = invoiceCount + receiptCount > 0;
        const documentsTaxCategoryIdPromise = async () => {
            if (charge.tax_category_id) {
                return charge.tax_category_id;
            }
            const taxCategoryId = await getChargeTaxCategoryId(chargeId, injector);
            if (taxCategoryId) {
                return taxCategoryId;
            }
            if (!gotRelevantDocuments) {
                return undefined;
            }
            errors.add('Tax category not found');
            return defaultTaxCategoryId;
        };
        const documentsPromise = gotRelevantDocuments
            ? injector.get(DocumentsProvider).getDocumentsByChargeIdLoader.load(chargeId)
            : Promise.resolve([]);
        const transactionsPromise = injector
            .get(TransactionsProvider)
            .transactionsByChargeIDLoader.load(chargeId);
        const unbalancedBusinessesPromise = injector
            .get(UnbalancedBusinessesProvider)
            .getChargeUnbalancedBusinessesByChargeIds.load(chargeId);
        const chargeBallanceCancellationsPromise = injector
            .get(BalanceCancellationProvider)
            .getBalanceCancellationByChargesIdLoader.load(chargeId);
        const [documentsTaxCategoryId, documents, transactions, unbalancedBusinesses, balanceCancellations, { mainBusinessId },] = await Promise.all([
            documentsTaxCategoryIdPromise(),
            documentsPromise,
            transactionsPromise,
            unbalancedBusinessesPromise,
            chargeBallanceCancellationsPromise,
            getChargeBusinesses(chargeId, injector),
        ]);
        const entriesPromises = [];
        const accountingLedgerEntries = [];
        const financialAccountLedgerEntries = [];
        const feeFinancialAccountLedgerEntries = [];
        const miscLedgerEntries = [];
        // generate ledger from documents
        if (gotRelevantDocuments) {
            // Get all relevant documents for charge
            const relevantDocuments = documents.filter(d => ['INVOICE', 'INVOICE_RECEIPT', 'CREDIT_INVOICE'].includes(d.type));
            // if no relevant documents found and business can settle with receipts, look for receipts
            if (!relevantDocuments.length && mainBusinessId) {
                const business = await injector
                    .get(BusinessesProvider)
                    .getBusinessByIdLoader.load(mainBusinessId);
                if (business?.can_settle_with_receipt) {
                    relevantDocuments.push(...documents.filter(d => d.type === 'RECEIPT'));
                }
            }
            // for each invoice - generate accounting ledger entry
            const documentsEntriesPromises = relevantDocuments.map(async (document) => {
                if (!documentsTaxCategoryId) {
                    return;
                }
                return ledgerEntryFromDocument(injector, document, charge, charge.owner_id, documentsTaxCategoryId)
                    .then(async (ledgerEntry) => {
                    await getDeelEmployeeId(injector, document, ledgerEntry, miscLedgerEntries, updateLedgerBalance);
                    accountingLedgerEntries.push(ledgerEntry);
                    updateLedgerBalance(ledgerEntry);
                    dates.add(ledgerEntry.valueDate.getTime());
                    currencies.add(ledgerEntry.currency);
                })
                    .catch(e => {
                    if (e instanceof LedgerError) {
                        errors.add(e.message);
                    }
                    else {
                        throw e;
                    }
                });
            });
            entriesPromises.push(...documentsEntriesPromises);
        }
        // generate ledger from transactions
        if (transactions.length > 0) {
            const { mainTransactions, feeTransactions } = splitFeeTransactions(transactions);
            // for each transaction, create a ledger record
            const mainTransactionsPromises = mainTransactions.map(async (transaction) => {
                const ledgerEntry = await ledgerEntryFromMainTransaction(transaction, injector, chargeId, charge.owner_id, mainBusinessId ?? undefined, gotRelevantDocuments).catch(e => {
                    if (e instanceof LedgerError) {
                        errors.add(e.message);
                    }
                    else {
                        throw e;
                    }
                });
                if (!ledgerEntry) {
                    return;
                }
                financialAccountLedgerEntries.push(ledgerEntry);
                updateLedgerBalance(ledgerEntry);
                dates.add(ledgerEntry.valueDate.getTime());
                currencies.add(ledgerEntry.currency);
            });
            // create a ledger record for fee transactions
            const feeTransactionsPromises = feeTransactions.map(async (transaction) => {
                const ledgerEntries = await getEntriesFromFeeTransaction(transaction, charge, injector).catch(e => {
                    if (e instanceof LedgerError) {
                        errors.add(e.message);
                    }
                    else {
                        throw e;
                    }
                });
                if (!ledgerEntries) {
                    return;
                }
                feeFinancialAccountLedgerEntries.push(...ledgerEntries);
                ledgerEntries.map(ledgerEntry => {
                    updateLedgerBalance(ledgerEntry);
                    dates.add(ledgerEntry.valueDate.getTime());
                    currencies.add(ledgerEntry.currency);
                });
            });
            entriesPromises.push(...mainTransactionsPromises, ...feeTransactionsPromises);
        }
        // generate ledger from misc expenses
        const expensesLedgerPromise = generateMiscExpensesLedger(charge, injector).then(entries => {
            entries.map(entry => {
                entry.ownerId = charge.owner_id;
                feeFinancialAccountLedgerEntries.push(entry);
                updateLedgerBalance(entry);
                dates.add(entry.valueDate.getTime());
                currencies.add(entry.currency);
            });
        });
        entriesPromises.push(expensesLedgerPromise);
        await Promise.all(entriesPromises);
        // generate ledger from balance cancellation
        for (const balanceCancellation of balanceCancellations) {
            try {
                const ledgerEntry = await ledgerEntryFromBalanceCancellation(balanceCancellation, ledgerBalance, financialAccountLedgerEntries, chargeId, charge.owner_id, injector);
                miscLedgerEntries.push(ledgerEntry);
                updateLedgerBalance(ledgerEntry);
            }
            catch (e) {
                if (e instanceof LedgerError) {
                    errors.add(e.message);
                    continue;
                }
                else {
                    throw e;
                }
            }
        }
        const allowedUnbalancedBusinesses = new Set(unbalancedBusinesses.map(({ business_id }) => business_id));
        // multiple currencies balance
        const businessBalance = ledgerBalance.get(mainBusinessId ?? '');
        if (mainBusinessId &&
            Object.keys(businessBalance?.foreignAmounts ?? {}).length >
                (charge.invoice_payment_currency_diff ? 0 : 1)) {
            const transactionEntries = financialAccountLedgerEntries.filter(entry => {
                if ([entry.creditAccountID1, entry.debitAccountID1].includes(mainBusinessId))
                    return true;
                return false;
            });
            const documentEntries = accountingLedgerEntries.filter(entry => {
                if ([entry.creditAccountID1, entry.debitAccountID1].includes(mainBusinessId))
                    return true;
                return false;
            });
            try {
                const entries = await multipleForeignCurrenciesBalanceEntries(injector, defaultLocalCurrency, documentEntries, transactionEntries, charge, businessBalance.foreignAmounts, charge.invoice_payment_currency_diff ?? false);
                for (const ledgerEntry of entries) {
                    miscLedgerEntries.push(ledgerEntry);
                    updateLedgerBalance(ledgerEntry);
                }
            }
            catch (e) {
                if (e instanceof LedgerError) {
                    errors.add(e.message);
                }
                else {
                    throw e;
                }
            }
        }
        // Add ledger completion entries
        const { balanceSum, isBalanced, unbalancedEntities, financialEntities } = await getLedgerBalanceInfo(injector, ledgerBalance, undefined, allowedUnbalancedBusinesses);
        if (errors.size) {
            const records = [
                ...financialAccountLedgerEntries,
                ...feeFinancialAccountLedgerEntries,
                ...accountingLedgerEntries,
                ...miscLedgerEntries,
            ];
            if (records.length && insertLedgerRecordsIfNotExists) {
                await storeInitialGeneratedRecords(charge.id, records, injector);
            }
            return {
                records: ledgerProtoToRecordsConverter(records),
                charge,
                balance: { balanceSum, isBalanced, unbalancedEntities },
                errors: Array.from(errors),
            };
        }
        if (Math.abs(balanceSum) > 0.005) {
            errors.add(`Failed to balance: ${formatStringifyAmount(balanceSum)} diff; ${unbalancedEntities.map(entity => entity.entityId).join(', ')} are not balanced`);
        }
        else if (!isBalanced) {
            // check if business doesn't require documents
            if (!accountingLedgerEntries.length && mainBusinessId) {
                const business = await injector
                    .get(BusinessesProvider)
                    .getBusinessByIdLoader.load(mainBusinessId);
                if (business?.no_invoices_required) {
                    const unbalancedBusinesses = unbalancedEntities.find(b => b.entityId === business.id);
                    if (unbalancedBusinesses) {
                        errors.add(`Business "${business.name}" is unbalanced (in the scope of this charge)`);
                    }
                    else {
                        errors.add('Some businesses are not balanced in the scope of this charge');
                    }
                    const records = [...financialAccountLedgerEntries, ...feeFinancialAccountLedgerEntries];
                    if (insertLedgerRecordsIfNotExists) {
                        await storeInitialGeneratedRecords(charge.id, records, injector);
                    }
                    return {
                        records: ledgerProtoToRecordsConverter(records),
                        charge,
                        balance: { balanceSum, isBalanced, unbalancedEntities },
                        errors: Array.from(errors),
                    };
                }
            }
            // check if exchange rate record is needed
            const hasMultipleDates = dates.size > 1;
            const foreignCurrencyCount = currencies.size - (currencies.has(defaultLocalCurrency) ? 1 : 0);
            const mightRequireExchangeRateRecord = (hasMultipleDates && !!foreignCurrencyCount) || foreignCurrencyCount >= 2;
            const unbalancedBusinesses = unbalancedEntities.filter(({ entityId }) => financialEntities.some(fe => fe.id === entityId && fe.type === 'business') &&
                !allowedUnbalancedBusinesses.has(entityId));
            if (mightRequireExchangeRateRecord && unbalancedBusinesses.length === 1) {
                const transactionEntry = financialAccountLedgerEntries[0];
                const entryDate = getExchangeDates(financialAccountLedgerEntries);
                const { entityId, balance } = unbalancedBusinesses[0];
                const amount = Math.abs(balance.raw);
                const isCreditorCounterparty = balance.raw < 0;
                let exchangeRateEntry = accountingLedgerEntries.find(entry => [entry.creditAccountID1, entry.debitAccountID1].includes(entityId));
                let exchangeRateTaxCategory = undefined;
                const deelCharge = documents.some(doc => isDeelDocument(doc));
                if (deelCharge) {
                    exchangeRateEntry ||= miscLedgerEntries.find(entry => [entry.creditAccountID1, entry.debitAccountID1].includes(entityId));
                }
                if (exchangeRateEntry) {
                    if (documentsTaxCategoryId) {
                        exchangeRateTaxCategory = documentsTaxCategoryId;
                    }
                    else {
                        exchangeRateTaxCategory =
                            exchangeRateEntry.debitAccountID1 === entityId
                                ? exchangeRateEntry.creditAccountID1
                                : exchangeRateEntry.debitAccountID1;
                    }
                    if (deelCharge) {
                        // If Deel - override to generic exchange rate tax category
                        exchangeRateTaxCategory = exchangeRateTaxCategoryId;
                    }
                    const isRefund = isRefundCharge(charge.user_description);
                    const isIncomeCharge = !isRefund && !!formattedChargeAmount && formattedChargeAmount.raw > 0;
                    if (isIncomeCharge) {
                        exchangeRateTaxCategory = incomeExchangeRateTaxCategoryId;
                    }
                }
                else if (charge.documents_optional_flag) {
                    exchangeRateTaxCategory = exchangeRateTaxCategoryId;
                }
                // validate exchange rate
                const validation = validateExchangeRate(entityId, [
                    ...accountingLedgerEntries,
                    ...financialAccountLedgerEntries,
                    ...feeFinancialAccountLedgerEntries,
                    ...miscLedgerEntries,
                ], balance.raw, defaultLocalCurrency);
                if (validation === true) {
                    if (exchangeRateTaxCategory) {
                        const ledgerEntry = {
                            id: transactionEntry.id + '|fee', // NOTE: this field is dummy
                            creditAccountID1: isCreditorCounterparty ? entityId : exchangeRateTaxCategory,
                            localCurrencyCreditAmount1: amount,
                            debitAccountID1: isCreditorCounterparty ? exchangeRateTaxCategory : entityId,
                            localCurrencyDebitAmount1: amount,
                            description: 'Exchange ledger record',
                            isCreditorCounterparty,
                            invoiceDate: entryDate,
                            valueDate: entryDate,
                            currency: defaultLocalCurrency,
                            ownerId: transactionEntry.ownerId,
                            chargeId,
                        };
                        miscLedgerEntries.push(ledgerEntry);
                        updateLedgerBalance(ledgerEntry);
                    }
                    else {
                        errors.add(`Failed to locate tax category for exchange rate for business ID="${entityId}"`);
                    }
                }
                else {
                    errors.add(validation);
                }
            }
            else {
                errors.add(`Failed to balance: ${hasMultipleDates ? 'Dates are different' : 'Dates are consistent'} and ${foreignCurrencyCount ? 'currencies are foreign' : 'currencies are local'}`);
            }
        }
        const crossYearLedgerEntries = await handleCrossYearLedgerEntries(charge, injector, accountingLedgerEntries);
        const records = [
            ...(crossYearLedgerEntries ?? accountingLedgerEntries),
            ...financialAccountLedgerEntries,
            ...feeFinancialAccountLedgerEntries,
            ...miscLedgerEntries,
        ];
        if (insertLedgerRecordsIfNotExists) {
            await storeInitialGeneratedRecords(charge.id, records, injector);
        }
        const ledgerBalanceInfo = await getLedgerBalanceInfo(injector, ledgerBalance, errors, allowedUnbalancedBusinesses);
        return {
            records: ledgerProtoToRecordsConverter(records),
            charge,
            balance: ledgerBalanceInfo,
            errors: Array.from(errors),
        };
    }
    catch (e) {
        const message = `Failed to generate ledger records for charge ID="${chargeId}"\n${e}`;
        console.error(message);
        return {
            __typename: 'CommonError',
            message,
        };
    }
};
//# sourceMappingURL=common-ledger-generation.resolver.js.map