/**
 * Charges Matcher Provider
 *
 * Provides database-integrated charge matching functionality using the Injector pattern.
 * Integrates with existing modules: charges, transactions, and documents.
 */
import { __decorate, __metadata, __param } from "tslib";
import { subYears } from 'date-fns';
import { CONTEXT, Inject, Injectable, Scope } from 'graphql-modules';
import { dateToTimelessDateString } from '../../../shared/helpers/index.js';
import { AdminContextProvider } from '../../admin-context/providers/admin-context.provider.js';
import { mergeChargesExecutor } from '../../charges/helpers/merge-charges.helper.js';
import { ChargesProvider } from '../../charges/providers/charges.provider.js';
import { isAccountingDocument, isInvoice, isReceipt, } from '../../documents/helpers/common.helper.js';
import { DocumentsProvider } from '../../documents/providers/documents.provider.js';
import { TransactionsProvider } from '../../transactions/providers/transactions.provider.js';
import { validateChargeIsUnmatched } from '../helpers/charge-validator.helper.js';
import { ChargeType, } from '../types.js';
import { determineMergeDirection, processChargeForAutoMatch } from './auto-match.provider.js';
import { aggregateDocuments } from './document-aggregator.js';
import { findMatches } from './single-match.provider.js';
import { aggregateTransactions } from './transaction-aggregator.js';
/**
 * Charges Matcher Provider
 *
 * Provides high-level charge matching operations with database integration.
 * Uses the Injector pattern to access existing providers from other modules.
 */
