/**
 * Shared section builders for the Slack notification templates. Each returns
 * SlackBlock[] (empty when its data is absent) so templates compose by listing
 * sections and the body reveals itself by field presence. Pure; no I/O, no env.
 */
import { errorSummaryTrips } from "../../services/error-summary-policy.js";
import { describeCacheState, reportCacheState, } from "../../services/report-cache-state.js";
import { actionButton, actions, changeIcon, context, fmtTokens, formatDuration, linkButton, noiseArrow, overflow, reportUrl, scoreBar, scoreDot, section, sectionFields, signedDelta, SLACK_ACK_ACTION_ID, } from "./block-kit.js";
/** A wired action is rendered unless explicitly disabled via options.actions. */
function enabled(options, id) {
    return options.actions?.[id] !== false;
}
/**
 * A not-yet-wired action renders only when explicitly opted in. The gateway
 * handles exactly one interactive action today (acknowledge, via
 * `SLACK_ACK_ACTION_ID`); `rerun`/`overflow` have no handler, so by default they
 * are suppressed rather than shipped as buttons that error pre-config and
 * silently no-op post-config.
 */
function optedIn(options, id) {
    return options.actions?.[id] === true;
}
export function overallSection(report, _options) {
    const o = report.summary.overall;
    const cmp = report.comparison
        ? `    ${noiseArrow(report.comparison.deltas.overall, report.comparison.noiseThreshold)} *${signedDelta(Math.round(report.comparison.deltas.overall))}* vs baseline`
        : "";
    const decomposition = [
        `Floor ${Math.round(o.avgFloorScore)} → Ceiling ${Math.round(o.avgCeilingScore)}`,
        `Doc lift +${Math.round(o.avgDocLift)}`,
        o.avgInfrastructureEfficiency != null
            ? `Infra eff ${Math.round(o.avgInfrastructureEfficiency * 100)}%`
            : null,
        o.avgRetrievalGap != null
            ? `Retrieval gap ${Math.round(o.avgRetrievalGap)}`
            : null,
        `Doc-quality gap ${Math.round(o.avgDocQualityGap)}`,
    ]
        .filter((s) => s !== null)
        .join("  ·  ");
    return [
        section(`${scoreDot(o.avgScore)}  *AI Literacy Score*    *${Math.round(o.avgScore)}* / 100${cmp}`),
        context(decomposition),
    ];
}
export function areaScoresSection(report, options, heading) {
    const scores = report.summary.scores;
    if (scores.length === 0)
        return [];
    const deltaFor = (area) => report.comparison?.deltas.perArea.find((d) => d.area === area)?.delta;
    if (options.areaViz === "fields") {
        return [
            sectionFields(scores.map((s) => `${scoreDot(s.totalScore)} *${s.feature}*  *${Math.round(s.totalScore)}*\n` +
                `task ${Math.round(s.taskCompletion)} · code ${Math.round(s.codeCorrectness)} · doc ${Math.round(s.docCoverage)}\n` +
                `ceil ${Math.round(s.ceilingScore)} · lift +${Math.round(s.docLift)} · gap ${Math.round(s.docQualityGap)}`)),
        ];
    }
    const noise = report.comparison?.noiseThreshold ?? 2;
    // Areas scored in this run but not the baseline carry no comparable delta
    // (W-mark-uncomparable-areas). Mark them "new" rather than a fabricated
    // triangle — but only when a comparison ran at all, so first-run reports
    // (no baseline) stay unmarked.
    const uncomparable = new Set(report.comparison?.notEvaluated ?? []);
    const lines = scores
        .map((s) => {
        const d = deltaFor(s.feature);
        const tail = d !== undefined
            ? `   ${noiseArrow(d, noise)} ${signedDelta(Math.round(d))}`
            : report.comparison && uncomparable.has(s.feature)
                ? "   — _new_"
                : "";
        return `${scoreDot(s.totalScore)} *${s.feature}*  \`${scoreBar(s.totalScore)}\` *${Math.round(s.totalScore)}*${tail}`;
    })
        .join("\n");
    return [section(`${heading ? `*${heading}*\n` : ""}${lines}`)];
}
export function comparisonSection(report) {
    const c = report.comparison;
    if (!c)
        return [];
    const head = `*Compared to baseline*   ·   overall ${noiseArrow(c.deltas.overall, c.noiseThreshold)} *${signedDelta(Math.round(c.deltas.overall))}*`;
    const lines = c.areas
        .filter((a) => !(a.change === "unchanged" && a.delta === 0))
        .map((a) => {
        // No comparable baseline → render "new" instead of a fabricated
        // `0 → score (+score)` line (W-mark-uncomparable-areas).
        if (a.change === "not-evaluated" || a.delta === undefined) {
            return `${changeIcon("not-evaluated")} *${a.area}*  _new — not scored in baseline_`;
        }
        let l = `${changeIcon(a.change)} *${a.area}*  ${Math.round(a.baseline)} → ${Math.round(a.experiment)}  (${signedDelta(Math.round(a.delta))})`;
        const dims = a.dimensions;
        if (a.change === "regressed" && Object.keys(dims).length > 0) {
            const dimStr = Object.entries(dims)
                .map(([k, v]) => `${k} ${signedDelta(Math.round(v.delta))}`)
                .join(" · ");
            l += `\n      ${dimStr}`;
        }
        return l;
    });
    const blocks = [section(head)];
    if (lines.length > 0)
        blocks.push(section(lines.join("\n")));
    blocks.push(context(`Noise threshold ±${c.noiseThreshold}   ·   improved ${c.improved.length} · regressed ${c.regressed.length} · unchanged ${c.unchanged.length}`));
    return blocks;
}
export function recommendationsSection(report) {
    const rec = report.summary.recommendations;
    if (!rec || rec.top3.length === 0)
        return [];
    const list = rec.top3
        .map((g, i) => `${i + 1}. *${g.area}* — ${g.title}`)
        .join("\n");
    const counts = Object.entries(rec.counts)
        .map(([k, v]) => `${k} ${v}`)
        .join(" · ");
    return [
        section(`:sparkles: *Top documentation gaps* — ${rec.totalGaps} total · ~+${Math.round(rec.totalPotentialLift)} potential lift\n${list}`),
        context(`By area: ${counts}`),
    ];
}
export function failureModesSection(report) {
    const fm = report.summary.failureModes;
    if (!fm)
        return [];
    const t = Object.entries(fm.counts)
        .map(([k, v]) => `\`${k}\` ×${v}`)
        .join("  ·  ");
    return [
        section(`:mag: *Failure modes* — ${fm.totalJudgments} judgments, ${Math.round(fm.classificationRate * 100)}% classified\n${t}`),
    ];
}
/** Max characters of a sample error to inline before truncating with an ellipsis. */
const SAMPLE_ERROR_MAX = 140;
function truncate(text, max) {
    const collapsed = text.replace(/\s+/g, " ").trim();
    return collapsed.length > max ? `${collapsed.slice(0, max - 1)}…` : collapsed;
}
/**
 * Shared errors section (B3): fully-errored tasks (id + classification +
 * truncated sample error), the error-rate ("N of M tests errored"), and
 * classification counts (`byClassification`). Rendered by the `errored`
 * template and reused inside `breached` / `regressed`.
 *
 * Returns `[]` unless `report.summary.errorSummary` is present AND trips the
 * shared B2 condition ({@link errorSummaryTrips}), so a run with no error data
 * — or an error rate below threshold and no fully-errored task — leaves the
 * other templates unchanged.
 */
