/**
 * # vocab — the single source for every closed lexical vocabulary
 *
 * Each runtime table below is DERIVED from a closed string-union in {@link ./ast.ts}, so the
 * vocabulary the parser recognizes and the printer emits can never silently drift from the
 * type that defines the language. Every table is a `Record<Union, …>`: removing a union
 * member leaves an excess key here (a compile error at the table), and adding a member leaves
 * a missing key here (also a compile error at the table). The human-facing ordered lists
 * (`ANCHOR_NAMES`, `TIME_UNITS_LIST`) come from `Object.keys` of those Records — insertion
 * order is stable for non-integer keys — so "expected one of …" error strings are
 * single-sourced too, never a hand-retyped third copy.
 *
 * `parse.ts` and `print.ts` consume this module; neither keeps a private copy (ADR-0004: the
 * two projections must agree, and the surest way is to read the same table).
 */

import type {
  AnchorName,
  Citation,
  ClockStop,
  CompareOp,
  FillEvent,
  GradeSubject,
  ProvenanceTier,
  Standing,
  TimeHeldStop,
  TimeUnit,
} from "./ast.ts";

/** The keys of a closed-vocabulary table, in insertion order, typed back to the union. */
function keysOf<T extends string>(table: Record<T, unknown>): readonly T[] {
  return Object.keys(table) as T[];
}

// ── Time units (TimeUnit) ──────────────────────────────────────────────────────

const TIME_UNIT_TABLE: Record<TimeUnit, true> = {
  s: true,
  m: true,
  h: true,
  d: true,
  w: true,
  mo: true,
  q: true,
  y: true,
};
/** Ordered for the "a time unit (…)" / "expected one of …" parse messages. */
export const TIME_UNITS_LIST = keysOf(TIME_UNIT_TABLE);
export const TIME_UNITS: ReadonlySet<string> = new Set(TIME_UNITS_LIST);

// ── Ordinal-literal prefixes (SessionOrdinal / ExpiryOrdinal — Train 1B, ADR-0041 §1) ───────────
//
// The two single-letter prefix words that open a tight ordinal literal in a pane-argument position:
// `d-<n>` (a SessionOrdinal — `d-0` current session, `d-1` prior…) and `e-<n>` (an ExpiryOrdinal —
// `e-0` nearest expiry…). Date-blind RELATIVE addresses (never a date — same class as wake ordinals),
// each with ONE canonical printed form so `print(parse(text))` stays byte-stable (ADR-0004). The
// parser assigns the SUPERSORT from the prefix with zero catalog knowledge (the `wf` rung); the
// catalog `ParamSlot` refines it (SessionOrdinal / ExpiryOrdinal) at the `mat` rung.
export const ORDINAL_PREFIX = "d" as const;
export const EXPIRY_PREFIX = "e" as const;

// ── Price anchors (AnchorName) ─────────────────────────────────────────────────

const ANCHOR_TABLE: Record<AnchorName, true> = {
  fair: true,
  intrinsic: true,
  basis: true,
  bid: true,
  ask: true,
  mid: true,
  last: true,
  join: true,
  improve: true,
  stub: true,
  // Emergent anchor (ADR-0030 / kestrel-ipc): admitting `spot` here auto-updates the parser's
  // accepted set (ANCHORS) AND the "expected a price: an anchor (…)" message (ANCHOR_NAMES) —
  // a single-sourced, strict-superset relaxation.
  spot: true,
};
/** Ordered for the "an anchor (fair, intrinsic, …)" parse message. */
export const ANCHOR_NAMES = keysOf(ANCHOR_TABLE);
export const ANCHORS: ReadonlySet<string> = new Set(ANCHOR_NAMES);

// ── Exit time-stop keywords (TimeHeldStop / ClockStop) — the 0DTE time-exit surface ──

/** The lead keyword for each 0DTE time-exit stop node, single-sourced so the parser that reads
 * it (`parse.ts`) and the printer that emits it (`print.ts`) — and the WATCHER grammar overlay
 * that references `clockET` (`scripts/gen-grammar.ts`) — all draw the SAME spelling from one
 * table, never a hand-retyped copy (ADR-0004; the file's single-source thesis). Derived from the
 * {@link TimeHeldStop}/{@link ClockStop} `kind` union so adding/removing a stop is a compile error
 * here. `held` is the same spelling the `break-hold` postfix uses; `clockET` is the new token
 * this axis adds (it mirrors the watcher's `atClockET` wake vocabulary). */
