/**
 * # frame/paradigm-ledger — the FOUR-STATE paradigm cell ledger (ADR-0041 §3, kestrel-wa0j.30)
 *
 * ADR-0041 §3 makes every `(pane × sort-value-bundle)` cell exactly one of four states:
 *   • **ATTESTED**   — materializes byte-identically today (and is monotone forever — the ratchet).
 *   • **LATENT**     — not yet, but a NAMED train will land it (a LATENT refusal names its train).
 *   • **DEFECTIVE**  — never, for a WRITTEN reason, with a SANCTIONED PERIPHRASIS.
 *   • **UNATTESTED** — outside the closed roster (the closed-world default; never enumerated).
 *
 * This module is the SINGLE SOURCE of that table. It ships in `src/` (the package's `files`
 * excludes `tests/`), so production refusal constructors ({@link ./refusals.ts}) can read it — a
 * LATENT refusal pulls its train from HERE, never a second hand-kept list. The committed sidecar
 * `tests/golden/pane-paradigm.json` is this data SERIALIZED (generated by
 * `scripts/gen-paradigm-ledger.ts`, staleness-checked in `tests/frame.paradigm-ledger.test.ts`) —
 * the repo's generate-golden-from-source pattern (cf. `gen-detmath-golden.ts`), so the JSON can
 * never drift from this module.
 *
 * ## Totality (ADR-0041 §3: "a total function over a known grid")
 * {@link resolveCellState} is a TOTAL function over `pane × {@link PARADIGM_ROSTER}`: every cell
 * resolves to exactly one state. The listed {@link PARADIGM_CELLS} carry the non-UNATTESTED states;
 * every unlisted cell is UNATTESTED by the closed-world rule. `tests/frame.paradigm-ledger.test.ts`
 * asserts totality (no cell unclassified) and binds the ATTESTED rows to the live catalog signatures.
 *
 * ## Forward-compatibility with scheme conditioning (wa0j.44 — NOT implemented here)
 * A cell's state may later be CONDITIONED on a registry attribute (instrumentClass / SessionScheme):
 * `prior-context` is DEFECTIVE under a SessionScheme with no close boundary but ATTESTED under one
 * with a close — ONE paradigm, two conditioned cells (ADR-0041 §3). The cell KEY carries an OPTIONAL
 * {@link CellConditioning}; every current cell OMITS it (= unconditioned, applies to all schemes).
 * wa0j.44 adds scheme-conditioned cells by ADDING a more-specific conditioned cell — it NEVER
 * re-keys an existing unconditioned one. Pinning the optional field into the key shape NOW (while the
 * ledger is small) is the empty-ledger lesson (wa0j.52 / instrumentClass): an axis added to an
 * established key is an evidence-splitting migration; an optional axis reserved up front is additive.
 */

import {
  SESSION_SCHEMES,
  isSessionScheme,
  schemeHasBoundary,
  type BoundaryKind,
  type SessionScheme,
} from "./session-scheme.ts";

/** The four paradigm states (ADR-0041 §3). */
export const PARADIGM_STATES = ["ATTESTED", "LATENT", "DEFECTIVE", "UNATTESTED"] as const;
export type ParadigmState = (typeof PARADIGM_STATES)[number];

/** A state a LISTED cell may carry — UNATTESTED is the closed-world default and is NEVER listed
 * (listing it would enumerate the open world the closed-world rule exists to avoid). */
export type ListedState = Exclude<ParadigmState, "UNATTESTED">;

/**
 * The CLOSED sort roster (ADR-0041 §1/§3) — the ground index sorts a pane argument may address. It
 * is closed at any instant and opened ONLY by the sort-introduction ceremony (five artifacts in one
 * commit). Train 1A's LIVE subset is {@link LIVE_SORTS}; the rest are pinned into the roster but
 * DEFERRED (their slots arrive with later trains). A value OUTSIDE this roster is UNATTESTED and
 * refused by name.
 */
export const PARADIGM_ROSTER = [
  "Instrument",
  "LevelName",
  "Window",
  "Count",
  "SessionOrdinal",
  "ExpiryOrdinal",
  "Band",
] as const;
export type RosterSort = (typeof PARADIGM_ROSTER)[number];