export function errorsSection(report) {
    const es = report.summary.errorSummary;
    if (!errorSummaryTrips(es) || !es)
        return [];
    const rate = Math.round(es.errorRate * 100);
    const head = `:warning: *Test errors* — ${es.erroredTestCount} of ${es.totalTestCount} tests errored (${rate}%)`;
    const blocks = [section(head)];
    if (es.fullyErroredTasks.length > 0) {
        const lines = es.fullyErroredTasks
            .map((t) => `• *${t.taskId}* _(${t.area})_  \`${t.classification}\`\n   ${truncate(t.sampleError, SAMPLE_ERROR_MAX)}`)
            .join("\n");
        blocks.push(section(`*Fully-errored tasks*\n${lines}`));
    }
    const counts = Object.entries(es.byClassification)
        .filter(([, n]) => n > 0)
        .map(([k, n]) => `\`${k}\` ×${n}`)
        .join("  ·  ");
    if (counts)
        blocks.push(context(`By classification: ${counts}`));
    return blocks;
}
export function provenanceSection(report, options) {
    const p = report.provenance;
    if (options.provenance !== "full") {
        return [
            context(`:package: ${p.source.name}  ·  :bust_in_silhouette: ${p.owner.team ?? "—"}  ·  :label: ${p.classification}`),
        ];
    }
    const git = p.git
        ? `:twisted_rightwards_arrows: \`${p.git.branch}\`${p.git.prNumber ? `  <https://github.com/${p.git.repo}/pull/${p.git.prNumber}|#${p.git.prNumber}>` : ""} @ \`${p.git.sha.slice(0, 7)}\``
        : ":twisted_rightwards_arrows: _no git context_";
    const owner = `:bust_in_silhouette: ${p.owner.team ?? "—"}${p.owner.individual ? ` / ${p.owner.individual}` : ""}`;
    const models = p.models.map((m) => m.label).join(", ");
    const line1 = `:gear: \`${p.mode}/${p.variant ?? "—"}\`  ·  :package: <${p.source.baseUrl}|${p.source.name}>  ·  ${git}  ·  ${owner}  ·  :label: ${p.classification}  ·  :robot_face: ${models}`;
    const blocks = [context(line1)];
    if (p.purpose) {
        blocks.push(context(`:pushpin: ${p.purpose}${p.labels && p.labels.length > 0 ? `   ·   :label: ${p.labels.join(", ")}` : ""}`));
    }
    return blocks;
}
export function footerSection(report) {
    const cost = report.summary.overall.cost;
    const testCount = report.summary.scores.reduce((n, s) => n + s.testCount, 0);
    // Cache provenance: when the run was served from the remote cache, the cost
    // and token figures are inherited from a source run, not spent here — mark
    // the money line and name the source run so neither is misread as this-run
    // spend (W-cache-provenance-notifications).
    const cache = describeCacheState(reportCacheState(report));
    const costSuffix = cache
        ? cache.kind === "fully-cached"
            ? " _(inherited)_"
            : " _(partly inherited)_"
        : "";
    const parts = [
        `:test_tube: ${testCount} tests`,
        `:stopwatch: ${formatDuration(report.durationMs)}`,
        cost
            ? `:money_with_wings: $${cost.total.toFixed(2)} ($${cost.perTest.toFixed(2)}/test) · ${fmtTokens(cost.totalTokens)} tok${costSuffix}`
            : null,
        cache ? `:recycle: ${cache.label} · ${cache.sourceAttribution}` : null,
        `run \`${report.provenance.runId}\``,
        report.validity
            ? report.validity.status === "ok"
                ? ":heavy_check_mark: valid"
                : `:warning: ${report.validity.status}`
            : null,
    ].filter((s) => s !== null);
    return context(parts.join("   ·   "));
}
/** Build the report `actions` block. URL buttons always render; interactive
 *  buttons render unless gated off via options.actions. */
