// Public facade for candidate ranking and negative-context filtering (Epic #177, Issue #182).
// Pure composition of signals → scoring → filter. Validates scopePath via the contracts'
// isValidScopePath (defense-in-depth) and accounts every CANDIDATE_OMISSION_REASONS key in
// the returned diagnostics so UI/audit consumers can render zeros without conditionals.
import { CANDIDATE_OMISSION_REASONS, isValidScopePath, } from "@oscharko-dev/keiko-contracts/connected-context";
import { DEFAULT_FILTER_OPTIONS, filterCandidates, } from "./filter.js";
import { DEFAULT_SCORING_WEIGHTS, computeScore } from "./scoring.js";
import { DEFAULT_GENERATED_PATTERNS, extractSignals, } from "./signals.js";
function resolveHints(hints) {
    return {
        generatedPathPatterns: hints?.generatedPathPatterns ?? DEFAULT_GENERATED_PATTERNS,
        duplicateOf: hints?.duplicateOf ?? new Map(),
    };
}
function resolveFilterOptions(filter, nowMs) {
    const base = filter ?? DEFAULT_FILTER_OPTIONS;
    return {
        minScore: base.minScore,
        maxKept: base.maxKept,
        omitGenerated: base.omitGenerated,
        omitNearDuplicates: base.omitNearDuplicates,
        nowMs,
    };
}
function groupAtomsByPath(atoms) {
    const valid = new Map();
    const invalidPaths = new Set();
    for (const candidate of atoms) {
        if (!isValidScopePath(candidate.scopePath, { mustBeRelative: true })) {
            invalidPaths.add(candidate.scopePath);
            continue;
        }
        const existing = valid.get(candidate.scopePath);
        if (existing === undefined) {
            valid.set(candidate.scopePath, [candidate]);
        }
        else {
            existing.push(candidate);
        }
    }
    return { valid, invalidPaths: [...invalidPaths] };
}
function buildAnnotated(group, input, hints, weights) {
    const annotated = [];
    for (const [scopePath, atomsForPath] of group) {
        const signals = extractSignals(atomsForPath, input.anchors, hints);
        const score = computeScore(signals, weights);
        const candidate = {
            scopePath,
            score,
            signals: signals.signals,
            omitted: undefined,
        };
        annotated.push({
            candidate,
            generatedHint: signals.generatedHint,
            duplicate: hints.duplicateOf.has(scopePath),
        });
    }
    return annotated;
}
function emptyOmittedCounts() {
    const counts = {};
    for (const reason of CANDIDATE_OMISSION_REASONS) {
        counts[reason] = 0;
    }
    return counts;
}
function tallyOmittedCounts(omitted, outsideScopeCount) {
    const counts = emptyOmittedCounts();
    for (const entry of omitted) {
        counts[entry.reason] += 1;
    }
    counts["outside-scope"] += outsideScopeCount;
    return counts;
}
export function rankCandidates(input, options = {}) {
    const clock = options.nowMs ?? Date.now;
    // Capture a single emission timestamp so every OmittedContextEntry created by this call carries
    // the same omittedAtMs. The end-of-run clock read below measures elapsed time only.
    const startMs = clock();
    const frozenStartMs = () => startMs;
    const hints = resolveHints(input.hints);
    const weights = options.weights ?? DEFAULT_SCORING_WEIGHTS;
    const { valid, invalidPaths } = groupAtomsByPath(input.atoms);
    const annotated = buildAnnotated(valid, input, hints, weights);
    const filterOptions = resolveFilterOptions(options.filter, frozenStartMs);
    const filterResult = filterCandidates(annotated, filterOptions);
    // Invalid paths cannot be represented as OmittedContextEntry values without breaking
    // ConnectedContextPack validation, so keep them diagnostics-only.
    const omittedCounts = tallyOmittedCounts(filterResult.omitted, invalidPaths.length);
    const elapsedMs = Math.max(0, clock() - startMs);
    const diagnostics = {
        totalAtoms: input.atoms.length,
        uniqueCandidates: valid.size,
        keptCount: filterResult.kept.length,
        omittedCounts,
        elapsedMs,
    };
    return { kept: filterResult.kept, omitted: filterResult.omitted, diagnostics };
}
