/**
 * # frame/refusals — the CLOSED refusal-constructor union for the inflection path (ADR-0041 §1/§3)
 *
 * Every {@link KernelHonestyError} the inflection path throws is now a TYPED constructor here, one
 * per refusal kind. The message is GENERATED from the constructor — never hand-written at a throw
 * site — so each refusal kind has ONE canonical printed form (golden-tested in
 * `tests/frame.refusal-constructors.test.ts`), and "refused" is never one undifferentiated
 * condition: every message names the rung it failed on (ADR-0041 §3's judgment ladder) plus the
 * pane / slot / sort / value the fix must address.
 *
 * The canonical forms are BYTE-IDENTICAL to Train 1A's inline strings ({@link ./pane-catalog.ts},
 * kestrel-wa0j.24) — this train relocates the message construction into the constructor without
 * changing a byte, so no existing refusal fixture re-pins.
 *
 * Cell-state citations (ADR-0041 §3): a LATENT refusal names its train, PULLED FROM THE LEDGER
 * ({@link ./paradigm-ledger.ts} — the single source, so the burn-down and the refusal quote the same
 * string); a DEFECTIVE refusal names its written reason + its sanctioned periphrasis.
 *
 * Import discipline: this module imports the arg/slot TYPES from `./pane-catalog.ts` type-only (erased
 * at runtime), and `./pane-catalog.ts` imports {@link paneRefusal} as a value — so the only runtime
 * edge is pane-catalog → refusals (no cycle).
 */

import { KernelHonestyError } from "./types.ts";
import { latentTrainFor } from "./paradigm-ledger.ts";
import type { PaneSelectionArg, ParamSlot, ParamSort } from "./pane-catalog.ts";

// ── local, frozen mirrors of the arg-printing helpers (kept here so this module has no runtime
//    dependency on pane-catalog — the map/print logic is trivial and semantically pinned) ──────────

/** Print one pane arg (`5m`, `skyline`, `12`, `d-0`, `e-1`) — mirrors the language print / canonical form. */
function argStr(a: PaneSelectionArg): string {
  if (a.kind === "arg-ident") return a.name;
  if (a.kind === "arg-count") return `${a.count}`;
  if (a.kind === "arg-ordinal") return `d-${a.ordinal}`;
  if (a.kind === "arg-expiry") return `e-${a.expiry}`;
  return `${a.window.value}${a.window.unit}`;
}

/** Join a run of args for a message (`"5m vwap"`), byte-identical to `args.map(paneArgStr).join(" ")`. */
function argsStr(args: readonly PaneSelectionArg[]): string {
  return args.map(argStr).join(" ");
}

type Supersort = "ident" | "window" | "numeral" | "ordinal" | "expiry";

/** The syntactic supersort the parser assigned an arg (mirrors `pane-catalog.syntacticSupersort`). */
function syntacticSupersort(a: PaneSelectionArg): Supersort {
  switch (a.kind) {
    case "arg-ident":
      return "ident";
    case "arg-count":
      return "numeral";
    case "arg-ordinal":
      return "ordinal";
    case "arg-expiry":
      return "expiry";
    case "arg-window":
      return "window";
  }
}

/** The syntactic supersort each semantic sort refines FROM (mirrors `pane-catalog.SORT_ACCEPTS`). */
const SORT_ACCEPTS: Record<ParamSort, Supersort> = {
  Instrument: "ident",
  LevelName: "ident",
  Window: "window",
  Count: "numeral",
  SessionOrdinal: "ordinal",
  ExpiryOrdinal: "expiry",
};

/** Repair-guiding phrase for a supersort (mirrors `pane-catalog.supersortPhrase`). */
function supersortPhrase(s: Supersort): string {
  switch (s) {
    case "ident":
      return "a bare ident";
    case "window":
      return "a window (a numeral + a time unit, e.g. `5m`)";
    case "numeral":
      return "a bare count numeral (e.g. `12`)";
    case "ordinal":
      return "a session ordinal (`d-<n>`, e.g. `d-0`)";
    case "expiry":
      return "an expiry ordinal (`e-<n>`, e.g. `e-0`)";
  }
}

/** The `name:sort…` spec of a signature (mirrors the inline `slotSpec` in `pane-catalog.bindArgs`). */
function slotSpecOf(slots: readonly ParamSlot[]): string {
  return slots.map((s) => `${s.name}:${s.sort}${s.arity === "many" ? "…" : ""}`).join(", ");
}

// ── the closed refusal union ─────────────────────────────────────────────────────────────────────

/**
 * The CLOSED union of refusal kinds on the inflection path (ADR-0041 §1/§3). One member per refusal
 * kind; {@link paneRefusal} maps each to its ONE canonical {@link KernelHonestyError} message.
 */