/** The LIVE subset of {@link PARADIGM_ROSTER} the catalog materializes (has a catalog slot for).
 * Train 1A opened `LevelName` / `Window` / `Count`; Train 1B opened `SessionOrdinal` (`tape d-N`) and
 * `ExpiryOrdinal` (`chain e-N`) by the sort-introduction ceremony (ADR-0041 §3). */
export const LIVE_SORTS: readonly RosterSort[] = ["LevelName", "Window", "Count", "SessionOrdinal", "ExpiryOrdinal"];

/**
 * A registry attribute a cell's paradigm state may be CONDITIONED on (ADR-0041 §3 / kestrel-wa0j.44).
 * ABSENT on every current cell (= unconditioned; the state holds for every instrumentClass /
 * SessionScheme). Reserved in the key shape NOW so wa0j.44 conditions cells additively — see the
 * module header. NOT populated here (wa0j.44 is explicitly out of scope for wa0j.30).
 */
export interface CellConditioning {
  readonly instrumentClass?: string;
  readonly sessionScheme?: string;
}

/**
 * One LISTED `(pane × sort)` cell of the ledger. UNATTESTED cells are never listed (closed-world).
 * Per-state required fields (enforced by {@link validateParadigmCell}):
 *   • ATTESTED  → `form` (the taught canonical View form that materializes it).
 *   • LATENT    → `train` (the named deferred train — ADR-0041 §3: a LATENT refusal names its train).
 *   • DEFECTIVE → `reason` + `periphrasis` (the written reason + the sanctioned periphrasis).
 */
export interface ParadigmCell {
  readonly pane: string;
  readonly sort: RosterSort;
  readonly state: ListedState;
  readonly form?: string;
  readonly train?: string;
  readonly reason?: string;
  readonly periphrasis?: string;
  readonly conditioning?: CellConditioning;
}

// ─────────────────────────────────────────────────────────────────────────────
// The listed cells — every non-UNATTESTED (pane × sort) cell, SEEDED FROM REALITY
// ─────────────────────────────────────────────────────────────────────────────

/**
 * The ATTESTED cells (kestrel-wa0j.24) — each mirrors a LIVE catalog signature slot, cross-checked
 * against `PANE_CATALOG` by the ledger test (the biconditional that makes flipping one to LATENT a
 * red). These are the seed of the conformance ratchet (monotone forever, ADR-0041 §3). Train 1A
 * seeded the first three; Train 1B added `tape × SessionOrdinal` and `chain × ExpiryOrdinal`.
 *
 * ## LEDGER RECONCILIATION — why `tape × SessionOrdinal` / `chain × ExpiryOrdinal` are ATTESTED, not
 * LATENT-naming-wa0j.20 (Train 1B decision, ADR-0041 §3)
 * RENDERABLE is a TOTAL judgment (ADR-0041 §3): absence is a VALUE, never stuckness. After Train 1B
 * each sort has a LIVE catalog slot and materialization is TOTAL — `tape d-0` renders the current
 * session's real bytes (byte-identical to no-arg tape, the ratchet anchor `form` below), `tape d-N`
 * (N≥1) renders a deterministic absent-with-reason value, and a no-session-boundary scheme renders the
 * sanctioned periphrasis. The cell MATERIALIZES (byte-identically for its d-0/e-0 anchor form) — the
 * ADR's definition of ATTESTED. Leaving it LATENT ("not yet materializable") would be FALSE: the
 * address binds and is served today. The ratchet pins ONLY the d-0/e-0 anchor bytes (`tape d-0` /
 * `chain e-0`), which are monotone forever regardless of wa0j.20 — the d-N/e-N (N≥1) ABSENCE bytes are
 * a renderable-rung total value that is deliberately NOT pinned, so kestrel-wa0j.20 can thread the
 * multiday/multi-expiry DATA and change what those addresses RENDER (absence → real prior bytes)
 * WITHOUT touching the ratchet. AUTHORITY ARROW (ADR-0041 §3: materialization never consults the
 * ledger): the builders read the SessionScheme/boundary DATA (not the ledger STATE) to decide the
 * scheme-conditioned vs frame-conditioned absence; this table merely DESCRIBES, consistently, what
 * materialization independently does — the two are kept in agreement by both reading
 * {@link PANE_BOUNDARY_REQUIREMENT} + the scheme boundary declarations, never by materialization
 * branching on this state. THE DIVISION: Train 1B lands the ADDRESS (the d-N/e-N morphology + binding +
 * honest absence); kestrel-wa0j.20 lands the DATA (multiday rows, axis re-anchoring, gap-respecting
 * aggregation, multi-expiry chain).
 */