let ChargesMatcherProvider = class ChargesMatcherProvider {
    adminContextProvider;
    chargesProvider;
    transactionsProvider;
    documentsProvider;
    context;
    constructor(adminContextProvider, chargesProvider, transactionsProvider, documentsProvider, context) {
        this.adminContextProvider = adminContextProvider;
        this.chargesProvider = chargesProvider;
        this.transactionsProvider = transactionsProvider;
        this.documentsProvider = documentsProvider;
        this.context = context;
    }
    /**
     * Find potential matches for an unmatched charge
     *
     * @param chargeId - ID of the unmatched charge to find matches for
     * @returns Top 5 matches ordered by confidence score
     * @throws Error if charge not found
     * @throws Error if charge is already matched
     * @throws Error if charge data is invalid
     */
    async findMatchesForCharge(chargeId) {
        const { ownerId } = await this.adminContextProvider.getVerifiedAdminContext();
        // Step 1: Load source charge data
        const sourceCharge = await this.chargesProvider.getChargeByIdLoader.load(chargeId);
        if (!sourceCharge || sourceCharge instanceof Error) {
            throw new Error(`Source charge not found: ${chargeId}`);
        }
        // Step 2: Load transactions and documents for source charge
        const sourceTransactions = await this.transactionsProvider.transactionsByChargeIDLoader.load(chargeId);
        const sourceDocuments = (await this.documentsProvider.getDocumentsByChargeIdLoader.load(chargeId)).filter(doc => isAccountingDocument(doc.type));
        // Step 3: Validate source charge is unmatched
        const sourceChargeWithData = {
            ...sourceCharge,
            transactions: sourceTransactions,
            documents: sourceDocuments,
        };
        validateChargeIsUnmatched(sourceChargeWithData);
        // Step 4: Determine reference date and date window from source charge
        let referenceDate;
        const hasTransactions = sourceTransactions && sourceTransactions.length > 0;
        if (hasTransactions) {
            // Use earliest transaction event_date
            const aggregated = aggregateTransactions(sourceTransactions);
            referenceDate = aggregated.date;
        }
        else {
            // Use latest document date
            const aggregated = aggregateDocuments(sourceDocuments, ownerId);
            referenceDate = aggregated.date;
        }
        // Step 5: Load candidate charges from database
        // Use 12-month window centered on reference date
        const windowStart = new Date(referenceDate);
        windowStart.setMonth(windowStart.getMonth() - 12);
        const windowEnd = new Date(referenceDate);
        windowEnd.setMonth(windowEnd.getMonth() + 12);
        const candidateCharges = await this.chargesProvider.getChargesByFilters({
            ownerIds: [ownerId],
            fromAnyDate: dateToTimelessDateString(windowStart),
            toAnyDate: dateToTimelessDateString(windowEnd),
        });
        // Step 6: Load transactions and documents for all candidate charges
        const candidateChargesWithData = [];
        await Promise.all(candidateCharges.map(async (candidate) => {
            // Skip the source charge itself
            if (candidate.id === chargeId) {
                return;
            }
            const candidateTransactionsPromise = this.transactionsProvider.transactionsByChargeIDLoader.load(candidate.id);
            const candidateDocumentsPromise = this.documentsProvider.getDocumentsByChargeIdLoader
                .load(candidate.id)
                .then(docs => docs.filter(doc => isAccountingDocument(doc.type)));
            const [candidateTransactions, candidateDocuments] = await Promise.all([
                candidateTransactionsPromise,
                candidateDocumentsPromise,
            ]);
            const candidateInvoiceDocuments = candidateDocuments.filter(doc => isInvoice(doc.type));
            const candidateReceiptDocuments = candidateDocuments.filter(doc => isReceipt(doc.type));
            const hasTxs = candidateTransactions && candidateTransactions.length > 0;
            const hasDocs = candidateDocuments && candidateDocuments.length > 0;
            const hasInvoiceDocs = candidateInvoiceDocuments && candidateInvoiceDocuments.length > 0;
            const hasReceiptDocs = candidateReceiptDocuments && candidateReceiptDocuments.length > 0;
            // Only include unmatched charges (not both types)
            if (hasTxs && !hasReceiptDocs) {
                candidateChargesWithData.push({
                    chargeId: candidate.id,
                    transactions: candidateTransactions,
                });
            }
            if (hasDocs && !hasTxs) {
                if (hasReceiptDocs) {
                    candidateChargesWithData.push({
                        chargeId: candidate.id,
                        documents: candidateReceiptDocuments,
                    });
                }
                else if (hasInvoiceDocs) {
                    candidateChargesWithData.push({
                        chargeId: candidate.id,
                        documents: candidateInvoiceDocuments,
                    });
                }
                else {
                    candidateChargesWithData.push({
                        chargeId: candidate.id,
                        documents: candidateDocuments,
                    });
                }
            }
            // Skip matched charges (have both) and empty charges (have neither)
        }));
        // Step 7: Build source charge object for findMatches
        let sourceChargeData;
        if (hasTransactions) {
            sourceChargeData = {
                chargeId,
                transactions: sourceTransactions,
            };
        }
        else {
            sourceChargeData = {
                chargeId,
                documents: sourceDocuments,
            };
        }
        // Step 8: Call core findMatches function
        const matches = await findMatches(sourceChargeData, candidateChargesWithData, ownerId, this.context.injector, {
            maxMatches: 5,
            dateWindowMonths: 12,
        });
        // Step 9: Format and return result
        return {
            matches: matches.map(match => ({
                chargeId: match.chargeId,
                confidenceScore: match.confidenceScore,
            })),
        };
    }
    /**
     * Auto-match all unmatched charges
     *
     * Automatically merges charges that have a single high-confidence match (≥0.95).
     * Skips charges with multiple high-confidence matches (ambiguous).
     * Processes all unmatched charges and returns a summary of actions taken.
     *
     * @returns Summary of matches made, skipped charges, and errors
     */
    async autoMatchCharges() {
        const { ownerId } = await this.adminContextProvider.getVerifiedAdminContext();
        // Step 1: Load all charges for this user
        const prevYear = dateToTimelessDateString(subYears(new Date(), 1));
        const allCharges = await this.chargesProvider.getChargesByFilters({
            ownerIds: [ownerId],
            fromAnyDate: prevYear,
        });
        // Step 2: Load transactions and documents for all charges
        const chargesWithData = [];
        const mergedChargeIds = new Set(); // Track merged charges to exclude from processing
        await Promise.all(allCharges.map(async (charge) => {
            const transactionsPromise = this.transactionsProvider.transactionsByChargeIDLoader.load(charge.id);
            const documentsPromise = this.documentsProvider.getDocumentsByChargeIdLoader
                .load(charge.id)
                .then(docs => docs.filter(doc => isAccountingDocument(doc.type)));
            const [transactions, documents] = await Promise.all([
                transactionsPromise,
                documentsPromise,
            ]);
            chargesWithData.push({
                chargeId: charge.id,
                ownerId: charge.owner_id ?? ownerId,
                type: ChargeType.TRANSACTION_ONLY, // Will be determined by processChargeForAutoMatch
                description: charge.user_description ?? undefined,
                transactions,
                documents,
            });
        }));
        // Step 3: Filter to get only unmatched charges
        const unmatchedCharges = chargesWithData.filter(charge => {
            const hasTx = charge.transactions && charge.transactions.length > 0;
            const hasAccountingDocs = charge.documents &&
                charge.documents.filter(doc => isAccountingDocument(doc.type)).length > 0;
            const hasReceipts = charge.documents?.some(doc => isReceipt(doc.type));
            return (hasTx && !hasReceipts) || (!hasTx && hasAccountingDocs);
        });
        // Step 4: Process each unmatched charge
        const result = {
            totalMatches: 0,
            mergedCharges: [],
            skippedCharges: [],
            errors: [],
        };
        for (const sourceCharge of unmatchedCharges) {
            // Skip if this charge was already merged in this run
            if (mergedChargeIds.has(sourceCharge.chargeId)) {
                continue;
            }
            try {
                // Get candidates (exclude already merged charges)
                const candidates = chargesWithData.filter(c => c.chargeId !== sourceCharge.chargeId && !mergedChargeIds.has(c.chargeId));
                // Process this charge for auto-match
                const processResult = await processChargeForAutoMatch(sourceCharge, candidates, ownerId, this.context.injector);
                if (processResult.status === 'matched' && processResult.match) {
                    // Found a single high-confidence match - execute merge
                    const matchedChargeId = processResult.match.chargeId;
                    const matchedCharge = chargesWithData.find(c => c.chargeId === matchedChargeId);
                    if (!matchedCharge) {
                        result.errors.push(`Matched charge ${matchedChargeId} not found in charge pool for ${sourceCharge.chargeId}`);
                        continue;
                    }
                    // Determine merge direction
                    const [sourceToMerge, targetToKeep] = determineMergeDirection(sourceCharge, matchedCharge);
                    try {
                        // Execute merge via existing merge functionality
                        await mergeChargesExecutor([sourceToMerge.chargeId], targetToKeep.chargeId, this.context.injector);
                        // Track successful merge
                        result.totalMatches++;
                        result.mergedCharges.push({
                            chargeId: targetToKeep.chargeId,
                            confidenceScore: processResult.match.confidenceScore,
                        });
                        // Mark both charges as processed (merged away charge and kept charge)
                        mergedChargeIds.add(sourceToMerge.chargeId);
                        mergedChargeIds.add(targetToKeep.chargeId); // Don't process the kept charge again
                    }
                    catch (mergeError) {
                        result.errors.push(`Failed to merge ${sourceToMerge.chargeId} into ${targetToKeep.chargeId}: ${mergeError instanceof Error ? mergeError.message : String(mergeError)}`);
                    }
                }
                else if (processResult.status === 'skipped') {
                    // Multiple high-confidence matches - ambiguous
                    result.skippedCharges.push(sourceCharge.chargeId);
                }
                // status === 'no-match': do nothing, silently skip
            }
            catch (error) {
                // Capture error but continue processing other charges
                result.errors.push(`Error processing charge ${sourceCharge.chargeId}: ${error instanceof Error ? error.message : String(error)}`);
            }
        }
        return result;
    }
};
ChargesMatcherProvider = __decorate([
    Injectable({
        scope: Scope.Operation,
    }),
    __param(4, Inject(CONTEXT)),
    __metadata("design:paramtypes", [AdminContextProvider,
        ChargesProvider,
        TransactionsProvider,
        DocumentsProvider, Object])
], ChargesMatcherProvider);
export { ChargesMatcherProvider };
//# sourceMappingURL=charges-matcher.provider.js.map