// Negative-context filter for the ranker (Epic #177, Issue #182). Converts annotated
// candidates into a kept/omitted partition with explicit CandidateOmissionReason values.
// Priority is fixed (pre-set reason > generated > low-relevance > duplicate); a hard maxKept
// cap demotes the overflow to "budget-exhausted". Determinism: ties break on scopePath asc.
export const DEFAULT_FILTER_OPTIONS = {
    minScore: 0.15,
    maxKept: 50,
    omitGenerated: true,
    omitNearDuplicates: true,
};
function classifyReason(entry, options) {
    const preset = entry.candidate.omitted;
    if (preset !== undefined) {
        return preset;
    }
    if (entry.generatedHint && options.omitGenerated) {
        return "generated";
    }
    if (entry.candidate.score < options.minScore) {
        return "low-relevance";
    }
    if (entry.duplicate && options.omitNearDuplicates) {
        return "near-duplicate";
    }
    return undefined;
}
function compareKept(a, b) {
    if (b.score !== a.score) {
        return b.score - a.score;
    }
    if (a.scopePath < b.scopePath) {
        return -1;
    }
    if (a.scopePath > b.scopePath) {
        return 1;
    }
    return 0;
}
function compareOmitted(a, b) {
    if (a.scopePath < b.scopePath) {
        return -1;
    }
    if (a.scopePath > b.scopePath) {
        return 1;
    }
    return 0;
}
function toOmittedEntry(scopePath, reason, nowMs) {
    return { scopePath, reason, omittedAtMs: nowMs };
}
function partition(entries, options, nowMs) {
    const kept = [];
    const lowRelevance = [];
    const omittedEntries = [];
    for (const entry of entries) {
        const reason = classifyReason(entry, options);
        if (reason === undefined) {
            kept.push(entry.candidate);
        }
        else if (reason === "low-relevance") {
            lowRelevance.push(entry.candidate);
        }
        else {
            omittedEntries.push(toOmittedEntry(entry.candidate.scopePath, reason, nowMs));
        }
    }
    return { kept, lowRelevance, omittedEntries };
}
function normalizeMaxKept(maxKept) {
    if (!Number.isFinite(maxKept)) {
        return 0;
    }
    return Math.max(0, Math.floor(maxKept));
}
function appendLowRelevanceOmissions(omittedEntries, candidates, nowMs) {
    for (const candidate of candidates) {
        omittedEntries.push(toOmittedEntry(candidate.scopePath, "low-relevance", nowMs));
    }
}
export function filterCandidates(entries, options) {
    const nowMs = options.nowMs();
    const { kept, lowRelevance, omittedEntries } = partition(entries, options, nowMs);
    const maxKept = normalizeMaxKept(options.maxKept);
    lowRelevance.sort(compareKept);
    if (kept.length === 0 && maxKept > 0) {
        const fallback = lowRelevance.shift();
        if (fallback !== undefined) {
            kept.push(fallback);
        }
    }
    appendLowRelevanceOmissions(omittedEntries, lowRelevance, nowMs);
    kept.sort(compareKept);
    const overflow = kept.splice(maxKept);
    for (const overflowed of overflow) {
        omittedEntries.push(toOmittedEntry(overflowed.scopePath, "budget-exhausted", nowMs));
    }
    omittedEntries.sort(compareOmitted);
    return { kept, omitted: omittedEntries };
}