const ATTESTED_CELLS: readonly ParadigmCell[] = [
  { pane: "levels", sort: "LevelName", state: "ATTESTED", form: "levels vwap hod" },
  { pane: "tape", sort: "Window", state: "ATTESTED", form: "tape 5m" },
  { pane: "chain", sort: "Count", state: "ATTESTED", form: "chain 12" },
  { pane: "tape", sort: "SessionOrdinal", state: "ATTESTED", form: "tape d-0" },
  { pane: "chain", sort: "ExpiryOrdinal", state: "ATTESTED", form: "chain e-0" },
];

/**
 * Roster-sort cells a NAMED train will land (ADR-0041 §3: a LATENT refusal names its train). Grounded
 * in the ADR-0041 §1 Train-1 roadmap ("`chain` gains `Instrument` + `Count` + `ExpiryOrdinal`";
 * "`tape` gains bucket + `SessionOrdinal` + anchor"). Train 1A migrated the Window arg (tape) + Count
 * (chain); Train 1B added the SessionOrdinal (tape) + ExpiryOrdinal (chain) ADDRESSES — so those flipped
 * to ATTESTED (see {@link ATTESTED_CELLS}). What REMAINS LATENT is `chain × Instrument` (the marked
 * cross-instrument leg source), which Train 1B did NOT land — it arrives with the CALC cross-instrument
 * lexeme, a later train. This is what keeps the four-state grid genuinely LATENT-bearing.
 */
const LATENT_CELLS: readonly ParadigmCell[] = [
  {
    pane: "chain",
    sort: "Instrument",
    state: "LATENT",
    train:
      "chain Instrument (cross-instrument leg source) — a LATER train, NOT Train 1B (ADR-0041 §1: the " +
      "marked cross-instrument read is a CALC lexeme taking two Instrument args; Train 1A added Count, " +
      "Train 1B added ExpiryOrdinal, neither added Instrument)",
  },
];

/**
 * DEFECTIVE cells — cells that NEVER materialize, for a written reason, with a sanctioned periphrasis
 * (ADR-0041 §3). EMPTY today: no cell in the current pane/sort space is unconditionally DEFECTIVE.
 * The state is fully SUPPORTED — {@link validateParadigmCell} enforces its shape and
 * `tests/frame.paradigm-ledger.test.ts` exercises a schema-validated DEFECTIVE example — but no fake
 * entry is invented ({@link DEFECTIVE_FIRST_ARRIVAL}). The first PRODUCTION DEFECTIVE is
 * SCHEME-CONDITIONED and arrives with kestrel-wa0j.44 (`prior-context` under a no-close SessionScheme)
 * and the content-pane discipline of kestrel-wa0j.42 (social/sentiment as a principled DEFECTIVE source).
 */
const DEFECTIVE_CELLS: readonly ParadigmCell[] = [];

/** Where the first production DEFECTIVE cell arrives (documented — ADR-0041 §3). It has LANDED with
 * kestrel-wa0j.44 as a SCHEME-CONDITIONED cell (see {@link SCHEME_CONDITIONED_CELLS}); no cell in the
 * UNCONDITIONED `(pane × sort)` space is DEFECTIVE (the ratchet's equity-rth bytes stay pinned). */