const TIME_STOP_TABLE: Record<TimeHeldStop["kind"] | ClockStop["kind"], string> = {
  "held-stop": "held",
  "clock-stop": "clockET",
};
export const TIME_STOP_KEYWORDS: Readonly<Record<TimeHeldStop["kind"] | ClockStop["kind"], string>> =
  TIME_STOP_TABLE;
/** `held` — the lead keyword of a `TimeHeldStop` (`EXIT held 90m`). */
export const HELD_STOP_KEYWORD = TIME_STOP_TABLE["held-stop"];
/** `clockET` — the lead keyword of a `ClockStop` (`EXIT clockET 15:40`). The NEW vocab token. */
export const CLOCK_STOP_KEYWORD = TIME_STOP_TABLE["clock-stop"];

// ── Fill-lifecycle events (FillEvent["event"]) ─────────────────────────────────

const FILL_EVENT_TABLE: Record<FillEvent["event"], true> = {
  filled: true,
  "partial-fill": true,
  unfilled: true,
  rejected: true,
  cancelled: true,
};
export const FILL_EVENTS: ReadonlySet<string> = new Set(keysOf(FILL_EVENT_TABLE));

// ── Standing lifecycle (Standing) ──────────────────────────────────────────────

const STANDING_TABLE: Record<Standing, true> = {
  authored: true,
  armed: true,
  versioned: true,
  superseded: true,
};
export const STANDINGS: ReadonlySet<string> = new Set(keysOf(STANDING_TABLE));

// ── Grade subjects (GradeSubject["what"]) ──────────────────────────────────────

const GRADE_WHAT_TABLE: Record<GradeSubject["what"], true> = {
  plan: true,
  wake: true,
  view: true,
  pod: true,
  tag: true,
};
export const GRADE_WHATS: ReadonlySet<string> = new Set(keysOf(GRADE_WHAT_TABLE));

// ── Citation content-hash algorithms (Citation["algo"]) — the `because` clause ─

/** The closed set of content-hash algorithms the `because` citation accepts (kestrel-rtf).
 * Derived from the {@link Citation} union so the lexed `sha256:<64 hex>` token shape and the
 * type can never drift; reuses the repo's ubiquitous sha256 digest shape (no second scheme). */
const CITATION_ALGO_TABLE: Record<Citation["algo"], true> = {
  sha256: true,
};
export const CITATION_ALGOS: ReadonlySet<string> = new Set(keysOf(CITATION_ALGO_TABLE));

// ── Provenance tiers + authority rank (ProvenanceTier) ─────────────────────────

/** Authority ordering for the provenance ceiling: a higher rank is more authority. The
 * Record over `ProvenanceTier` doubles as the closed-vocabulary table — membership is its
 * key set, so the rank map and the tier vocabulary can never diverge. */
export const PROV_RANK: Readonly<Record<ProvenanceTier, number>> = {
  unvetted: 0,
  candidate: 1,
  vetted: 2,
};
export const PROV_TIERS: ReadonlySet<string> = new Set(keysOf(PROV_RANK));

// ── Comparison operators (CompareOp) — ONE bidirectional map ───────────────────

/** The canonical direction the printer emits (op → punctuation). `Record<CompareOp, …>`
 * keeps it exhaustive over the union. */
export const CMP_TO_PUNCT: Readonly<Record<CompareOp, string>> = {
  gt: ">",
  ge: ">=",
  lt: "<",
  le: "<=",
  eq: "==",
  ne: "!=",
};

/** The parser's inverse (punctuation → op), derived mechanically from `CMP_TO_PUNCT` so the
 * two directions are one table, not two hand-kept copies that could disagree. */
export const CMP_FROM_PUNCT: Readonly<Record<string, CompareOp>> = Object.fromEntries(
  (Object.entries(CMP_TO_PUNCT) as [CompareOp, string][]).map(([op, punct]) => [punct, op] as const),
);

// ── Ordinals — ONE list (parser matches words, printer emits them) ─────────────

/** 1-based ordinals; index 0 (`zeroth`) is a slot filler so `ORDINALS[n]` lines up with the
 * cardinal `n` (the printer refuses n < 1, the parser matches from index 1). */
export const ORDINALS: readonly string[] = [
  "zeroth",
  "first",
  "second",
  "third",
  "fourth",
  "fifth",
  "sixth",
  "seventh",
  "eighth",
  "ninth",
  "tenth",
  "eleventh",
  "twelfth",
];
