/**
 * Single-Match Provider
 *
 * Implements the core single-match logic for finding potential charge matches.
 * This is a pure function implementation without database dependencies.
 */
import { isWithinDateWindow } from '../helpers/candidate-filter.helper.js';
import { aggregateDocuments } from './document-aggregator.js';
import { scoreMatch } from './match-scorer.provider.js';
import { aggregateTransactions } from './transaction-aggregator.js';
/**
 * Type guard: check if charge is TransactionCharge
 */
function isTransactionCharge(charge) {
    return 'transactions' in charge && charge.transactions.length > 0;
}
/**
 * Type guard: check if charge is DocumentCharge
 */
function isDocumentCharge(charge) {
    return 'documents' in charge && charge.documents.length > 0;
}
/**
 * Validate that source charge is unmatched (has only one type)
 */
function validateUnmatchedCharge(charge) {
    const hasTx = 'transactions' in charge && charge.transactions && charge.transactions.length > 0;
    const hasDocs = 'documents' in charge && charge.documents && charge.documents.length > 0;
    if (hasTx && hasDocs) {
        throw new Error(`Source charge ${charge.chargeId} is already matched (contains both transactions and documents)`);
    }
    if (!hasTx && !hasDocs) {
        throw new Error(`Source charge ${charge.chargeId} has no transactions or documents`);
    }
}
/**
 * Validate that source charge can be aggregated successfully
 */
function validateSourceAggregation(charge, userId) {
    try {
        if (isTransactionCharge(charge)) {
            aggregateTransactions(charge.transactions);
        }
        else {
            aggregateDocuments(charge.documents, userId);
        }
    }
    catch (error) {
        throw new Error(`Source charge ${charge.chargeId} failed validation: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
    }
}
/**
 * Calculate date proximity between transaction and document charges
 * Used for tie-breaking when confidence scores are equal
 *
 * @param txCharge - Transaction charge
 * @param docCharge - Document charge
 * @returns Number of days between earliest transaction date and latest document date
 */
function calculateDateProximity(txCharge, docCharge) {
    // Get earliest transaction event_date
    const earliestTxDate = txCharge.transactions.reduce((earliest, tx) => {
        return tx.event_date < earliest ? tx.event_date : earliest;
    }, txCharge.transactions[0].event_date);
    // Get latest document date
    const latestDocDate = docCharge.documents.reduce((latest, doc) => {
        if (!doc.date)
            return latest;
        return doc.date > latest ? doc.date : latest;
    }, docCharge.documents[0].date);
    // Calculate day difference
    const diffMs = Math.abs(earliestTxDate.getTime() - latestDocDate.getTime());
    return Math.floor(diffMs / (1000 * 60 * 60 * 24));
}
/**
 * Find top matches for an unmatched charge
 *
 * @param sourceCharge - The unmatched charge (transactions OR documents)
 * @param candidateCharges - All potential match candidates
 * @param userId - Current user ID
 * @param options - Optional configuration (maxMatches, dateWindowMonths, injector)
 * @returns Top matches sorted by confidence
 * @throws Error if source charge is matched or has validation issues
 */
export async function findMatches(sourceCharge, candidateCharges, userId, injector, options) {
    const maxMatches = options?.maxMatches ?? 5;
    const dateWindowMonths = options?.dateWindowMonths ?? 12;
    // Step 1: Validate source charge is unmatched
    validateUnmatchedCharge(sourceCharge);
    // Step 2: Validate source charge aggregation
    validateSourceAggregation(sourceCharge, userId);
    // Step 3: Determine source type and filter candidates by complementary type
    const isSourceTransaction = isTransactionCharge(sourceCharge);
    const complementaryCandidates = candidateCharges.filter(candidate => {
        if (isSourceTransaction) {
            return isDocumentCharge(candidate);
        }
        return isTransactionCharge(candidate);
    });
    // Step 4: Get source date for window filtering
    let sourceDate;
    if (isSourceTransaction) {
        // Use earliest event_date from transactions
        sourceDate = sourceCharge.transactions.reduce((earliest, tx) => {
            return tx.event_date < earliest ? tx.event_date : earliest;
        }, sourceCharge.transactions[0].event_date);
    }
    else {
        // Use latest date from documents
        sourceDate = sourceCharge.documents.reduce((latest, doc) => {
            if (!doc.date)
                return latest;
            return doc.date > latest ? doc.date : latest;
        }, sourceCharge.documents[0].date);
    }
    // Step 5: Filter candidates by date window
    const windowFilteredCandidates = complementaryCandidates.filter(candidate => {
        let candidateDate;
        if (isTransactionCharge(candidate)) {
            candidateDate = candidate.transactions.reduce((earliest, tx) => {
                return tx.event_date < earliest ? tx.event_date : earliest;
            }, candidate.transactions[0].event_date);
        }
        else {
            candidateDate = candidate.documents.reduce((latest, doc) => {
                if (!doc.date)
                    return latest;
                return doc.date > latest ? doc.date : latest;
            }, candidate.documents[0].date);
        }
        return isWithinDateWindow(sourceDate, candidateDate, dateWindowMonths);
    });
    // Step 6: Filter candidates using candidate filter logic
    // Note: Additional filtering (is_fee, null checks) is handled by aggregators
    // which will throw errors for invalid data
    // Step 7: Exclude candidates with same chargeId
    const sameChargeCandidate = windowFilteredCandidates.find(c => c.chargeId === sourceCharge.chargeId);
    if (sameChargeCandidate) {
        throw new Error(`Candidate charge ${sameChargeCandidate.chargeId} has the same ID as source charge`);
    }
    // Step 8: Score all remaining candidates
    const scoredCandidates = [];
    for (const candidate of windowFilteredCandidates) {
        try {
            let matchScore;
            let txCharge;
            let docCharge;
            if (isSourceTransaction) {
                txCharge = sourceCharge;
                docCharge = candidate;
                matchScore = await scoreMatch(txCharge, docCharge, userId, injector);
            }
            else {
                txCharge = candidate;
                docCharge = sourceCharge;
                matchScore = await scoreMatch(txCharge, docCharge, userId, injector);
            }
            // Calculate date proximity for tie-breaking
            const dateProximity = calculateDateProximity(txCharge, docCharge);
            scoredCandidates.push({
                chargeId: candidate.chargeId,
                confidenceScore: matchScore.confidenceScore,
                components: matchScore.components,
                dateProximity,
                gentleMode: matchScore.gentleMode === true,
                _txCharge: txCharge,
                _docCharge: docCharge,
            });
        }
        catch {
            // Skip candidates that fail scoring (e.g., mixed currencies, invalid data)
            // This is expected behavior - not all candidates will be scoreable
            continue;
        }
    }
    // Step 9: Sort by confidence descending, then by date proximity tie-breaker
    scoredCandidates.sort((a, b) => {
        // Primary: confidence score (descending)
        if (a.confidenceScore !== b.confidenceScore) {
            return b.confidenceScore - a.confidenceScore;
        }
        // Tie-breaker:
        // - If both in gentle mode: prefer earlier document (larger proximity)
        // - Otherwise: prefer closer dates (smaller proximity)
        const aProx = a.dateProximity ?? Infinity;
        const bProx = b.dateProximity ?? Infinity;
        if (a.gentleMode && b.gentleMode) {
            return bProx - aProx;
        }
        return aProx - bProx;
    });
    // Step 10: Return top N matches
    const topMatches = scoredCandidates.slice(0, maxMatches);
    // Clean up temporary fields
    return topMatches.map(({ _txCharge, _docCharge, ...match }) => match);
}
//# sourceMappingURL=single-match.provider.js.map