export type PaneRefusal =
  // resolve rung — the View selects a pane / passes args the catalog rejects structurally
  | { readonly kind: "unknown-pane"; readonly view: string; readonly pane: string; readonly known: readonly string[] }
  | { readonly kind: "takes-no-args"; readonly view: string; readonly pane: string; readonly args: readonly PaneSelectionArg[] }
  // mat rung — bindArgs / refineArg (catalog-only, Frame-free)
  | { readonly kind: "no-signature"; readonly pane: string; readonly args: readonly PaneSelectionArg[] }
  | { readonly kind: "arity"; readonly pane: string; readonly args: readonly PaneSelectionArg[]; readonly slots: readonly ParamSlot[] }
  | { readonly kind: "sort-mismatch"; readonly pane: string; readonly slot: ParamSlot; readonly arg: PaneSelectionArg }
  | { readonly kind: "count-not-integer"; readonly pane: string; readonly slot: ParamSlot; readonly arg: PaneSelectionArg }
  | { readonly kind: "ordinal-not-nonneg-integer"; readonly pane: string; readonly slot: ParamSlot; readonly arg: PaneSelectionArg }
  // renderable rung — value checks that need the Frame, in the arged builders
  | { readonly kind: "window-not-servable"; readonly pane: string; readonly arg: PaneSelectionArg; readonly servedBucketMin: number }
  | { readonly kind: "unknown-value-naming-roster"; readonly pane: string; readonly slot: string; readonly value: string; readonly roster: readonly string[] }
  // cell-state refusals (ADR-0041 §3 four-state paradigm)
  | { readonly kind: "latent-cell"; readonly pane: string; readonly form: string }
  | { readonly kind: "defective-cell"; readonly pane: string; readonly form: string; readonly reason: string; readonly periphrasis: string; readonly conditioning?: string }
  // budget guard
  | { readonly kind: "over-budget"; readonly view: string; readonly paneIds: readonly string[]; readonly spent: number; readonly tokenizer: string; readonly budget: number }
  // fail-closed "unreachable" guards — a builder reached with args bindArgs should have refined/rejected
  | { readonly kind: "unreachable-bound-arg"; readonly pane: string; readonly expected: string; readonly args: readonly PaneSelectionArg[] }
  | { readonly kind: "unreachable-not-catalogued"; readonly pane: string }
  | { readonly kind: "unreachable-args-on-argless"; readonly pane: string; readonly args: readonly PaneSelectionArg[] };

/** The refusal kinds, as a closed roster (for exhaustiveness tests). */
export const PANE_REFUSAL_KINDS = [
  "unknown-pane",
  "takes-no-args",
  "no-signature",
  "arity",
  "sort-mismatch",
  "count-not-integer",
  "ordinal-not-nonneg-integer",
  "window-not-servable",
  "unknown-value-naming-roster",
  "latent-cell",
  "defective-cell",
  "over-budget",
  "unreachable-bound-arg",
  "unreachable-not-catalogued",
  "unreachable-args-on-argless",
] as const;

/**
 * Construct the ONE canonical {@link KernelHonestyError} for a refusal (ADR-0041 §1/§3). The message
 * is generated HERE from `r`'s fields — never hand-written at the throw site. Exhaustive over the
 * closed {@link PaneRefusal} union.
 */
export function paneRefusal(r: PaneRefusal): KernelHonestyError {
  return new KernelHonestyError(refusalMessage(r));
}