export function reportActions(report, options, opts) {
    const els = [];
    els.push(linkButton(opts.primaryLabel ?? "View full report", reportUrl(options.dashboardBaseUrl, report.id), {
        style: "primary",
    }));
    if (report.provenance.git?.prNumber && enabled(options, "viewPr")) {
        els.push(linkButton(`View PR #${report.provenance.git.prNumber}`, `https://github.com/${report.provenance.git.repo}/pull/${report.provenance.git.prNumber}`));
    }
    if (opts.rerun && optedIn(options, "rerun")) {
        els.push(actionButton("Re-run eval", `rerun_${report.provenance.runId}`, {
            actionId: "rerun",
        }));
    }
    if (enabled(options, "acknowledge")) {
        els.push(actionButton(opts.ackLabel ?? "Acknowledge", report.id, {
            actionId: SLACK_ACK_ACTION_ID,
            ...(opts.ackDanger
                ? {
                    style: "danger",
                    confirm: {
                        title: { type: "plain_text", text: "Acknowledge alert?" },
                        text: {
                            type: "mrkdwn",
                            text: "Marks this alert as seen for the channel and records an ack on the report. It does not change any docs.",
                        },
                        confirm: { type: "plain_text", text: "Acknowledge" },
                        deny: { type: "plain_text", text: "Cancel" },
                        style: "danger",
                    },
                }
                : {}),
        }));
    }
    if (optedIn(options, "overflow")) {
        els.push(overflow([
            { text: "Copy run ID", value: "copy_run" },
            { text: "Open in promptfoo", value: "open_promptfoo" },
            { text: "Snooze channel 1h", value: "snooze" },
        ], "overflow"));
    }
    return actions(els);
}
