/**
 * Deterministic prompt-builder helpers for the 5 LLM diagnosis cards.
 *
 * Each function takes typed inputs and returns `{ system, user }` strings
 * (+ a `deltas` side-channel for regression-vs-baseline). The same inputs
 * always produce the same output — no randomness, no side effects.
 *
 * Per AI-SPEC §4: input tokens are bounded by truncation; the user message
 * projects only the fields each card needs.
 *
 * @see .planning/phases/05-diagnosis-engine-cli-llm-cards/05-AI-SPEC.md §4
 * @see .planning/phases/05-diagnosis-engine-cli-llm-cards/05-AI-SPEC.md §3 lines 603-664
 */
import { TOP_RECOMMENDATIONS_SYSTEM_PROMPT, WEAKEST_AREA_SYSTEM_PROMPT, LOW_CONFIDENCE_ATTRIBUTION_SYSTEM_PROMPT, DOC_ATTRIBUTION_SPOTLIGHT_SYSTEM_PROMPT, REGRESSION_VS_BASELINE_SYSTEM_PROMPT, } from "./prompts/index.js";
// ---------------------------------------------------------------------------
// Shared helper: build the docSlug allow-list from a report
// ---------------------------------------------------------------------------
/**
 * Build the allow-list of doc slugs for a report. This is the union of:
 * - report.summary.documentManifest[].slug
 * - per-score documents[].slug
 *
 * Only slugs (not documentIds) appear in the allow-list because the cards
 * reference human-readable slugs. DocumentId-only entries are skipped.
 */
export function buildDocSlugAllowList(report) {
    const slugs = new Set();
    const manifest = report.summary.documentManifest ?? [];
    for (const doc of manifest) {
        if ("slug" in doc && typeof doc.slug === "string" && doc.slug.length > 0) {
            slugs.add(doc.slug);
        }
    }
    for (const score of report.summary.scores ?? []) {
        for (const doc of score.documents ?? []) {
            if ("slug" in doc &&
                typeof doc.slug === "string" &&
                doc.slug.length > 0) {
                slugs.add(doc.slug);
            }
        }
    }
    return slugs;
}
// ---------------------------------------------------------------------------
// top-recommendations prompt builder
// ---------------------------------------------------------------------------
/**
 * Projects the 3 weakest areas + top failure modes + doc-slug allow-list
 * into the user message (~2500 input tokens).
 */
export function buildTopRecommendationsPrompt(report, allowList) {
    const scores = report.summary.scores ?? [];
    const sorted = [...scores].sort((a, b) => a.totalScore - b.totalScore);
    const weakest3 = sorted.slice(0, 3);
    const failureModes = report.summary.failureModes;
    const topModes = failureModes?.topTitles
        ?.slice(0, 5)
        .map((t) => `${t.category} (${t.count})`) ?? [];
    const allowListArr = [...allowList].slice(0, 50); // cap at 50 slugs
    const user = [
        "## Weakest Areas",
        weakest3
            .map((s) => `- ${s.feature}: totalScore=${s.totalScore}, judgmentCount=${s.testCount}`)
            .join("\n"),
        "",
        "## Top Failure Modes",
        topModes.length > 0 ? topModes.join(", ") : "(none recorded)",
        "",
        "## Document Slug Allow-List",
        "Suggestions MUST use one of these slugs:",
        allowListArr.map((s) => `- ${s}`).join("\n"),
        "",
        "Generate 1-5 actionable recommendations targeting the weakest areas above.",
    ].join("\n");
    return { system: TOP_RECOMMENDATIONS_SYSTEM_PROMPT, user };
}
// ---------------------------------------------------------------------------
// weakest-area prompt builder
// ---------------------------------------------------------------------------
/**
 * Projects the single weakest area + full failure-mode breakdown.
 */
