/**
 * Turns a Promptfoo results file into one `consumerVerification` artifact
 * per (run, mode, task, model) by calling the consumer's verifier module.
 * Non-blocking: verifier failures warn and skip, never a deduction (D0047).
 *
 * @see docs/decisions/D0047-deterministic-symbol-preflight-lane-separation.md
 */
import { readFileSync } from "node:fs";
import { resolveVariantMode, } from "../../_vendor/ailf-core/index.js";
import { loadVerifier, VerifierTimeoutError, } from "./load-verifier.js";
/**
 * Consecutive verifier timeouts that trip the circuit breaker — a hung
 * verifier would otherwise stall the run for rows × timeoutMs. Throws and
 * invalid verdicts are cheap and row-specific, so they reset the counter.
 */
const MAX_CONSECUTIVE_TIMEOUTS = 3;
/** Map a per-row verification key to a stable string for in-memory lookup. */
export function consumerVerificationKey(key) {
    return `${key.run}/${key.mode}/${key.task}/${key.model}`;
}
export async function emitConsumerVerification(input) {
    const reports = new Map();
    const refs = [];
    if (!input.config) {
        // No `consumerVerification` block — the lane is additive, so stay silent.
        return { reports, refs };
    }
    let raw;
    try {
        raw = JSON.parse(readFileSync(input.resultsPath, "utf-8"));
    }
    catch (err) {
        const message = err instanceof Error ? err.message : String(err);
        console.warn(`  ⚠️  emitConsumerVerification: failed to read ${input.resultsPath} — ${message}`);
        return { reports, refs };
    }
    const wrapper = raw.results && "results" in raw.results
        ? raw.results
        : raw;
    const rows = wrapper?.results ?? [];
    if (rows.length === 0)
        return { reports, refs };
    let verify;
    try {
        verify = await loadVerifier(input.config);
    }
    catch (err) {
        const message = err instanceof Error ? err.message : String(err);
        console.warn(`  ⚠️  emitConsumerVerification: failed to load verifier ${input.config.module} — ${message}`);
        return { reports, refs };
    }
    // Promptfoo emits one row per assertion for the same candidate; the
    // verdict depends only on the candidate's text, so one report per
    // (task, model) suffices.
    const seen = new Set();
    const emits = [];
    let consecutiveTimeouts = 0;
    for (const row of rows) {
        const rawTaskId = row.testCase?.description ?? "unknown-task";
        const modelId = row.provider?.id ?? row.provider?.label ?? "unknown-model";
        const { mode: axisMode, task: axisTask } = resolveVariantMode(rawTaskId, input.mode);
        const baseAssoc = {
            run: input.ctx.runId,
            mode: axisMode,
            task: axisTask,
            model: modelId,
        };
        const key = consumerVerificationKey(baseAssoc);
        if (seen.has(key))
            continue;
        seen.add(key);
        const output = row.response?.output;
        if (typeof output !== "string" || output.length === 0)
            continue;
        const startedAt = Date.now();
        let verdict;
        try {
            verdict = await verify(output, {
                task: axisTask,
                model: modelId,
                mode: axisMode,
            });
        }
        catch (err) {
            const message = err instanceof Error ? err.message : String(err);
            console.warn(`  ⚠️  emitConsumerVerification: verifier failed for ${key} — ${message}`);
            if (err instanceof VerifierTimeoutError) {
                consecutiveTimeouts++;
                if (consecutiveTimeouts >= MAX_CONSECUTIVE_TIMEOUTS) {
                    console.warn(`  ⚠️  emitConsumerVerification: ${consecutiveTimeouts} consecutive verifier timeouts — disabling the verification lane for the remaining rows`);
                    break;
                }
            }
            else {
                consecutiveTimeouts = 0;
            }
            continue;
        }
        consecutiveTimeouts = 0;
        // Not applicable — the verifier declares this response outside its
        // scope; no report, the rubric keeps full authority (never-deduct).
        if (!verdict.applicable)
            continue;
        const report = {
            verifier: input.config.module,
            pass: verdict.pass,
            score: verdict.score,
            reason: verdict.reason,
            durationMs: Date.now() - startedAt,
        };
        reports.set(key, report);
        // `ArtifactWriter.emit` contractually never rejects, but these promises
        // float unawaited — a contract-violating writer would surface as a
        // process-fatal unhandledRejection, so catch at push time.
        emits.push(input.writer
            .emit("consumerVerification", baseAssoc, report)
            .catch(() => null));
    }
    const settled = await Promise.all(emits);
    for (const ref of settled)
        refs.push(ref);
    return { reports, refs };
}
