/**
 * Pipeline step: FinalizeRunStep — writes the run manifest at pipeline end.
 *
 * Inserts between `GapAnalysis` and `PublishReport`. Assembles a
 * `RunManifest` from `state.artifactRefs` (populated by producer steps)
 * and the shared `RunContext` (via `buildRunContext`), then writes it to
 * `runs/{runId}/manifest.json`. The written manifest becomes the source
 * of truth for artifact locations; `PublishReportStep` snapshots the
 * `artifacts` slice into `Report.artifactManifest` (D0032).
 *
 * Design principles:
 * - Single writer — one `writeManifest()` call per pipeline run.
 * - Idempotent — retries produce the same manifest bytes for the same inputs.
 * - Skipped when no writer is wired (local/air-gapped runs stay functional).
 *
 * @see docs/decisions/D0032-run-anchored-artifact-store.md
 */
import { existsSync, readFileSync } from "node:fs";
import { resolve } from "node:path";
import { AccumulatingArtifactWriter } from "../../artifact-capture/accumulating-artifact-writer.js";
import { InstrumentedArtifactWriter } from "../../artifact-capture/instrumented-artifact-writer.js";
import { buildRunContext } from "../../pipeline/run-context.js";
import { loadSource } from "../../sources.js";
import { configToSourceOverrides } from "../config-to-source-overrides.js";
/**
 * Walk a writer's `inner` decorator chain looking for an
 * `AccumulatingArtifactWriter`. Composition root wraps the accumulator
 * in `InstrumentedArtifactWriter` when `AILF_UPLOAD_METRICS=1`, so a
 * naive `instanceof AccumulatingArtifactWriter` check misses it and the
 * manifest comes out empty. Rather than teaching every caller about the
 * instrumentation wrapper, unwrap once here.
 *
 * `MAX_DEPTH` is a safety belt against a future decorator chain
 * accidentally introducing a cycle — the current writers can't, but one
 * `inner` self-reference would otherwise spin forever.
 */
const FIND_ACCUMULATOR_MAX_DEPTH = 8;
function findAccumulator(writer) {
    let cursor = writer;
    for (let depth = 0; cursor && depth < FIND_ACCUMULATOR_MAX_DEPTH; depth++) {
        if (cursor instanceof AccumulatingArtifactWriter)
            return cursor;
        if (cursor instanceof InstrumentedArtifactWriter) {
            cursor = cursor.inner;
            continue;
        }
        return null;
    }
    return null;
}
export class FinalizeRunStep {
    pipelineStart;
    options;
    name = "finalize-run";
    optional = true;
    constructor(pipelineStart, options = {}) {
        this.pipelineStart = pipelineStart;
        this.options = options;
    }
    check() {
        return [];
    }
    async execute(ctx, state) {
        const start = Date.now();
        // W0050 — `ctx.artifactWriter` is now required on AppContext
        // (composition root always provides one; NoOpArtifactWriter when
        // `--no-artifacts`). The pre-W0050 guard that returned "skipped" has
        // been removed — a NoOp writer's writeManifest returns null and the
        // code below already handles that as a non-blocking failure.
        // Resolve the source (same input buildProvenance uses).
        const overrides = configToSourceOverrides(ctx.config);
        const resolvedSource = loadSource(ctx.config.source, overrides);
        // Optional: try to read the on-disk summary for test mode inference,
        // but don't fail finalize if it's missing — the manifest should still
        // be written so artifacts have a catalog.
        const maybeSummary = tryReadScoreSummary(ctx.config.rootDir);
        const runContext = buildRunContext({
            areas: maybeSummary?.scores?.map((s) => s.feature) ?? ctx.config.areas ?? [],
            callerGit: ctx.config.callerGit,
            callerEnvelope: ctx.config.callerEnvelope,
            evalFingerprint: state.evalFingerprint ?? this.options.evalFingerprint,
            logger: ctx.logger,
            mode: ctx.config.mode,
            rootDir: ctx.config.rootDir,
            source: resolvedSource,
            taskIds: ctx.config.tasks,
            variant: ctx.config.variant,
            requestedModelIds: ctx.config.models,
        });
        // W0051 revisit: the composition-root wraps `ctx.artifactWriter` in
        // `AccumulatingArtifactWriter`, which keeps a map of every ref any
        // producer emitted this run — the authoritative FULL set, merged by
        // entry key. `state.artifactRefs` is a producer-side fallback for
        // writers that don't accumulate (NoOp / plain decorators). The
        // accumulator wins per type when both exist, because producer-side
        // registrations tend to capture only the last ref from a parallel
        // batch and would otherwise clobber the merged entries list.
        //
        // W0058: `findAccumulator` unwraps `InstrumentedArtifactWriter` too
        // so the manifest stays fully populated when `AILF_UPLOAD_METRICS=1`.
        const accumulator = findAccumulator(ctx.artifactWriter);
        const aggregated = accumulator
            ? accumulator.getAccumulatedArtifactRefs()
            : {};
        const artifacts = {
            ...(state.artifactRefs ?? {}),
            ...aggregated,
        };
        const manifest = {
            version: 1,
            runId: ctx.runId,
            createdAt: new Date().toISOString(),
            durationMs: Date.now() - this.pipelineStart,
            status: "completed",
            context: runContext,
            outcomes: state.testSummary
                ? { testSummary: state.testSummary }
                : undefined,
            promptfooUrls: state.promptfooUrls,
            artifacts,
        };
        const ref = await ctx.artifactWriter.writeManifest(ctx.runId, manifest);
        if (!ref) {
            // Non-blocking: writer logged the warning. Still populate state so
            // publish can snapshot `artifacts` even without a persisted manifest.
            state.runManifest = manifest;
            return {
                durationMs: Date.now() - start,
                status: "success",
                summary: "Run manifest computed (GCS write failed — non-blocking)",
            };
        }
        state.runManifest = manifest;
        const artifactCount = Object.keys(manifest.artifacts).length;
        return {
            durationMs: Date.now() - start,
            status: "success",
            summary: `Run manifest written to ${ref.path} (${artifactCount} artifact ref${artifactCount === 1 ? "" : "s"})`,
        };
    }
}
function tryReadScoreSummary(rootDir) {
    const path = resolve(rootDir, "results", "latest", "score-summary.json");
    if (!existsSync(path))
        return undefined;
    try {
        return JSON.parse(readFileSync(path, "utf-8"));
    }
    catch {
        return undefined;
    }
}