export function buildWeakestAreaPrompt(report) {
    const scores = report.summary.scores ?? [];
    if (scores.length === 0) {
        return {
            system: WEAKEST_AREA_SYSTEM_PROMPT,
            user: "No areas in report.",
        };
    }
    const weakest = [...scores].sort((a, b) => a.totalScore - b.totalScore)[0];
    const judgmentCount = weakest.testCount ?? 0;
    const topMode = report.summary.failureModes?.topTitles?.[0]?.category ?? "unclassified";
    const user = [
        "## Weakest Area",
        `Feature: ${weakest.feature}`,
        `Total Score: ${weakest.totalScore}`,
        `Ceiling Score: ${weakest.ceilingScore}`,
        `Floor Score: ${weakest.floorScore}`,
        `Judgment Count (sampleSize): ${judgmentCount}`,
        "",
        "## Top Failure Mode Observed",
        `Category: ${topMode}`,
        "",
        "## Failure Mode by Dimension",
        report.summary.failureModes?.topTitles
            ?.slice(0, 5)
            .map((t) => `- ${t.category}: ${t.count} judgments`)
            .join("\n") ?? "(no data)",
        "",
        "Identify the area, its primary dimension, and most frequent failure mode.",
        `sampleSize in your response MUST equal exactly ${judgmentCount} (the judgment count above).`,
        judgmentCount < 10
            ? `WARNING: sampleSize=${judgmentCount} < 10 — you MUST set confidence.level = "low".`
            : "",
    ]
        .filter(Boolean)
        .join("\n");
    return { system: WEAKEST_AREA_SYSTEM_PROMPT, user };
}
// ---------------------------------------------------------------------------
// low-confidence-attribution prompt builder
// ---------------------------------------------------------------------------
/**
 * Filters to low-confidence entries (or top-N if none low), produces a table
 * for the LLM (~2000 input tokens).
 *
 * Caller short-circuits on empty `judgmentAttributions` BEFORE calling this.
 */
export function buildLowConfidenceAttributionPrompt(report, judgmentAttributions) {
    // Filter to low-confidence entries (by any attribution in the set)
    const lowConf = judgmentAttributions.filter((ja) => ja.attributions.some((a) => a.confidence.level === "low"));
    // If no low-confidence entries, use all sorted by score ascending (most
    // uncertain first). Guard against entries with empty `attributions` arrays:
    // `Math.min(...[])` is Infinity and produces an unstable sort that ranks
    // empty-attribution entries identically at the top of the prompt. The card
    // schema requires `judgmentRefs.min(1)`, so emitting empty-attribution rows
    // here forces a degraded card downstream. Caller should short-circuit to
    // missing before reaching here, but defend against the seam regressing.
    const validAttrs = judgmentAttributions.filter((ja) => ja.attributions.length > 0);
    if (validAttrs.length === 0) {
        return {
            system: LOW_CONFIDENCE_ATTRIBUTION_SYSTEM_PROMPT,
            user: "(no attribution data with non-empty attributions)",
        };
    }
    const source = lowConf.length > 0
        ? lowConf
        : [...validAttrs].sort((a, b) => {
            const aMin = Math.min(...a.attributions.map((x) => x.score));
            const bMin = Math.min(...b.attributions.map((x) => x.score));
            return aMin - bMin;
        });
    // Cap at 20 to stay within token budget
    const capped = source.slice(0, 20);
    const tableRows = capped
        .map((ja) => {
        const minConf = ja.attributions.reduce((worst, a) => a.confidence.level === "low"
            ? "low"
            : worst === "low"
                ? "low"
                : a.confidence.level === "medium"
                    ? "medium"
                    : worst, "high");
        return `| ${ja.judgmentRef} | ${ja.taskId} | ${ja.modelId} | ${ja.dimension} | ${minConf} |`;
    })
        .join("\n");
    const user = [
        "## Per-Judgment Attribution Confidence",
        `Total entries: ${judgmentAttributions.length}; Low-confidence: ${lowConf.length}`,
        "",
        "| judgmentRef | taskId | modelId | dimension | minConfidence |",
        "|-------------|--------|---------|-----------|---------------|",
        tableRows,
        "",
        lowConf.length === 0
            ? "No low-confidence entries found. Return the highest-uncertainty entry and note that confidence is well-calibrated."
            : `Identify the ${Math.min(lowConf.length, 5)} most uncertain judgments.`,
    ].join("\n");
    return { system: LOW_CONFIDENCE_ATTRIBUTION_SYSTEM_PROMPT, user };
}
// ---------------------------------------------------------------------------
// doc-attribution-spotlight prompt builder
// ---------------------------------------------------------------------------
/**
 * Aggregates attributions by documentId, picks top-5 by aggregate score,
 * emits a table for the LLM.
 *
 * Caller short-circuits on empty `judgmentAttributions` BEFORE calling this.
 */
