/**
 * Diagnosis runner — engine entry point (D0048).
 *
 * Phase 5 implements the factory body; Phase 1 shipped `diagnosisVersion` only.
 * `GeneratorContext.judgmentAttributions` is sourced once per `.run({...})` via
 * `deps.loadAttributions(runId)` reading Phase 4's
 * `runs/{runId}/attribution/{entryKey}.json` per-entry artifacts (RESEARCH
 * Landmine 11).
 *
 * @see docs/decisions/D0048-engine-homes-for-cli-api-parity.md
 * @see .planning/phases/01-foundation-contracts-cross-cutting-schemas/01-CONTEXT.md (D-02)
 * @see .planning/phases/05-diagnosis-engine-cli-llm-cards/05-RESEARCH.md (Landmine 11)
 * @see .planning/phases/05-diagnosis-engine-cli-llm-cards/05-CONTEXT.md (D-02, D-06, D-10)
 */
import { z } from "zod";
import { ARTIFACT_REGISTRY, encodeDiagnosisPathVersion, } from "../artifact-registry.js";
// ---------------------------------------------------------------------------
// Version constant (Phase 1 / VER-01 / D-02)
// ---------------------------------------------------------------------------
/**
 * Bumped when the runner's selection logic, prompt orchestration, or
 * card-set composition changes in a way that should invalidate cached
 * Diagnoses (VER-01 / D-02). Co-located here so the cache-invalidation
 * contract test reads the canonical value.
 *
 * `export const` (never `export let`) — module-scope mutables leak
 * across vitest workers (cross-cutting hazard #2).
 */
export const diagnosisVersion = "0.1.0";
// ---------------------------------------------------------------------------
// Private helpers
// ---------------------------------------------------------------------------
/**
 * Build the deterministic cache path that incorporates all four version
 * segments AND the model id (AI-SPEC §3 lines 463-473 + D-02).
 *
 * The artifact path from `ARTIFACT_REGISTRY.diagnosis.objectPath(...)` is
 * already version-scoped; we append `::${model}` to include model identity
 * in the key without changing the artifact path shape.
 */
function buildCacheKey(report, versions, model) {
    const artifactPath = ARTIFACT_REGISTRY.diagnosis.objectPath(report.provenance.runId, report.id, encodeDiagnosisPathVersion(versions.diagnosisVersion, versions.cardVersion));
    // Embed the remaining two version axes + model in the key string. The
    // artifact path already carries diagnosisVersion + cardVersion; the other
    // two axes are appended here so any single-segment bump produces a
    // distinct key.
    return `${artifactPath}::grader=${versions.graderJudgmentsVersion}::ensemble=${versions.ensembleVersion}::model=${model}`;
}
/**
 * Per-card invocation — never panics. ZodError or any other thrown value
 * both translate to a degraded card (AI-SPEC §3 lines 530-552).
 */
async function runOne(generator, report, ctx, cardType) {
    try {
        return await generator(report, ctx);
    }
    catch (err) {
        const meta = {
            cardVersion: `${cardType}@unknown`,
            generatedAt: new Date().toISOString(),
        };
        const isZodErr = err instanceof z.ZodError;
        return {
            status: "degraded",
            cardType,
            reason: err instanceof Error ? err.message : String(err),
            parseFailed: isZodErr,
            meta,
        };
    }
}
// ---------------------------------------------------------------------------
// Factory (AI-SPEC §3 lines 458-523 + D-02 / Landmine-11 deltas)
// ---------------------------------------------------------------------------
/**
 * Build a `DiagnosisRunner` whose `.run({report, versions, baseline?, refresh?})`
 * produces a `Diagnosis` with cards in registry-order.
 *
 * No module-scope `let` — all state lives in the `deps` closure and per-run
 * local variables (AI-SPEC §3 Pitfall 1).
 */
export function createDiagnosisRunner(deps) {
    return {
        async run({ report, versions, baseline, refresh }) {
            const cachePath = buildCacheKey(report, versions, deps.model);
            // Cache lookup (bypassed when --refresh).
            if (!refresh) {
                const cached = await deps.diagnosisReader(cachePath);
                if (cached !== null)
                    return cached;
            }
            // One-shot attribution load (Landmine 11 — Phase 4 per-entry artifacts).
            let judgmentAttributions;
            try {
                judgmentAttributions = await deps.loadAttributions(report.provenance.runId);
            }
            catch (err) {
                deps.logger.warn("diagnosis-runner: loadAttributions failed", {
                    runId: report.provenance.runId,
                    error: err instanceof Error ? err.message : String(err),
                });
                judgmentAttributions = undefined;
            }
            const ctx = {
                llm: deps.llm,
                model: deps.model,
                logger: deps.logger,
                progress: deps.progress,
                versions,
                runId: report.provenance.runId, // D-10: provenance.runId, NOT report.runId
                reportId: report.id,
                judgmentAttributions, // Landmine 11
                ...(baseline ? { baseline } : {}),
            };
            const cardTypes = Object.keys(deps.registry);
            const cards = [];
            let parseFailures = 0;
            for (const cardType of cardTypes) {
                const generator = deps.registry[cardType];
                // Budget enforcement: once ≤1 budget is breached, downgrade
                // subsequent parse-failing cards to "missing" before even running
                // the generator (AI-SPEC §3 lines 496-510 + must-have #4).
                // We still RUN the generator here to match the behavior spec —
                // the budget check happens AFTER the card result is obtained.
                const card = await runOne(generator, report, ctx, cardType);
                if (card.status === "degraded" && card.parseFailed) {
                    if (parseFailures >= 1) {
                        // Budget exceeded — demote to missing.
                        deps.logger.warn(`diagnosis-runner: parse-failure budget exceeded for card "${cardType}"; demoting to missing`, { reportId: report.id });
                        cards.push({
                            status: "missing",
                            cardType,
                            reason: "degraded-budget-exceeded",
                        });
                        continue;
                    }
                    parseFailures++;
                }
                cards.push(card);
            }
            const diagnosis = {
                runId: report.provenance.runId, // D-10: provenance.runId
                reportId: report.id,
                inputs: versions,
                cards,
                generatedAt: new Date().toISOString(),
            };
            // Unconditional write — a refreshed call replaces the cached Diagnosis.
            await deps.diagnosisWriter(cachePath, diagnosis);
            return diagnosis;
        },
    };
}
