/**
 * Pipeline step: Calculate AI Literacy Scores from eval results.
 *
 * Calls calculateAndWriteScores() from pipeline/calculate-scores.ts with
 * typed options derived from AppContext. No env bridge needed.
 */
import { existsSync, readFileSync } from "node:fs";
import { join, resolve } from "path";
import { assoc, } from "../../_vendor/ailf-core/index.js";
import { emitFileContents } from "../../artifact-capture/emit-file.js";
import { LiteracyVariant } from "../../pipeline/normalize-mode.js";
import { getStepInputPaths } from "../../pipeline/cache.js";
import { buildCacheContext } from "../cache-context.js";
import { allRequiredEvalRunsCached } from "../required-eval-runs.js";
import { calculateAndWriteScores } from "../../pipeline/calculate-scores.js";
import { checkResultsExist, checkScoreSummaryValid, } from "../../pipeline/checks.js";
import { resultsFileForMode } from "../../pipeline/eval-constants.js";
import { gradeOnce, loadGraderModel } from "../../pipeline/grader-api.js";
import { createBorderlineConsensusRunner } from "../../composition-root.js";
import { loadPreflightScoring } from "../../pipeline/preflight/load-preflight-scoring.js";
import { loadSource } from "../../sources.js";
import { uploadTestOutputs } from "../../pipeline/upload-test-outputs.js";
import { configToSourceOverrides } from "../config-to-source-overrides.js";
export class CalculateScoresStep {
    name = "calculate-scores";
    check() {
        return [];
    }
    async execute(ctx, state) {
        const start = Date.now();
        // When all required eval runs were satisfied by remote cache hits,
        // score-summary.json was already restored from the cached report.
        // Skip re-calculation — the raw eval-results files don't exist. A partial
        // cache hit (only some required runs cached) falls through to normal
        // calculation: the freshly-run sub-evals produced raw results to score.
        if (allRequiredEvalRunsCached(ctx.config, state)) {
            // Verify the restored score-summary.json is valid
            const summaryIssues = checkScoreSummaryValid(ctx.config.rootDir);
            const summaryErrors = summaryIssues.filter((i) => i.severity === "error");
            if (summaryErrors.length === 0) {
                return {
                    reason: "Remote cache hit — score-summary.json restored from cached report",
                    status: "skipped",
                };
            }
            // If the summary is invalid, fall through to normal calculation
        }
        // Primary results file to score.
        // For literacy: "full" variant uses baseline as primary; others use variant directly.
        // For other modes: use the mode name.
        const primaryResultsRun = ctx.config.mode === "literacy"
            ? ctx.config.variant === LiteracyVariant.FULL
                ? LiteracyVariant.STANDARD
                : (ctx.config.variant ?? LiteracyVariant.STANDARD)
            : ctx.config.mode;
        const resultsFile = resultsFileForMode(primaryResultsRun);
        // Precondition: results file exists
        const resultsIssues = checkResultsExist(ctx.config.rootDir, resultsFile);
        const resultsErrors = resultsIssues.filter((i) => i.severity === "error");
        if (resultsErrors.length > 0) {
            return {
                durationMs: Date.now() - start,
                error: `Results missing: ${resultsErrors.map((e) => e.message).join("; ")}`,
                status: "failed",
            };
        }
        // Resolve source once with typed overrides
        const overrides = configToSourceOverrides(ctx.config);
        let resolvedSource;
        try {
            resolvedSource = loadSource(ctx.config.source, overrides);
        }
        catch {
            // Non-fatal — proceed without source metadata
        }
        // W0198 — load preflight scoring config (silent fall-through to the
        // default weight when absent). Lazy: ignored when no preflight reports
        // were emitted upstream.
        const preflightScoring = await loadPreflightScoring(ctx.config.rootDir).catch((err) => {
            ctx.logger.warn(`[warn] W0198 preflight: failed to load preflight-scoring config — ${err instanceof Error ? err.message : String(err)}`);
            return undefined;
        });
        // CR-01 — wire the borderline-consensus runner end-to-end. The
        // composition root owns the threshold + replication defaults; the
        // orchestration step supplies the regrade entry point (gradeOnce
        // against the configured grader model). Built lazily — when no
        // judgments are extracted (or none land in the ±5 borderline band),
        // the runner short-circuits without paying the grader-model load.
        let borderlineRegradeOnce;
        try {
            const grader = loadGraderModel(ctx.config.rootDir);
            borderlineRegradeOnce = (responseText, rubricText) => gradeOnce(grader.id, responseText, rubricText, ctx.logger);
        }
        catch (err) {
            ctx.logger.warn(`[warn] borderline consensus skipped — grader model not loadable: ${err instanceof Error ? err.message : String(err)}`);
        }
        const borderlineConsensusRunner = createBorderlineConsensusRunner(ctx.config.borderlineReplications !== undefined
            ? { borderlineReplications: ctx.config.borderlineReplications }
            : {});
        let belowCritical = [];
        try {
            const result = await calculateAndWriteScores({
                allowedOrigins: ctx.config.allowedOrigins,
                ...(borderlineRegradeOnce
                    ? { borderlineConsensusRunner, borderlineRegradeOnce }
                    : {}),
                logger: ctx.logger,
                // Pass the variant for literacy (scoring uses it to decide
                // whether to read agentic results), or mode for other modes
                mode: ctx.config.mode === "literacy"
                    ? (ctx.config.variant ?? LiteracyVariant.STANDARD)
                    : ctx.config.mode,
                preflightReports: state.preflightReports,
                preflightWeight: preflightScoring?.codeCorrectnessWeight,
                resolvedSource,
                resultsPath: primaryResultsRun !== LiteracyVariant.STANDARD
                    ? join(ctx.config.rootDir, resultsFile)
                    : undefined,
                rootDir: ctx.config.rootDir,
                runId: ctx.runId,
                searchMode: ctx.config.searchMode,
                source: ctx.config.source,
            });
            belowCritical = result.belowCritical;
            if (result.testSummary) {
                state.testSummary = result.testSummary;
            }
        }
        catch (err) {
            return {
                durationMs: Date.now() - start,
                error: `calculate-scores failed: ${err instanceof Error ? err.message : String(err)}`,
                status: "failed",
            };
        }
        // Postcondition: score summary exists and is valid
        const summaryIssues = checkScoreSummaryValid(ctx.config.rootDir);
        const summaryErrors = summaryIssues.filter((i) => i.severity === "error");
        if (summaryErrors.length > 0) {
            return {
                durationMs: Date.now() - start,
                error: `Postcondition failed: ${summaryErrors.map((e) => e.message).join("; ")}`,
                status: "failed",
            };
        }
        // Propagate belowCritical into pipeline state for downstream consumers
        // (e.g., orchestrator reporting, publish step metadata).
        // This is informational — the pipeline continues to run subsequent steps.
        if (belowCritical.length > 0) {
            state.belowCritical = belowCritical;
        }
        // Capture score artifacts
        // W0050 — score-summary → scoreSummary (run-scoped bulk).
        // grader-judgments.json and test-results.json were aggregated captures
        // without registered descriptors. graderJudgments is now per-entry
        // ({run, mode, task, model, grader}) and lands via run-eval-step in
        // Slice 6.6; the aggregated file is dropped.
        const resultsDir = join(ctx.config.rootDir, "results", "latest");
        const summaryPath = join(resultsDir, "score-summary.json");
        if (existsSync(summaryPath)) {
            await emitFileContents(ctx.artifactWriter, "scoreSummary", assoc(ctx), summaryPath);
        }
        // Upload testOutputs to GCS (D0032 — non-blocking, P5).
        // Read from test-results.json rather than score-summary.json: the
        // gap-analysis step (downstream) is the one that enriches score-summary
        // with testResults, so at this point the summary still has an empty
        // testResults[]. test-results.json is written by calculateAndWriteScores
        // above and carries the full per-test shape we need for per-entry upload.
        // The full responseOutput lives in the GCS artifact; PublishReportStep
        // later strips it from the inline Content Lake document when this
        // upload succeeds.
        //
        // The emits flow through `ctx.artifactWriter`, which the composition
        // root wraps in `AccumulatingArtifactWriter`. That's where the
        // authoritative merged ref is built; `FinalizeRunStep` reads it
        // straight from the accumulator, so producer-side registration on
        // `state.artifactRefs` would only clobber the accumulator's full set
        // with a partial single-entry ref.
        //
        // W0050 — ctx.artifactWriter is always present; no guard needed.
        const testResults = tryReadTestResults(ctx.config.rootDir);
        if (testResults?.length) {
            await uploadTestOutputs(ctx.artifactWriter, ctx.runId, testResults, ctx.config.mode);
        }
        const criticalSuffix = belowCritical.length > 0
            ? ` (${belowCritical.length} area(s) below critical threshold: ${belowCritical.join(", ")})`
            : "";
        return {
            durationMs: Date.now() - start,
            status: "success",
            summary: `Scores calculated and summary written${criticalSuffix}`,
        };
    }
    cacheInputs(ctx) {
        return getStepInputPaths(ctx.config.rootDir, "calculate-scores");
    }
    cacheContext(ctx) {
        return buildCacheContext(ctx.config);
    }
}
/**
 * Read the per-test result set written by `calculateAndWriteScores`.
 *
 * This is the authoritative source for `uploadTestOutputs` at the time
 * CalculateScoresStep runs — `score-summary.json` doesn't carry
 * `testResults[]` until `gap-analysis-step` enriches it downstream.
 */
function tryReadTestResults(rootDir) {
    const path = resolve(rootDir, "results", "latest", "test-results.json");
    if (!existsSync(path))
        return undefined;
    try {
        return JSON.parse(readFileSync(path, "utf-8"));
    }
    catch {
        return undefined;
    }
}