function refusalMessage(r: PaneRefusal): string {
  switch (r.kind) {
    case "unknown-pane":
      return (
        `View "${r.view}" names pane "${r.pane}", which is NOT in the pane catalog — the pane cannot ` +
        `render (single-source: a View selects only catalogued panes). Known panes: ` +
        `${r.known.join(", ")} (fail-closed, never a silent drop).`
      );
    case "takes-no-args":
      return (
        `View "${r.view}" passes arg${r.args.length === 1 ? "" : "s"} "${argsStr(r.args)}" ` +
        `to pane "${r.pane}", which takes no arguments — an arg a pane does not understand is refused ` +
        `(fail-closed, never silently ignored).`
      );
    case "no-signature":
      return (
        `pane "${r.pane}" received arg${r.args.length === 1 ? "" : "s"} "${argsStr(r.args)}" but declares ` +
        `no ParamSlot signature — cannot elaborate its args (fail-closed at the \`mat\` rung; a catalog defect).`
      );
    case "arity":
      return (
        `pane "${r.pane}" was served ${r.args.length} arg${r.args.length === 1 ? "" : "s"} "${argsStr(r.args)}" ` +
        `but its signature binds only [${slotSpecOf(r.slots)}] — too many arguments. Judgment REFUSED at the \`mat\` rung ` +
        `(materializable: an argument must elaborate at its slot's sort and arity, ADR-0041 §3; fail-closed, never silently dropped).`
      );
    case "sort-mismatch": {
      const want = SORT_ACCEPTS[r.slot.sort];
      const got = syntacticSupersort(r.arg);
      return (
        `pane "${r.pane}" · slot "${r.slot.name}" (sort ${r.slot.sort}): expected ${supersortPhrase(want)} but was served ` +
        `${supersortPhrase(got)} "${argStr(r.arg)}". Judgment REFUSED at the \`mat\` rung (materializable: an argument ` +
        `must elaborate at its slot's sort, ADR-0041 §3; fail-closed, never a silent coercion).`
      );
    }
    case "count-not-integer":
      return (
        `pane "${r.pane}" · slot "${r.slot.name}" (sort Count): a Count must be a positive integer, got "${argStr(r.arg)}". ` +
        `Judgment REFUSED at the \`mat\` rung (a cardinal names N nearest-money addresses; fail-closed, never a fractional count).`
      );
    case "ordinal-not-nonneg-integer":
      return (
        `pane "${r.pane}" · slot "${r.slot.name}" (sort ${r.slot.sort}): an ordinal must be a non-negative integer, got "${argStr(r.arg)}". ` +
        `Judgment REFUSED at the \`mat\` rung (a ${r.slot.sort} names an ordinal into the declared boundary stream — ` +
        `${r.slot.sort === "SessionOrdinal" ? "d-0 the current session, d-1 the prior" : "e-0 the nearest expiry, e-1 the next out"}; ` +
        `fail-closed, never a fractional or negative ordinal).`
      );
    case "window-not-servable":
      return (
        `pane "${r.pane}" cannot serve window arg "${argStr(r.arg)}": the tape rows are served pre-bucketed at ` +
        `${r.servedBucketMin}m — a renderable window must be a multiple of the served ${r.servedBucketMin}m (a sub-resolution or ` +
        `non-multiple window cannot be expressed by these rows; rendering them under a "${argStr(r.arg)}" ` +
        `label would misstate the scale — fail-closed, never mismatched data as if requested).`
      );
    case "unknown-value-naming-roster":
      return (
        `pane "${r.pane}" · slot "${r.slot}": "${r.value}" is not a known level — the levels roster is ` +
        `{${r.roster.join(", ")}}. Judgment REFUSED (an ident must name a cell the LevelSet carries; ` +
        `fail-closed, never a silent drop). Cell state: a named level-SET like \`registry\` is LATENT — the ` +
        `band-keyed level-set inflection (a later train), NOT Train 1A's individual-level idents (ADR-0041 §3).`
      );
    case "latent-cell": {
      const train = latentTrainFor(r.pane);
      const named = train ?? "(no train recorded in the ledger — a ledger gap, fail-closed)";
      return (
        `pane "${r.pane}" · form "${r.form}": the paradigm cell is LATENT — not yet materializable, but a ` +
        `named train will land it. Deferred train: ${named}. Judgment REFUSED at the \`mat\` rung ` +
        `(ADR-0041 §3: a LATENT refusal names its train; fail-closed, never a silent drop).`
      );
    }
    case "defective-cell": {
      const cond = r.conditioning !== undefined ? ` (conditioned on ${r.conditioning})` : "";
      return (
        `pane "${r.pane}" · form "${r.form}": the paradigm cell is DEFECTIVE — it never materializes${cond}. ` +
        `Written reason: ${r.reason}. Sanctioned periphrasis: ${r.periphrasis}. Judgment REFUSED ` +
        `(ADR-0041 §3: a DEFECTIVE refusal names its written reason + its sanctioned periphrasis; ` +
        `absent-not-hidden — the address has no value under this paradigm, and the periphrasis renders in its place, never an invented one).`
      );
    }
    case "over-budget":
      return (
        `View "${r.view}" is over its token budget: the selected panes (${r.paneIds.join(", ")}) ` +
        `measured ${r.spent} tokens under tokenizer "${r.tokenizer}", exceeding the budget of ${r.budget}. ` +
        "Refusing to render a View that does not fit its own budget (fail-closed — trim the View or raise its budget; " +
        "the non-configurable kernel is not counted against the View budget)."
      );
    case "unreachable-bound-arg":
      return `pane "${r.pane}" expected ${r.expected} but was reached with "${argsStr(r.args)}" (fail-closed).`;
    case "unreachable-not-catalogued":
      return `pane "${r.pane}" is not in the catalog — cannot materialize (fail-closed).`;
    case "unreachable-args-on-argless":
      return (
        `pane "${r.pane}" takes no arguments but was resolved with "${argsStr(r.args)}" ` +
        `— cannot materialize (fail-closed).`
      );
  }
}
