import { lastDayOfMonth } from 'date-fns';
import { AdminContextProvider } from '../../../admin-context/providers/admin-context.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 { EmployeesProvider } from '../../../salaries/providers/employees.provider.js';
import { FundsProvider } from '../../../salaries/providers/funds.provider.js';
import { SalariesProvider } from '../../../salaries/providers/salaries.provider.js';
import { TransactionsProvider } from '../../../transactions/providers/transactions.provider.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 { generateEntriesFromSalaryRecords } from '../../helpers/salary-charge-ledger.helper.js';
import { generatePartialLedgerEntry, getFinancialAccountTaxCategoryId, getLedgerBalanceInfo, LedgerError, ledgerProtoToRecordsConverter, updateLedgerBalanceByEntry, validateTransactionRequiredVariables, } from '../../helpers/utils.helper.js';
import { BalanceCancellationProvider } from '../../providers/balance-cancellation.provider.js';
import { UnbalancedBusinessesProvider } from '../../providers/unbalanced-businesses.provider.js';
import { generateLedgerRecordsForCommonCharge } from './common-ledger-generation.resolver.js';
export const generateLedgerRecordsForSalary = async (charge, { insertLedgerRecordsIfNotExists }, context, _info) => {
    const { injector } = context;
    const { allBusinessIds } = await getChargeBusinesses(charge.id, injector);
    if (allBusinessIds.length === 1) {
        // for one business, use the default ledger generation
        return generateLedgerRecordsForCommonCharge(charge, { insertLedgerRecordsIfNotExists }, context, _info);
    }
    const chargeId = charge.id;
    const errors = new Set();
    const { defaultLocalCurrency, general: { taxCategories: { balanceCancellationTaxCategoryId, exchangeRateTaxCategoryId }, }, salaries: { batchedFundsBusinessId, batchedEmployeesBusinessId, salaryBatchedBusinessIds }, } = await injector.get(AdminContextProvider).getVerifiedAdminContext();
    try {
        // validate ledger records are balanced
        const ledgerBalance = new Map();
        let entriesPromises = [];
        const accountingLedgerEntries = [];
        const foreignCurrencySalaryBusinesses = new Set();
        // Get relevant data for generation
        const salaryRecordsPromise = injector
            .get(SalariesProvider)
            .getSalaryRecordsByChargeIdLoader.load(chargeId);
        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 [salaryRecords, transactions, unbalancedBusinesses, balanceCancellations, adminContext] = await Promise.all([
            salaryRecordsPromise,
            transactionsPromise,
            unbalancedBusinessesPromise,
            chargeBallanceCancellationsPromise,
            injector.get(AdminContextProvider).getVerifiedAdminContext(),
        ]);
        // generate ledger from salary records
        try {
            const { entries, monthlyEntriesProto, month } = generateEntriesFromSalaryRecords(salaryRecords, charge, adminContext);
            entries.map(ledgerEntry => {
                accountingLedgerEntries.push(ledgerEntry);
                updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance, defaultLocalCurrency);
            });
            // generate monthly expenses ledger entries
            const monthlyEntriesPromises = monthlyEntriesProto
                .filter(({ amount }) => amount !== 0)
                .map(async ({ taxCategoryId, amount, isCredit }) => {
                const taxCategory = await injector
                    .get(TaxCategoriesProvider)
                    .taxCategoryByIdLoader.load(taxCategoryId);
                if (!taxCategory) {
                    throw new LedgerError(`Tax category "${taxCategoryId}" not found`);
                }
                const ledgerEntry = {
                    id: taxCategoryId,
                    invoiceDate: lastDayOfMonth(new Date(`${month}-01`)),
                    valueDate: lastDayOfMonth(new Date(`${month}-01`)),
                    currency: defaultLocalCurrency,
                    ...(isCredit
                        ? { creditAccountID1: taxCategoryId }
                        : { debitAccountID1: taxCategoryId }),
                    localCurrencyCreditAmount1: amount,
                    localCurrencyDebitAmount1: amount,
                    description: `${month} salary: ${taxCategory.name}`,
                    isCreditorCounterparty: false,
                    ownerId: charge.owner_id,
                    chargeId,
                };
                accountingLedgerEntries.push(ledgerEntry);
                updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance, defaultLocalCurrency);
            });
            entriesPromises.push(...monthlyEntriesPromises);
        }
        catch (e) {
            if (e instanceof LedgerError) {
                errors.add(e.message);
            }
            else {
                throw e;
            }
        }
        const { mainTransactions, feeTransactions } = splitFeeTransactions(transactions);
        // generate ledger from main transactions
        const batchedTransactionEntriesMaterials = [];
        // for each common transaction, create a ledger record
        const financialAccountLedgerEntries = [];
        const miscExpensesLedgerEntries = [];
        const transactionEntriesPromises = mainTransactions.map(async (preValidatedTransaction) => {
            try {
                const transaction = validateTransactionRequiredVariables(preValidatedTransaction);
                // preparations for core ledger entries
                let exchangeRate = undefined;
                if (transaction.currency !== defaultLocalCurrency) {
                    // get exchange rate for currency
                    exchangeRate = await injector
                        .get(ExchangeProvider)
                        .getExchangeRates(transaction.currency, defaultLocalCurrency, transaction.debit_timestamp);
                }
                const partialEntry = generatePartialLedgerEntry(transaction, charge.owner_id, exchangeRate);
                const financialAccountTaxCategoryId = await getFinancialAccountTaxCategoryId(injector, transaction);
                if (transaction.business_id && salaryBatchedBusinessIds.includes(transaction.business_id)) {
                    batchedTransactionEntriesMaterials.push({
                        transaction,
                        partialEntry,
                        taxCategoryId: financialAccountTaxCategoryId,
                    });
                    return;
                }
                const ledgerEntry = {
                    ...partialEntry,
                    ...(partialEntry.isCreditorCounterparty
                        ? {
                            creditAccountID1: transaction.business_id,
                            debitAccountID1: financialAccountTaxCategoryId,
                        }
                        : {
                            creditAccountID1: financialAccountTaxCategoryId,
                            debitAccountID1: transaction.business_id,
                        }),
                };
                financialAccountLedgerEntries.push(ledgerEntry);
                updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance, defaultLocalCurrency);
                // on foreign currency salary, update matching accounting ledger entry
                if (ledgerEntry.currency !== defaultLocalCurrency) {
                    const matchingEntry = accountingLedgerEntries.find(accountingEntry => {
                        return (transaction.business_id ===
                            (accountingEntry.isCreditorCounterparty
                                ? accountingEntry.debitAccountID1
                                : accountingEntry.creditAccountID1));
                    });
                    if (matchingEntry && matchingEntry.currency !== ledgerEntry.currency) {
                        matchingEntry.currency = ledgerEntry.currency;
                        matchingEntry.currencyRate = ledgerEntry.currencyRate;
                        matchingEntry.creditAmount1 = ledgerEntry.creditAmount1;
                        matchingEntry.debitAmount1 = ledgerEntry.debitAmount1;
                        foreignCurrencySalaryBusinesses.add(transaction.business_id);
                    }
                }
            }
            catch (e) {
                if (e instanceof LedgerError) {
                    errors.add(e.message);
                }
                else {
                    throw e;
                }
            }
        });
        const miscExpensesEntriesPromise = generateMiscExpensesLedger(charge, injector).then(entries => {
            entries.map(entry => {
                entry.ownerId = charge.owner_id;
                miscExpensesLedgerEntries.push(entry);
                updateLedgerBalanceByEntry(entry, ledgerBalance, defaultLocalCurrency);
            });
        });
        entriesPromises.push(...transactionEntriesPromises, miscExpensesEntriesPromise);
        await Promise.all(entriesPromises);
        const miscLedgerEntries = [];
        // Add exchange ledger in case of foreign currency salaries
        for (const entry of accountingLedgerEntries) {
            const businessId = entry.creditAccountID1;
            const matchingTransaction = financialAccountLedgerEntries.find(entry => businessId ===
                (entry.isCreditorCounterparty ? entry.creditAccountID1 : entry.debitAccountID1));
            if (!matchingTransaction || !businessId) {
                continue;
            }
            const balance = (matchingTransaction?.localCurrencyCreditAmount1 ?? 0) *
                (matchingTransaction?.isCreditorCounterparty ? 1 : -1) +
                entry.localCurrencyCreditAmount1;
            if (balance === 0) {
                continue;
            }
            const datesDiff = entry.invoiceDate.getTime() !== matchingTransaction?.invoiceDate.getTime();
            if (!datesDiff) {
                continue;
            }
            const currencyDiff = entry.currency !== matchingTransaction.currency;
            if (!currencyDiff && !foreignCurrencySalaryBusinesses.has(businessId)) {
                continue;
            }
            const amount = Math.abs(balance);
            const isCreditorCounterparty = balance < 0;
            // validate exchange rate
            const validation = validateExchangeRate(businessId, [...accountingLedgerEntries, ...financialAccountLedgerEntries], balance, defaultLocalCurrency);
            if (validation === true) {
                const ledgerEntry = {
                    id: matchingTransaction.id + '|fee', // NOTE: this field is dummy
                    creditAccountID1: isCreditorCounterparty ? businessId : exchangeRateTaxCategoryId,
                    localCurrencyCreditAmount1: amount,
                    debitAccountID1: isCreditorCounterparty ? exchangeRateTaxCategoryId : businessId,
                    localCurrencyDebitAmount1: amount,
                    description: 'Exchange ledger record',
                    isCreditorCounterparty,
                    invoiceDate: matchingTransaction.invoiceDate,
                    valueDate: entry.valueDate,
                    currency: defaultLocalCurrency,
                    ownerId: matchingTransaction.ownerId,
                    chargeId,
                };
                miscLedgerEntries.push(ledgerEntry);
                updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance, defaultLocalCurrency);
            }
            else {
                errors.add(validation);
            }
        }
        // for each batched transaction, create a ledger record
        const batchedLedgerEntries = [];
        entriesPromises = [];
        const batchedTransactionEntriesPromises = batchedTransactionEntriesMaterials.map(async ({ transaction, partialEntry, taxCategoryId }) => {
            try {
                const unbatchedBusinesses = [];
                switch (transaction.business_id) {
                    case batchedEmployeesBusinessId: {
                        const employees = await injector
                            .get(EmployeesProvider)
                            .getEmployeesByEmployerLoader.load(charge.owner_id);
                        unbatchedBusinesses.push(...employees.map(({ business_id }) => business_id));
                        break;
                    }
                    case batchedFundsBusinessId: {
                        const funds = await injector.get(FundsProvider).getAllFunds();
                        unbatchedBusinesses.push(...funds.map(({ id }) => id));
                        break;
                    }
                    default: {
                        throw new LedgerError(`Business ID="${transaction.business_id}" is not supported as batched ID`);
                    }
                }
                const batchedEntries = [...accountingLedgerEntries, ...miscExpensesLedgerEntries]
                    .filter(accountingLedgerEntry => accountingLedgerEntry.creditAccountID1
                    ? unbatchedBusinesses.includes(accountingLedgerEntry.creditAccountID1)
                    : false)
                    .map(accountingLedgerEntry => {
                    const externalPayments = financialAccountLedgerEntries
                        .filter(financialAccountEntry => !!financialAccountEntry.debitAccountID1 &&
                        accountingLedgerEntry.creditAccountID1 ===
                            financialAccountEntry.debitAccountID1)
                        .map(entry => entry.localCurrencyCreditAmount1)
                        .reduce((a, b) => a + b, 0);
                    if (externalPayments === 0) {
                        return accountingLedgerEntry;
                    }
                    if (externalPayments < accountingLedgerEntry.localCurrencyCreditAmount1) {
                        return {
                            ...accountingLedgerEntry,
                            localCurrencyCreditAmount1: accountingLedgerEntry.localCurrencyCreditAmount1 - externalPayments,
                            localCurrencyDebitAmount1: accountingLedgerEntry.localCurrencyDebitAmount1 - externalPayments,
                        };
                    }
                    return {
                        ...accountingLedgerEntry,
                        localCurrencyCreditAmount1: externalPayments - accountingLedgerEntry.localCurrencyCreditAmount1,
                        creditAccountID1: accountingLedgerEntry.debitAccountID1,
                        localCurrencyDebitAmount1: externalPayments - accountingLedgerEntry.localCurrencyDebitAmount1,
                        debitAccountID1: accountingLedgerEntry.creditAccountID1,
                    };
                });
                const balance = batchedEntries
                    .map(entry => entry.localCurrencyCreditAmount1)
                    .reduce((a, b) => a + b, 0);
                if (balance !== partialEntry.localCurrencyCreditAmount1) {
                    throw new LedgerError(`Batched business ID="${transaction.business_id}" cannot be unbatched as it is not balanced`);
                }
                for (const batchedEntry of batchedEntries) {
                    const ledgerEntry = {
                        ...partialEntry,
                        ...(partialEntry.isCreditorCounterparty
                            ? {
                                creditAccountID1: batchedEntry.creditAccountID1,
                                debitAccountID1: taxCategoryId,
                            }
                            : {
                                creditAccountID1: taxCategoryId,
                                debitAccountID1: batchedEntry.creditAccountID1,
                            }),
                        creditAmount1: batchedEntry.creditAmount1,
                        localCurrencyCreditAmount1: batchedEntry.localCurrencyCreditAmount1,
                        debitAmount1: batchedEntry.creditAmount1,
                        localCurrencyDebitAmount1: batchedEntry.localCurrencyCreditAmount1,
                    };
                    batchedLedgerEntries.push(ledgerEntry);
                    updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance, defaultLocalCurrency);
                }
            }
            catch (e) {
                if (e instanceof LedgerError) {
                    errors.add(e.message);
                }
                else {
                    throw e;
                }
            }
        });
        entriesPromises.push(...batchedTransactionEntriesPromises);
        // create a ledger record for fee transactions
        const feeFinancialAccountLedgerEntries = [];
        const feeFinancialAccountLedgerEntriesPromises = feeTransactions.map(async (transaction) => {
            await getEntriesFromFeeTransaction(transaction, charge, injector).then(ledgerEntries => {
                feeFinancialAccountLedgerEntries.push(...ledgerEntries);
                ledgerEntries.map(ledgerEntry => {
                    updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance, defaultLocalCurrency);
                });
            });
        });
        entriesPromises.push(...feeFinancialAccountLedgerEntriesPromises);
        await Promise.all(entriesPromises);
        // generate ledger from balance cancellation
        for (const balanceCancellation of balanceCancellations) {
            try {
                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 = undefined;
                if (financialAccountEntry.currency !== defaultLocalCurrency &&
                    financialAccountEntry.currencyRate) {
                    foreignAmount = financialAccountEntry.currencyRate * amount;
                }
                const isCreditorCounterparty = amount > 0;
                const ledgerEntry = {
                    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 ?? 'Balance Cancellation',
                    isCreditorCounterparty,
                    ownerId: charge.owner_id,
                    currencyRate: financialAccountEntry.currencyRate,
                    chargeId,
                };
                miscLedgerEntries.push(ledgerEntry);
                updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance, defaultLocalCurrency);
            }
            catch (e) {
                if (e instanceof LedgerError) {
                    errors.add(e.message);
                }
                else {
                    throw e;
                }
            }
        }
        const allowedUnbalancedBusinesses = new Set(unbalancedBusinesses.map(({ business_id }) => business_id));
        const ledgerBalanceInfo = await getLedgerBalanceInfo(injector, ledgerBalance, errors, allowedUnbalancedBusinesses);
        const records = [
            ...accountingLedgerEntries,
            ...financialAccountLedgerEntries,
            ...batchedLedgerEntries,
            ...feeFinancialAccountLedgerEntries,
            ...miscExpensesLedgerEntries,
            ...miscLedgerEntries,
        ];
        if (insertLedgerRecordsIfNotExists) {
            await storeInitialGeneratedRecords(charge.id, records, injector);
        }
        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=salary-ledger-generation.resolver.js.map