/**
 * emit-eval-results.ts — decompose the promptfoo results file into the
 * per-entry descriptors that W0049's registry expects.
 *
 * Replaces the Phase-B-stopgap "route the aggregated JSON through the
 * deprecated `evalResults` bulk descriptor" path. For each test in the
 * promptfoo output we emit:
 *
 *   - `rawResults`      per (run, mode, task, model)           — the full result
 *   - `renderedPrompts` per (run, mode, task, model)           — prompt the model saw
 *   - `graderPrompts`   per (run, mode, task, model, grader)   — rubric text
 *   - `graderJudgments` per (run, mode, task, model, grader)   — {score, reason, pass}
 *
 * `testOutputs` is still emitted separately by `calculate-scores-step`
 * via `uploadTestOutputs()` (carried forward from W0048 for byte-
 * equivalence with the original rollout).
 *
 * `traces` is NOT produced here — agentic trace data flows through the
 * agent-observer, not through the promptfoo result shape. Traces
 * emission is out of scope for this helper and lands when the observer
 * integration migrates (follow-up; not in W0050).
 *
 * The "grader" axis value is the rubric dimension string produced by
 * `classifyRubric` (e.g. "task-completion", "code-correctness"). Non-
 * LLM-rubric component assertions (javascript, contains, etc.) don't
 * have a natural grader identifier and are skipped — their outcomes
 * still live inside the full `rawResults` object.
 */
import { readFileSync } from "node:fs";
import { classifyRubric, parseRubricScore, resolveVariantMode, } from "../_vendor/ailf-core/index.js";
// ---------------------------------------------------------------------------
// Public entry point
// ---------------------------------------------------------------------------
/**
 * Parse a promptfoo results file and emit the per-entry artifacts.
 *
 * Non-blocking: any individual emit failure warns but does not halt.
 * File read/parse errors are caught and logged; the caller keeps going.
 */
export async function emitPerEntryEvalResults(writer, ctx, mode, resultsPath) {
    let raw;
    try {
        raw = JSON.parse(readFileSync(resultsPath, "utf-8"));
    }
    catch (err) {
        const message = err instanceof Error ? err.message : String(err);
        console.warn(`  ⚠️  emitPerEntryEvalResults: failed to read ${resultsPath} — ${message}`);
        return;
    }
    // Promptfoo wraps results in either `{ results: { results: [...] } }`
    // (older shape) or directly as `{ results: [...] }` (some adapters).
    const wrapper = raw.results && "results" in raw.results
        ? raw.results
        : raw;
    const rows = wrapper?.results ?? [];
    if (rows.length === 0) {
        console.warn(`  ⚠️  emitPerEntryEvalResults: ${resultsPath} has no results[]`);
        return;
    }
    // W0058: fire every emit synchronously and `Promise.all` once at the
    // end. The previous `parallelMap` pattern created producer-side
    // backpressure (each worker was blocked on its own `await emit(...)`),
    // which kept the batching writer's queue shallow (≤ producer
    // concurrency). With queueing delegated entirely to the writer, the
    // batching API-Gateway writer gets a fully-populated pending queue
    // and can pack ~hundreds of entries into a single batch-sign RTT;
    // the GCS-direct writer's own `ConcurrencyLimiter` caps the PUT
    // fan-out so the 1 500-concurrent-PUT scenario cannot happen.
    const emits = [];
    for (const result of rows) {
        const rawTaskId = result.testCase?.description ?? "unknown-task";
        const modelId = result.provider?.id ?? result.provider?.label ?? "unknown-model";
        // D0033 axis convention: literacy-mode task descriptions carry a
        // `(gold)` / `(baseline)` suffix; that variant IS the `mode` axis
        // value. Stripping the suffix here keeps the writer's key aligned
        // with what `slim-report-summary#slimJudgments` and the Studio
        // `testOutputsKeyFor` hook compute on the read side. Without this,
        // signed-URL lookups for grader/judgment artifacts 404.
        const { mode: axisMode, task: axisTask } = resolveVariantMode(rawTaskId, mode);
        const baseAssoc = {
            run: ctx.runId,
            mode: axisMode,
            task: axisTask,
            model: modelId,
        };
        // rawResults — full raw entry (bounded by descriptor capBytes: 1 MB)
        emits.push(writer.emit("rawResults", baseAssoc, result));
        // renderedPrompts — what the model saw + which provider it went to
        if (result.prompt !== undefined) {
            emits.push(writer.emit("renderedPrompts", baseAssoc, {
                prompt: result.prompt,
                provider: result.provider,
            }));
        }
        // Per-grader decomposition — only LLM-rubric assertions have a
        // natural grader identity. Code assertions (javascript/contains/…)
        // show up in rawResults but not as standalone graderJudgments.
        const components = result.gradingResult?.componentResults ?? [];
        for (const comp of components) {
            if (comp.assertion?.type !== "llm-rubric")
                continue;
            const dimension = classifyRubric(comp);
            if (!dimension)
                continue;
            const graderAssoc = { ...baseAssoc, grader: dimension };
            emits.push(writer.emit("graderPrompts", graderAssoc, {
                dimension,
                assertion: comp.assertion,
            }));
            emits.push(writer.emit("graderJudgments", graderAssoc, {
                score: parseRubricScore(comp) ?? 0,
                reason: comp.reason ?? "",
                pass: comp.pass,
            }));
        }
    }
    await Promise.all(emits);
}