export const DEFECTIVE_FIRST_ARRIVAL =
  "No UNCONDITIONED (pane × sort) cell is DEFECTIVE. The first production DEFECTIVE is " +
  "SCHEME-CONDITIONED and LANDED with kestrel-wa0j.44 (prior-context × SessionOrdinal under a " +
  "SessionScheme with no close boundary: reason 'no close under this SessionScheme', periphrasis " +
  "'anchor to the declared rolling extreme' — see schemeConditioned). A further DEFECTIVE source " +
  "arrives with kestrel-wa0j.42 (social/sentiment — no as-of source, determinism unachievable at the feed).";

// ─────────────────────────────────────────────────────────────────────────────
// Scheme-conditioned cells (ADR-0041 §3, kestrel-wa0j.44) — ONE paradigm, conditioned on the registry
// ─────────────────────────────────────────────────────────────────────────────

/**
 * The SessionOrdinal boundary each pane's read REQUIRES (ADR-0041 §3 conditioning). A pane listed here
 * is DEFECTIVE under any {@link ../frame/session-scheme.ts SessionScheme} that does NOT carry the
 * required boundary, and ATTESTED-as-today under one that does — ONE paradigm, conditioned on the
 * registry attribute (never two paradigms).
 *   • `prior-context` anchors to the prior CLOSE (the `d-1` close boundary of the SessionOrdinal
 *     stream), so it requires `close`.
 *   • `tape` (Train 1B) addresses a PRIOR SESSION (`tape d-N`, N≥1); a prior session must be delimited
 *     by a session `close`, so a scheme with no close (a perp's rolling UTC day) has no prior-session
 *     address at all — `tape × SessionOrdinal` is scheme-conditioned DEFECTIVE there (the builder
 *     renders the sanctioned periphrasis, anchor to the declared rolling extreme, never an invented
 *     session), and ATTESTED-as-today under a close-bearing scheme (equity-rth, the default). Note this
 *     is the CONDITIONED overlay on the UNCONDITIONED ATTESTED `tape × SessionOrdinal` cell — exactly
 *     the prior-context pattern (one paradigm, two conditioned cells).
 * Flipping a declaration is the red-capability lever: retarget the required boundary and the conditioned
 * cell + the production refusal follow. This is the SINGLE SOURCE of the requirement —
 * {@link resolveCellState}, {@link schemeConditionedDefect}, and the census all read it.
 */
export const PANE_BOUNDARY_REQUIREMENT: Readonly<Record<string, BoundaryKind>> = {
  "prior-context": "close",
  tape: "close",
};

/** The roster sort a boundary-conditioned cell is keyed on: `SessionOrdinal` — the monotone boundary
 * stream `d-N` indexes (ADR-0041 §1). A close/open boundary IS a SessionOrdinal address. */
export const CONDITIONED_SORT: RosterSort = "SessionOrdinal";

/** The written reason a boundary-conditioned cell is DEFECTIVE (ADR-0041 §3 exemplar, verbatim for
 * `close`). */
function conditionedReason(kind: BoundaryKind): string {
  return `no ${kind} under this SessionScheme`;
}

/** The sanctioned periphrasis for a boundary-conditioned DEFECTIVE cell (ADR-0041 §3 exemplar). */
export const CONDITIONED_PERIPHRASIS = "anchor to the declared rolling extreme";

/**
 * The scheme-conditioned DEFECTIVE cell for `(pane × scheme)`, or `undefined` when the pane has no
 * boundary requirement OR the scheme CARRIES the required boundary (then the pane is ATTESTED-as-today,
 * not DEFECTIVE). Generated from {@link PANE_BOUNDARY_REQUIREMENT} × the scheme's boundary declaration —
 * the production refusal ({@link ./refusals.ts}) and the census both read this one function, so they
 * can never quote a different reason/periphrasis than the cell states.
 */
export function schemeConditionedDefect(pane: string, scheme: SessionScheme): ParadigmCell | undefined {
  const req = PANE_BOUNDARY_REQUIREMENT[pane];
  if (req === undefined) return undefined;
  if (schemeHasBoundary(scheme, req)) return undefined; // the scheme carries the boundary ⇒ ATTESTED-as-today
  return {
    pane,
    sort: CONDITIONED_SORT,
    state: "DEFECTIVE",
    reason: conditionedReason(req),
    periphrasis: CONDITIONED_PERIPHRASIS,
    conditioning: { sessionScheme: scheme },
  };
}

