import { GraphQLError } from 'graphql';
import { Repeater } from 'graphql-yoga';
import { ChargeSortByField, } from '../../../__generated__/types.js';
import { EMPTY_UUID } from '../../../shared/constants.js';
import { formatFinancialAmount } from '../../../shared/helpers/index.js';
import { AdminContextProvider } from '../../admin-context/providers/admin-context.provider.js';
import { ScopeProvider } from '../../auth/providers/scope.provider.js';
import { ChargesProvider } from '../../charges/providers/charges.provider.js';
import { FinancialEntitiesProvider } from '../../financial-entities/providers/financial-entities.provider.js';
import { ledgerGenerationByCharge, ledgerUnbalancedBusinessesByCharge, } from '../helpers/ledger-by-charge-type.helper.js';
import { isChargeLocked } from '../helpers/ledger-lock.js';
import { convertLedgerRecordToInput, convertLedgerRecordToProto, ledgerRecordsGenerationFullMatchComparison, ledgerRecordsGenerationPartialMatchComparison, } from '../helpers/ledgrer-storage.helper.js';
import { getLedgerBalanceInfo, updateLedgerBalanceByEntry } from '../helpers/utils.helper.js';
import { LedgerProvider } from '../providers/ledger.provider.js';
import { commonChargeLedgerResolver } from './common.resolver.js';
// Resolve a ledger record's local currency from its OWNING business, so
// multi-business reads format each record in the right currency. Falls back to
// the request's primary business when the per-business preference is missing.
async function recordLocalCurrency(injector, ownerId) {
    if (ownerId) {
        const currency = await injector
            .get(ScopeProvider)
            .getBusinessPreference(ownerId, 'defaultLocalCurrency');
        if (currency) {
            return currency;
        }
    }
    const { defaultLocalCurrency } = await injector
        .get(AdminContextProvider)
        .getVerifiedAdminContext();
    return defaultLocalCurrency;
}
export const ledgerResolvers = {
    Query: {
        chargesWithLedgerChanges: async (_, { filters, limit }, context, info) => {
            const { injector } = context;
            const { ledgerLock } = await injector.get(AdminContextProvider).getVerifiedAdminContext();
            // handle sort column
            let sortColumn = 'event_date';
            switch (filters?.sortBy?.field) {
                case ChargeSortByField.Amount:
                    sortColumn = 'event_amount';
                    break;
                case ChargeSortByField.AbsAmount:
                    sortColumn = 'abs_event_amount';
                    break;
                case ChargeSortByField.Date:
                    sortColumn = 'event_date';
                    break;
            }
            // get max date of filter and ledger lock
            const fromDate = ledgerLock || filters?.fromDate
                ? [ledgerLock, filters?.fromDate].filter(Boolean).sort().reverse()[0]
                : undefined;
            const charges = await injector
                .get(ChargesProvider)
                .getChargesByFilters({
                ownerIds: filters?.byOwners,
                fromDate,
                toDate: filters?.toDate,
                fromAnyDate: filters?.fromAnyDate,
                toAnyDate: filters?.toAnyDate,
                sortColumn,
                asc: filters?.sortBy?.asc !== false,
                chargeType: filters?.chargesType,
                businessIds: filters?.byBusinesses,
                withoutInvoice: filters?.withoutInvoice,
                withoutReceipt: filters?.withoutReceipt,
                withoutDocuments: filters?.withoutDocuments,
                withoutLedger: filters?.withoutLedger,
                tags: filters?.byTags,
                accountantStatuses: filters?.accountantStatus,
            })
                .catch(e => {
                const message = 'Error fetching charges';
                console.error(`${message}: ${e}`);
                throw new Error(message);
            });
            const limitedCharges = limit ? charges.slice(0, limit) : charges;
            return new Repeater(async (push, stop) => {
                push({ progress: 20 });
                let handledCharges = 0;
                function calculateProgress() {
                    return ((handledCharges * 100) / limitedCharges.length) * 0.8 + 20;
                }
                await Promise.all(limitedCharges.map(async (charge) => {
                    if (await isChargeLocked(charge, injector, ledgerLock)) {
                        handledCharges++;
                        if (handledCharges % 50 === 0 || handledCharges === limitedCharges.length) {
                            push({ progress: calculateProgress() });
                        }
                        return;
                    }
                    try {
                        const generatedRecordsPromise = ledgerGenerationByCharge(charge, { insertLedgerRecordsIfNotExists: false }, context, info);
                        const storageRecordsPromise = injector
                            .get(LedgerProvider)
                            .getLedgerRecordsByChargesIdLoader.load(charge.id);
                        const [generatedRecords, storageRecords] = await Promise.all([
                            generatedRecordsPromise,
                            storageRecordsPromise,
                        ]);
                        if (!generatedRecords || 'message' in generatedRecords) {
                            handledCharges++;
                            push({ progress: calculateProgress(), charge });
                            return;
                        }
                        const newRecords = generatedRecords.records;
                        const fullMatching = ledgerRecordsGenerationFullMatchComparison(storageRecords, newRecords);
                        if (fullMatching.isFullyMatched) {
                            handledCharges++;
                            if (handledCharges % 50 === 0 || handledCharges === limitedCharges.length) {
                                push({ progress: calculateProgress() });
                            }
                            return;
                        }
                        handledCharges++;
                        push({ progress: calculateProgress(), charge });
                    }
                    catch (err) {
                        console.error(err);
                        handledCharges++;
                        push({ progress: calculateProgress(), charge });
                    }
                }));
                push({ progress: 100 });
                stop();
            });
        },
        ledgerRecordsByDates: async (_, { fromDate, toDate }, { injector }) => {
            if (fromDate > toDate) {
                throw new GraphQLError('fromDate must be before or equal to toDate');
            }
            const { ownerId } = await injector.get(AdminContextProvider).getVerifiedAdminContext();
            return await injector
                .get(LedgerProvider)
                .getLedgerRecordsByDates({
                fromDate,
                toDate,
                ownerId,
            })
                .then(records => records.sort((a, b) => a.invoice_date.getTime() - b.invoice_date.getTime()))
                .catch(error => {
                console.error('Failed to fetch ledger records:', error);
                throw new GraphQLError('Failed to fetch ledger records');
            });
        },
        ledgerRecordsByFinancialEntity: async (_, { financialEntityId }, { injector }) => {
            return await injector
                .get(LedgerProvider)
                .getLedgerRecordsByFinancialEntityIdLoader.load(financialEntityId);
        },
    },
    Mutation: {
        regenerateLedgerRecords: async (_, { chargeId }, context, info) => {
            const { injector } = context;
            const { ledgerLock, ownerId } = await injector
                .get(AdminContextProvider)
                .getVerifiedAdminContext();
            const charge = await injector.get(ChargesProvider).getChargeByIdLoader.load(chargeId);
            if (!charge) {
                throw new GraphQLError(`Charge with id ${chargeId} not found`);
            }
            if (await isChargeLocked(charge, injector, ledgerLock)) {
                return {
                    __typename: 'CommonError',
                    message: `Charge with id ${chargeId} is locked`,
                };
            }
            try {
                const generated = await ledgerGenerationByCharge(charge, { insertLedgerRecordsIfNotExists: true }, context, info);
                if (!generated || 'message' in generated) {
                    const message = generated?.message ?? 'generation error';
                    throw new Error(message);
                }
                const records = generated.records;
                const storageLedgerRecords = await injector
                    .get(LedgerProvider)
                    .getLedgerRecordsByChargesIdLoader.load(chargeId);
                const fullMatching = ledgerRecordsGenerationFullMatchComparison(storageLedgerRecords, records);
                if (fullMatching.isFullyMatched) {
                    return {
                        records: storageLedgerRecords,
                        charge,
                        errors: generated.errors,
                    };
                }
                const { toUpdate, toRemove } = ledgerRecordsGenerationPartialMatchComparison(fullMatching.unmatchedStorageRecords, fullMatching.unmatchedNewRecords);
                const [newRecords, recordsToUpdate] = toUpdate.reduce((acc, record) => {
                    if (record.id === EMPTY_UUID) {
                        acc[0].push(record);
                    }
                    else {
                        acc[1].push(record);
                    }
                    return acc;
                }, [[], []]);
                const updatePromise = injector
                    .get(LedgerProvider)
                    .deleteLedgerRecordsByIdLoader.loadMany(recordsToUpdate.map(r => r.id))
                    .catch(e => {
                    const message = `Failed to delete ledger records for charge ID="${chargeId}"`;
                    console.error(`${message}: ${e}`);
                    if (e instanceof GraphQLError) {
                        throw e;
                    }
                    throw new Error(message);
                })
                    .then(() => injector.get(LedgerProvider).insertLedgerRecords({
                    ledgerRecords: recordsToUpdate
                        .map(record => convertLedgerRecordToInput(record, ownerId))
                        .map(record => {
                        record.chargeId = chargeId;
                        return record;
                    }),
                }))
                    .catch(e => {
                    if (e instanceof GraphQLError) {
                        throw e;
                    }
                    const message = `Failed to update ledger records for charge ID="${chargeId}"`;
                    console.error(`${message}: ${e}`);
                    throw new Error(message);
                });
                const insertPromise = newRecords.length > 0
                    ? injector
                        .get(LedgerProvider)
                        .insertLedgerRecords({
                        ledgerRecords: newRecords.map(record => convertLedgerRecordToInput(record, ownerId)),
                    })
                        .catch(e => {
                        const message = `Failed to insert new ledger records for charge ID="${chargeId}"`;
                        console.error(`${message}: ${e}`);
                        if (e instanceof GraphQLError) {
                            throw e;
                        }
                        throw new Error(message);
                    })
                    : Promise.resolve();
                const removePromises = toRemove.map(record => injector
                    .get(LedgerProvider)
                    .deleteLedgerRecordsByIdLoader.load(record.id)
                    .catch(e => {
                    const message = `Failed to delete ledger records for charge ID="${chargeId}"`;
                    console.error(`${message}: ${e}`);
                    if (e instanceof GraphQLError) {
                        throw e;
                    }
                    throw new Error(message);
                }));
                await Promise.all([updatePromise, insertPromise, ...removePromises]);
                return {
                    records: toUpdate,
                    charge,
                    errors: generated.errors,
                };
            }
            catch (e) {
                if (e instanceof GraphQLError) {
                    throw e;
                }
                return {
                    __typename: 'CommonError',
                    message: `Failed to generate ledger records for charge ID="${chargeId}"\n${e}`,
                };
            }
        },
        lockLedgerRecords: async (_, { date }, { injector }) => {
            try {
                const { ownerId } = await injector.get(AdminContextProvider).getVerifiedAdminContext();
                const lockByRecords = injector
                    .get(LedgerProvider)
                    .lockLedgerRecords(date)
                    .catch(e => {
                    console.error(`Error locking ledger records for ${date}: ${e}`);
                    throw new Error(`Error locking ledger records for ${date}`);
                });
                const lockByDate = injector
                    .get(AdminContextProvider)
                    .updateAdminContext({
                    ownerId,
                    ledgerLock: date,
                })
                    .catch(e => {
                    console.error(`Error locking ledger by date ${date}: ${e}`);
                    throw new Error(`Error locking ledger by date ${date}`);
                });
                await Promise.all([lockByRecords, lockByDate]);
                return true;
            }
            catch (error) {
                console.error(`Error locking ledger for ${date}: ${error}`);
                return false;
            }
        },
    },
    LedgerRecord: {
        id: DbLedgerRecord => DbLedgerRecord.id,
        debitAmount1: DbLedgerRecord => DbLedgerRecord.debit_foreign_amount1 == null
            ? null
            : formatFinancialAmount(DbLedgerRecord.debit_foreign_amount1, DbLedgerRecord.currency),
        debitAmount2: DbLedgerRecord => DbLedgerRecord.debit_foreign_amount2 == null
            ? null
            : formatFinancialAmount(DbLedgerRecord.debit_foreign_amount2, DbLedgerRecord.currency),
        creditAmount1: DbLedgerRecord => DbLedgerRecord.credit_foreign_amount1 == null
            ? null
            : formatFinancialAmount(DbLedgerRecord.credit_foreign_amount1, DbLedgerRecord.currency),
        creditAmount2: DbLedgerRecord => DbLedgerRecord.credit_foreign_amount2 == null
            ? null
            : formatFinancialAmount(DbLedgerRecord.credit_foreign_amount2, DbLedgerRecord.currency),
        localCurrencyDebitAmount1: async (DbLedgerRecord, _, { injector }) => {
            const defaultLocalCurrency = await recordLocalCurrency(injector, DbLedgerRecord.owner_id);
            return formatFinancialAmount(DbLedgerRecord.debit_local_amount1, defaultLocalCurrency);
        },
        localCurrencyDebitAmount2: async (DbLedgerRecord, _, { injector }) => {
            const defaultLocalCurrency = await recordLocalCurrency(injector, DbLedgerRecord.owner_id);
            return DbLedgerRecord.debit_local_amount2 == null
                ? null
                : formatFinancialAmount(DbLedgerRecord.debit_local_amount2, defaultLocalCurrency);
        },
        localCurrencyCreditAmount1: async (DbLedgerRecord, _, { injector }) => {
            const defaultLocalCurrency = await recordLocalCurrency(injector, DbLedgerRecord.owner_id);
            return formatFinancialAmount(DbLedgerRecord.credit_local_amount1, defaultLocalCurrency);
        },
        localCurrencyCreditAmount2: async (DbLedgerRecord, _, { injector }) => {
            const defaultLocalCurrency = await recordLocalCurrency(injector, DbLedgerRecord.owner_id);
            return DbLedgerRecord.credit_local_amount2 == null
                ? null
                : formatFinancialAmount(DbLedgerRecord.credit_local_amount2, defaultLocalCurrency);
        },
        invoiceDate: DbLedgerRecord => DbLedgerRecord.invoice_date,
        valueDate: DbLedgerRecord => DbLedgerRecord.value_date,
        description: DbLedgerRecord => DbLedgerRecord.description ?? null,
        reference: DbLedgerRecord => DbLedgerRecord.reference1 ?? null,
    },
    Ledger: {
        records: parent => parent.records,
        balance: async (parent, _, { injector }) => {
            if (parent.balance) {
                return parent.balance;
            }
            const { ownerId, defaultLocalCurrency } = await injector
                .get(AdminContextProvider)
                .getVerifiedAdminContext();
            const financialEntitiesIds = new Set();
            parent.records.map(record => {
                if (record.debit_entity1) {
                    financialEntitiesIds.add(record.debit_entity1);
                }
                if (record.debit_entity2) {
                    financialEntitiesIds.add(record.debit_entity2);
                }
                if (record.credit_entity1) {
                    financialEntitiesIds.add(record.credit_entity1);
                }
                if (record.credit_entity2) {
                    financialEntitiesIds.add(record.credit_entity2);
                }
            });
            const financialEntitiesPromise = await injector
                .get(FinancialEntitiesProvider)
                .getFinancialEntityByIdLoader.loadMany(Array.from(financialEntitiesIds))
                .then(res => res.filter(e => !!e && !(e instanceof Error)));
            const allowedUnbalancedBusinessesPromise = ledgerUnbalancedBusinessesByCharge(parent.charge, injector);
            const [financialEntities, allowedUnbalancedBusinesses] = await Promise.all([
                financialEntitiesPromise,
                allowedUnbalancedBusinessesPromise,
            ]);
            const ledgerBalance = new Map();
            const ledgerEntries = parent.records.map(record => convertLedgerRecordToProto(record, ownerId));
            for (const ledgerEntry of ledgerEntries) {
                updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance, defaultLocalCurrency);
            }
            return getLedgerBalanceInfo(injector, ledgerBalance, undefined, allowedUnbalancedBusinesses, financialEntities);
        },
        validate: async ({ charge, records }, _, context, info) => {
            const { injector } = context;
            const { ledgerLock } = await injector.get(AdminContextProvider).getVerifiedAdminContext();
            if (await isChargeLocked(charge, injector, ledgerLock)) {
                return {
                    isValid: true,
                    differences: [],
                    matches: records.map(r => r.id),
                    errors: [],
                };
            }
            try {
                const generated = await ledgerGenerationByCharge(charge, { insertLedgerRecordsIfNotExists: records.length === 0 }, context, info);
                if (!generated || 'message' in generated) {
                    return {
                        isValid: false,
                        differences: [],
                        matches: [],
                        errors: [],
                    };
                }
                const newRecords = generated.records;
                const fullMatching = ledgerRecordsGenerationFullMatchComparison(records, newRecords);
                if (fullMatching.isFullyMatched) {
                    return {
                        isValid: true,
                        differences: [],
                        matches: Array.from(fullMatching.fullMatches.values()).filter(Boolean),
                        errors: generated.errors,
                    };
                }
                const { toUpdate } = ledgerRecordsGenerationPartialMatchComparison(fullMatching.unmatchedStorageRecords, fullMatching.unmatchedNewRecords);
                return {
                    isValid: fullMatching.isFullyMatched && generated.errors.length === 0,
                    differences: toUpdate,
                    matches: Array.from(fullMatching.fullMatches.values()).filter(Boolean),
                    errors: generated.errors,
                };
            }
            catch (err) {
                console.error(err);
                return {
                    isValid: false,
                    differences: [],
                    matches: [],
                    errors: [],
                };
            }
        },
    },
    LedgerBalanceUnbalancedEntity: {
        entity: (parent, _, { injector }) => injector
            .get(FinancialEntitiesProvider)
            .getFinancialEntityByIdLoader.load(parent.entityId)
            .then(res => {
            if (!res) {
                throw new GraphQLError(`Financial entity with id ${parent.entityId} not found`);
            }
            return res;
        }),
        balance: parent => parent.balance,
    },
    CommonCharge: {
        ...commonChargeLedgerResolver,
    },
    FinancialCharge: {
        ...commonChargeLedgerResolver,
    },
    ConversionCharge: {
        ...commonChargeLedgerResolver,
    },
    SalaryCharge: {
        ...commonChargeLedgerResolver,
    },
    InternalTransferCharge: {
        ...commonChargeLedgerResolver,
    },
    DividendCharge: {
        ...commonChargeLedgerResolver,
    },
    BusinessTripCharge: {
        ...commonChargeLedgerResolver,
    },
    MonthlyVatCharge: {
        ...commonChargeLedgerResolver,
    },
    BankDepositCharge: {
        ...commonChargeLedgerResolver,
    },
    ForeignSecuritiesCharge: {
        ...commonChargeLedgerResolver,
    },
    CreditcardBankCharge: {
        ...commonChargeLedgerResolver,
    },
    GeneratedLedgerRecords: {
        __resolveType: (obj, _context, _info) => {
            if ('__typename' in obj && obj.__typename === 'CommonError')
                return 'CommonError';
            return 'Ledger';
        },
    },
    ChargeMetadata: {
        isLedgerLocked: async (DbCharge, _, { injector }) => {
            const { ledgerLock } = await injector.get(AdminContextProvider).getVerifiedAdminContext();
            return isChargeLocked(DbCharge, injector, ledgerLock);
        },
    },
};
//# sourceMappingURL=ledger.resolver.js.map