export class RetrievalDebugBuilder {
    input;
    timingsMs = {};
    stages = [];
    queryRewrite;
    providerRerank;
    fusionBreakdown;
    filterEvents = [];
    suppressionEvents = [];
    constructor(input) {
        this.input = input;
    }
    recordFusionBreakdown(breakdown) {
        this.fusionBreakdown = breakdown;
    }
    recordRerankScores(candidates) {
        if (!this.fusionBreakdown) {
            return;
        }
        for (const candidate of candidates) {
            const entry = this.fusionBreakdown.get(candidate.knowledgeId);
            if (entry) {
                entry.rerankScore = candidate.rerankScore;
                entry.rerankDelta = candidate.rerankScore - entry.fusedScore;
            }
        }
    }
    recordFitScores(candidates) {
        if (!this.fusionBreakdown) {
            return;
        }
        for (const candidate of candidates) {
            const entry = this.fusionBreakdown.get(candidate.knowledgeId);
            if (entry && typeof candidate.fitScore === 'number') {
                entry.fitScore = candidate.fitScore;
            }
        }
    }
    recordFilterEvent(event) {
        this.filterEvents.push(event);
    }
    recordSuppressionEvent(event) {
        this.suppressionEvents.push(event);
        if (this.fusionBreakdown) {
            const entry = this.fusionBreakdown.get(event.knowledgeId);
            if (entry) {
                entry.suppressionDeltas.push(event);
            }
        }
    }
    recordTiming(name, startedAt) {
        this.timingsMs[name] = elapsedMs(startedAt);
    }
    recordElapsed(name, milliseconds) {
        this.timingsMs[name] = Math.max(0, milliseconds);
    }
    recordStage(name, candidates) {
        this.stages.push({
            name,
            candidateCount: candidates.length,
            candidates: candidates.map(toDebugCandidate),
        });
    }
    recordQueryRewrite(input) {
        // Phase 7 — also persist the gating decision when the rewrite was skipped
        // (no rewrite payload, but the gate ran). That signal is load-bearing for
        // debugging "why did the rewrite not fire on this query".
        if (!input.rewrite && !input.gated) {
            return;
        }
        this.queryRewrite = {
            originalLexicalQuery: input.originalLexicalQuery,
            rewrittenLexicalQuery: input.rewrite?.lexicalQuery ?? input.originalLexicalQuery,
            addedExactTerms: input.addedExactTerms,
            reasons: input.rewrite?.reasons ?? [],
            model: input.rewrite?.model,
            gated: input.gated,
            probeConfidence: input.probeConfidence,
            probeThreshold: input.probeThreshold,
            skipped: input.skipped,
        };
    }
    recordProviderRerank(input) {
        if (!input.decisions?.length && !input.model) {
            return;
        }
        this.providerRerank = {
            model: input.model,
            candidateCount: input.inputKnowledgeIds.length,
            inputKnowledgeIds: input.inputKnowledgeIds,
            decisions: input.decisions ?? [],
        };
    }
    buildTrace(pack) {
        return {
            fingerprint: this.input.fingerprint,
            cache: {
                key: this.input.cacheKey,
                hit: this.input.cacheHit,
                bypassed: this.input.cacheBypassed,
            },
            limits: {
                searchLimit: this.input.searchLimit,
                rerankLimit: this.input.rerankLimit,
                tokenBudget: this.input.tokenBudget,
            },
            filters: {
                rejectedKnowledgeIds: this.input.rejectedKnowledgeIds,
                decisions: [
                    ...this.input.rejectedKnowledgeIds.map((knowledgeId) => ({
                        type: 'rejected_knowledge',
                        action: 'excluded_before_search',
                        knowledgeId,
                        reason: 'Search input rejectedKnowledgeIds excluded this knowledge item from candidate searches.',
                    })),
                    ...this.input.rejectedKnowledgeIds.map((knowledgeId) => ({
                        type: 'stale_feedback_retry',
                        action: 'retry_exclusion',
                        knowledgeId,
                        reason: 'Rejected, stale, or irrelevant feedback retries pass rejectedKnowledgeIds into the next search.',
                    })),
                ],
            },
            queryRewrite: this.queryRewrite,
            providerRerank: this.providerRerank,
            timingsMs: this.timingsMs,
            stages: this.stages,
            selected: Object.fromEntries(pack.sections.map((section) => [section.name, section.items.map(toDebugCandidate)])),
            fusionBreakdown: this.fusionBreakdown ? [...this.fusionBreakdown.values()] : undefined,
            filterEvents: this.filterEvents.length > 0 ? this.filterEvents : undefined,
            suppressionEvents: this.suppressionEvents.length > 0 ? this.suppressionEvents : undefined,
        };
    }
}
export async function timed(name, work, debug) {
    const startedAt = Date.now();
    try {
        return await work;
    }
    finally {
        debug?.recordTiming(name, startedAt);
    }
}
export function stripDebugTrace(pack) {
    const { debug: _debug, ...withoutDebug } = pack;
    return withoutDebug;
}
function toDebugCandidate(candidate) {
    const ranked = candidate;
    const debugCandidate = {
        knowledgeId: candidate.knowledgeId,
        chunkId: candidate.chunkId,
        title: candidate.title,
        itemType: candidate.itemType,
        project: candidate.project,
        source: candidate.source,
        rank: candidate.rank,
        rawScore: roundScore(candidate.rawScore),
        trustLevel: candidate.trustLevel,
        tokenEstimate: candidate.tokenEstimate,
        matchReasons: ranked.matchReasons ?? [],
        references: candidate.references,
    };
    if (typeof ranked.fusedScore === 'number') {
        debugCandidate.fusedScore = roundScore(ranked.fusedScore);
    }
    if (typeof ranked.rerankScore === 'number') {
        debugCandidate.rerankScore = roundScore(ranked.rerankScore);
    }
    if (typeof ranked.finalScore === 'number') {
        debugCandidate.finalScore = roundScore(ranked.finalScore);
    }
    if (typeof ranked.fitScore === 'number') {
        debugCandidate.fitScore = roundScore(ranked.fitScore);
    }
    if (ranked.fitReasons) {
        debugCandidate.fitReasons = ranked.fitReasons;
    }
    if (ranked.fitMissingSignals) {
        debugCandidate.fitMissingSignals = ranked.fitMissingSignals;
    }
    if (ranked.evidenceCategory) {
        debugCandidate.evidenceCategory = ranked.evidenceCategory;
    }
    if (ranked.evidenceStrength) {
        debugCandidate.evidenceStrength = ranked.evidenceStrength;
    }
    if (ranked.usefulnessReason) {
        debugCandidate.usefulnessReason = ranked.usefulnessReason;
    }
    if (Array.isArray(candidate.metadata?.graphPaths)) {
        debugCandidate.graphPaths = candidate.metadata.graphPaths
            .filter((path) => Boolean(path) && typeof path === 'object')
            .slice(0, 3);
    }
    return debugCandidate;
}
function roundScore(value) {
    return Math.round(value * 10000) / 10000;
}
function elapsedMs(startedAt) {
    return Math.max(0, Date.now() - startedAt);
}