/**
 * Every scheme-conditioned DEFECTIVE cell in the v1 grid (ADR-0041 §3) — the Cartesian product of the
 * boundary-requiring panes × the schemes that LACK their required boundary. This is the production
 * DEFECTIVE set (the census surfaces it); it is DELIBERATELY separate from {@link PARADIGM_CELLS} (the
 * unconditioned grid) so the unconditioned census + conformance ratchet keep the equity-rth bytes
 * pinned. Regenerating the sidecar re-derives this from the tables — it can never drift.
 */
export const SCHEME_CONDITIONED_CELLS: readonly ParadigmCell[] = Object.keys(PANE_BOUNDARY_REQUIREMENT)
  .flatMap((pane) => SESSION_SCHEMES.map((scheme) => schemeConditionedDefect(pane, scheme)))
  .filter((c): c is ParadigmCell => c !== undefined);

/** Every LISTED cell (ATTESTED + LATENT + DEFECTIVE). Unlisted cells are UNATTESTED (closed-world). */
export const PARADIGM_CELLS: readonly ParadigmCell[] = [...ATTESTED_CELLS, ...LATENT_CELLS, ...DEFECTIVE_CELLS];

// ─────────────────────────────────────────────────────────────────────────────
// The destination backlog — the burn-down's per-pane views.kestrel LATENT trains (single source)
// ─────────────────────────────────────────────────────────────────────────────

/**
 * The LATENT train each `views.kestrel` destination pane form is waiting on (ADR-0041 §3), keyed by
 * pane id. This is the SINGLE SOURCE that `tests/frame.inflection-burndown.test.ts` reads — the
 * burn-down no longer keeps its own copy. Distinct projection from {@link PARADIGM_CELLS}: those
 * classify `(pane × roster-sort)`; this names the train for the destination-corpus FORMS (mostly
 * new, not-yet-admitted sorts — named level-SETs, tape style/overlay/strip, macro/vol sub-selectors).
 */
export const DESTINATION_BACKLOG: Readonly<Record<string, string>> = {
  macro: "macro period/context sub-selectors (a later train — outside Train 1A/1B)",
  vol: "vol straddle / expected-move sub-selectors (a later train)",
  tape: "multi-arg tape — style/overlay/strip position classes (deferred; Train 1A migrated the Window arg, Train 1B added the SessionOrdinal address — style/overlay/strip remain LATENT)",
  levels: "named / band-keyed level-set idents (e.g. `registry`) — deferred; Train 1A serves individual-level idents only",
  chain: "chain sub-selectors (fair/realness) + Instrument — deferred to a LATER train (Train 1A added Count, Train 1B added ExpiryOrdinal; Instrument + fair/realness sub-selectors remain LATENT)",
};

// ─────────────────────────────────────────────────────────────────────────────
// Resolution + accessors
// ─────────────────────────────────────────────────────────────────────────────

/** True iff `sort` is a member of the closed {@link PARADIGM_ROSTER}. */
export function isRosterSort(sort: string): sort is RosterSort {
  return (PARADIGM_ROSTER as readonly string[]).includes(sort);
}

/**
 * The TOTAL four-state resolution (ADR-0041 §3). A listed unconditioned cell carries its state; every
 * other `(pane × sort)` is UNATTESTED by the closed-world rule. Total by construction: it returns a
 * {@link ParadigmState} for EVERY input, never throws, never `undefined` — the property the ledger
 * test asserts over the whole `pane × roster` grid.
 *
 * **Scheme conditioning (kestrel-wa0j.44).** When a {@link CellConditioning} supplying a
 * `sessionScheme` is passed AND the pane declares a {@link PANE_BOUNDARY_REQUIREMENT} on the
 * {@link CONDITIONED_SORT}, the state is conditioned on the registry attribute — ONE paradigm, two
 * conditioned cells: DEFECTIVE when the scheme LACKS the required boundary (a no-close perp), ATTESTED
 * (as-today) when it carries it (equity-rth). The UNCONDITIONED call (no `conditioning`) is unchanged —
 * the pane's conditioned axis is invisible to the unconditioned census + the conformance ratchet, so
 * the equity-rth bytes stay pinned.
 */