export function buildDocAttributionSpotlightPrompt(report, judgmentAttributions) {
    // Aggregate by documentId (D0052 canonical ref)
    const byDoc = new Map();
    for (const ja of judgmentAttributions) {
        for (const a of ja.attributions) {
            const entry = byDoc.get(a.documentId) ?? {
                slug: a.slug,
                scoreSum: 0,
                count: 0,
            };
            entry.scoreSum += a.score;
            entry.count += 1;
            // Keep slug if we have it
            if (a.slug && !entry.slug)
                entry.slug = a.slug;
            byDoc.set(a.documentId, entry);
        }
    }
    // Sort by aggregate score descending, take top 5
    const sorted = [...byDoc.entries()]
        .map(([docId, v]) => ({
        documentId: docId,
        slug: v.slug,
        aggregateScore: v.scoreSum / v.count,
        signalCount: v.count,
    }))
        .filter((d) => d.slug) // only emit docs with slugs (allow-list check in Zod)
        .sort((a, b) => b.aggregateScore - a.aggregateScore)
        .slice(0, 5);
    const tableRows = sorted
        .map((d) => `| ${d.documentId} | ${d.slug ?? "(no slug)"} | ${d.aggregateScore.toFixed(3)} | ${d.signalCount} |`)
        .join("\n");
    const user = [
        "## Top Documents by Attribution Score",
        `Total unique documents: ${byDoc.size}`,
        "",
        "| documentId | slug | aggregateScore | signalCount |",
        "|------------|------|----------------|-------------|",
        tableRows,
        "",
        "For each document in the table, determine its role (supports/contradicts/missing/irrelevant).",
        "docSlug in your output MUST exactly match the slug column — do not invent slugs.",
    ].join("\n");
    return { system: DOC_ATTRIBUTION_SPOTLIGHT_SYSTEM_PROMPT, user };
}
// ---------------------------------------------------------------------------
// regression-vs-baseline prompt builder
// ---------------------------------------------------------------------------
/**
 * Computes per-area deltas in JS (failure-mode #1 mitigation), then builds
 * the user message. Returns `deltas` as a side-channel for the per-call
 * schema `.refine()`.
 */
export function buildRegressionVsBaselinePrompt(report, baseline) {
    // Build area → score maps
    const currentByArea = new Map((report.summary.scores ?? []).map((s) => [s.feature, s.totalScore]));
    const baselineByArea = new Map((baseline.summary.scores ?? []).map((s) => [s.feature, s.totalScore]));
    // Only compute deltas for areas present in BOTH reports
    const deltas = [];
    for (const [area, current] of currentByArea) {
        const base = baselineByArea.get(area);
        if (base !== undefined) {
            deltas.push({
                area,
                pointsDelta: parseFloat((current - base).toFixed(2)),
            });
        }
    }
    // Sort by absolute delta descending (most changed first), cap at 10
    const topDeltas = deltas
        .sort((a, b) => Math.abs(b.pointsDelta) - Math.abs(a.pointsDelta))
        .slice(0, 10);
    const deltaRows = topDeltas
        .map((d) => `| ${d.area} | ${d.pointsDelta > 0 ? "+" : ""}${d.pointsDelta} | ${d.pointsDelta > 0 ? "improved" : d.pointsDelta < 0 ? "regressed" : "unchanged"} |`)
        .join("\n");
    const user = [
        "## Pre-Computed Score Deltas (current minus baseline)",
        "These values are FACTS — do not modify them.",
        "",
        "| area | pointsDelta | expectedDirection |",
        "|------|-------------|-------------------|",
        deltaRows,
        "",
        "For each row, echo the exact area + pointsDelta, assign the matching direction label, and add prose drivers.",
        "Do NOT round or change any numeric value.",
        "",
        `Current run: ${report.provenance.runId}`,
        `Baseline run: ${baseline.provenance.runId}`,
    ].join("\n");
    return {
        system: REGRESSION_VS_BASELINE_SYSTEM_PROMPT,
        user,
        deltas: topDeltas,
    };
}