export function resolveCellState(pane: string, sort: string, conditioning?: CellConditioning): ParadigmState {
  if (!isRosterSort(sort)) return "UNATTESTED"; // a value outside the closed roster (closed-world)
  // Scheme-conditioned path: a boundary-requiring pane on the conditioned sort resolves against the
  // supplied scheme's boundary declaration (ADR-0041 §3) — DEFECTIVE if the boundary is absent, else
  // ATTESTED-as-today. Only reached when a sessionScheme is explicitly supplied (else the unconditioned
  // grid answers, keeping the ratchet's equity-rth bytes pinned).
  const scheme = conditioning?.sessionScheme;
  if (scheme !== undefined && isSessionScheme(scheme) && sort === CONDITIONED_SORT) {
    const req = PANE_BOUNDARY_REQUIREMENT[pane];
    if (req !== undefined) {
      return schemeHasBoundary(scheme, req) ? "ATTESTED" : "DEFECTIVE";
    }
  }
  const cell = PARADIGM_CELLS.find((c) => c.pane === pane && c.sort === sort && c.conditioning === undefined);
  return cell ? cell.state : "UNATTESTED";
}

/** The named LATENT train for a destination-corpus pane form, or `undefined` if the pane is not in
 * the backlog (ADR-0041 §3: a LATENT refusal names its train — pulled from HERE by the constructor). */
export function latentTrainFor(pane: string): string | undefined {
  return DESTINATION_BACKLOG[pane];
}

/** The ATTESTED cells (the conformance-ratchet seed — monotone forever, ADR-0041 §3). */
export function attestedCells(): readonly ParadigmCell[] {
  return PARADIGM_CELLS.filter((c) => c.state === "ATTESTED");
}

// ─────────────────────────────────────────────────────────────────────────────
// The executable schema (mirrors tests/golden/pane-paradigm.schema.json)
// ─────────────────────────────────────────────────────────────────────────────

/**
 * Validate ONE cell against the paradigm-cell schema, returning a list of human-readable errors
 * (empty ⇒ valid). Enforces the per-state required fields (ADR-0041 §3) so a DEFECTIVE cell can
 * never omit its reason/periphrasis, an ATTESTED cell can never omit its form, etc. Executable mirror
 * of `tests/golden/pane-paradigm.schema.json`; used by the totality test AND the DEFECTIVE
 * schema-example test (which proves the state is supported without a fake production entry).
 */
export function validateParadigmCell(cell: unknown): string[] {
  const errs: string[] = [];
  if (typeof cell !== "object" || cell === null) return ["cell is not an object"];
  const c = cell as Record<string, unknown>;
  if (typeof c.pane !== "string" || c.pane.length === 0) errs.push("pane must be a non-empty string");
  if (typeof c.sort !== "string" || !isRosterSort(c.sort)) errs.push(`sort must be one of the roster {${PARADIGM_ROSTER.join(", ")}}`);
  if (c.state !== "ATTESTED" && c.state !== "LATENT" && c.state !== "DEFECTIVE") {
    errs.push("state must be one of ATTESTED | LATENT | DEFECTIVE (UNATTESTED is the closed-world default, never listed)");
  }
  if (c.state === "ATTESTED" && (typeof c.form !== "string" || c.form.length === 0)) errs.push("an ATTESTED cell requires a non-empty `form`");
  if (c.state === "LATENT" && (typeof c.train !== "string" || c.train.length === 0)) errs.push("a LATENT cell requires a non-empty `train`");
  if (c.state === "DEFECTIVE") {
    if (typeof c.reason !== "string" || c.reason.length === 0) errs.push("a DEFECTIVE cell requires a non-empty `reason`");
    if (typeof c.periphrasis !== "string" || c.periphrasis.length === 0) errs.push("a DEFECTIVE cell requires a non-empty `periphrasis`");
  }
  if (c.conditioning !== undefined) {
    if (typeof c.conditioning !== "object" || c.conditioning === null) errs.push("`conditioning`, when present, must be an object");
  }
  return errs;
}
