/**
 * # validate — the single home for the six doctrine invariants (ADR-0004 / ADR-0005)
 *
 * The typed object model IS the language (ADR-0004), authored three ways: `parse(text)`, the
 * `builders`, and hand-built literals. For the round-trip to hold, an invariant that any one
 * surface enforces must be enforced by ALL of them — otherwise a builder-authored object can
 * exist that the parser would reject, so `parse(print(x))` throws on something only a builder
 * could make. This module is where the six invariants live, once, so every surface reads the
 * same rule and the same words:
 *
 *   1. `atomic` is reserved (never faked)                    — ADR-0005
 *   2. a cross may not be `held` (edge, not level)           — sfg8 / xrearm-1
 *   3. a cross re-arm band must be a positive width          — sfg8 / xrearm-1
 *   4. a risk budget must be a positive risk fraction        — ARCHITECTURE §6.1
 *   5. an EXIT may not condition on a mark (marks lie)       — ADR-0005 / ARCHITECTURE §6.3
 *   6. provenance may only narrow downward (never elevate)   — ARCHITECTURE §6
 *
 * `parse` throws a positioned {@link KestrelParseError} at the offending token, so it reads
 * the message text and predicates here but keeps its own `line`/`col` throw. `builders` call
 * the `refuse*` guards at construction (fail closed before a bad object exists); `print` calls
 * them before emitting (never print text `parse` would reject). Invariants 1–3 carry two
 * voices — a rich parse-time message that names the remedy, and a terse `*_RESERVED` message
 * the builder/printer share — because the parser can afford prose the object surfaces cannot.
 */

import type { Citation, Operand, ProvenanceTier, Quantity, SeriesRef, Statement, Trigger } from "./ast.ts";
import { PROV_RANK } from "./vocab.ts";

/** Fail closed on an unmodeled Trigger variant rather than silently returning "no mark". */
function assertNever(x: never, ctx: string): never {
  throw new Error(`kestrel/validate: unhandled variant in ${ctx}: ${JSON.stringify(x)}`);
}

// ── 1. `atomic` is reserved (ADR-0005) ─────────────────────────────────────────

/** The rich parse-time refusal: names why atomicity cannot be faked and what to do instead. */
export const ATOMIC_MESSAGE =
  "the `atomic` keyword is reserved and refused in v1: an atomic multi-leg structure " +
  "needs a whole-structure preflight and an atomic execution adapter, which do not exist " +
  "yet (ADR-0005). Kestrel refuses rather than silently leg a named structure sequentially " +
  "(text that parses but lies). Author the legs as separate single-leg tickets, or wait for " +
  "atomic support";

/** The terse refusal the builder and printer share byte-for-byte (object surfaces, no prose). */
export const ATOMIC_RESERVED =
  "atomic is reserved: refused until whole-structure preflight + atomic execution adapter exist";

/** Refuse to CONSTRUCT/EMIT an atomic multi-leg. Atomicity is never faked (ADR-0005). */
export function refuseAtomic(atomic: boolean | undefined): void {
  if (atomic === true) throw new Error(ATOMIC_RESERVED);
}

// ── 2. a cross cannot be `held` (sfg8 / xrearm-1) ──────────────────────────

/** The rich parse-time refusal: a cross is an edge, not a level; holding it is the anti-pattern
 * that phantom-fired in graded sim. Names both remedies (re-arm band, or hold a level). */
export const HELD_CROSS_MESSAGE =
  "a cross cannot be `held`: a cross is an edge event, not a level, so sustaining it for a " +
  "duration is the anti-pattern that phantom-fired 10/12 in graded sim on a single anomalous " +
  "print (xrearm-1). To reject a lone spurious tick, give the cross a re-arm band " +
  "(`spot crosses above hod band 5c`); to require persistence, hold a level comparison instead " +
  "(`spot > hod held 30s`). `within` over a cross stays legal (the cross occurred inside a window)";

/** The terse refusal the builder and printer share byte-for-byte. */
export const HELD_CROSS_RESERVED =
  "a cross cannot be held: a cross is an edge event, not a level — use a re-arm band instead of holding the edge (xrearm-1)";

/** Refuse to CONSTRUCT/EMIT held-over-cross. Robustness comes from a re-arm band, not from
 * sustaining an edge; `within` over a cross stays legal. */
export function refuseHeldOverCross(inner: Trigger): void {
  if (inner.kind === "cross") throw new Error(HELD_CROSS_RESERVED);
}

// ── 3. a cross re-arm band must be a positive width (sfg8 / xrearm-1) ──────

/** The terse refusal the builder and printer share byte-for-byte. */
export const BAND_POSITIVE_RESERVED =
  "a cross re-arm band must be a positive width: a zero/negative band can never re-arm — the cross would never clear its band to fire again (xrearm-1)";

/** The rich parse-time refusal, which also names the offending width. A zero/negative band's
 * clear-and-re-arm distance would be ≤ 0, so the cross could never clear its band to fire
 * again — a no-op guard, refused rather than armed. */
export function bandMessage(widthText: string): string {
  return (
    `a cross re-arm band must be a positive width (got ${widthText}); a zero/negative ` +
    `band cannot re-arm — the cross would never clear its band to fire again`
  );
}

/** Refuse to CONSTRUCT/EMIT a non-positive band (a no-op re-arm guard). */
export function refuseNonPositiveBand(band: Quantity | undefined): void {
  if (band !== undefined && !(band.value > 0)) throw new Error(BAND_POSITIVE_RESERVED);
}

// ── 4. a risk budget must be a positive risk fraction (ARCHITECTURE §6.1) ───────

/** `size × max_loss ≤ budget` (ARCHITECTURE §6.1): a non-positive risk budget can never admit
 * any real position, so it is always wrong — fail closed rather than arm a dead plan. Shared
 * on every surface: a builder-authored `budget(-0.5)` must throw exactly where the parser does,
 * or its printed text (`budget -0.5R`) would not round-trip. */
export function budgetMessage(value: number): string {
  return `a risk budget must be a positive risk fraction (got ${value}R); \`size × max_loss ≤ budget\` requires budget > 0`;
}

/** Refuse to CONSTRUCT/EMIT a non-positive risk budget. */
export function refuseNonPositiveBudget(value: number): void {
  if (!(value > 0)) throw new Error(budgetMessage(value));
}

// ── 5. an EXIT may not condition on a mark (ADR-0005; ARCHITECTURE §6.3) ────────

// The "marks lie" doctrine: the observed mid/bid/ask/last is a health signal, never a value.
// An EXIT keyed on one of them conditions the position's fate on a possibly-fictional book —
// refused rather than armed. (Resting an order AT a mark is legitimate authoring — this guards
// the *trigger*, not the price.) Shared on every surface so a builder-authored EXIT throws
// exactly where the parser does.
//
// The set carries BOTH the exchange jargon (`mid`/`bid`/`ask`/`last`) AND the plain-English words a
// written human playbook uses for the very same option quote (`mark`/`premium`) — e.g. the theta
// operator's "close at 2x premium/credit". These are synonyms for the observed option mark, so an
// EXIT conditioning on them is the identical doctrine violation and is refused the identical way
// (kestrel-markets-yrpv). Wiring them to RESOLVE instead would be exactly the "marks lie" breach the
// guard exists to prevent — the persona's intent is the supported `EXIT … @ fair`/`spot`/a level,
// which the refusal copy already teaches. `mark`/`premium` are not reserved series (vocab.ts, zero
// hits), so before this they parsed as bare simple-series operands that resolve UNKNOWN every wake —
// the EXIT armed and then silently never fired.
const MARK_ANCHORS: ReadonlySet<string> = new Set(["mid", "bid", "ask", "last", "mark", "premium"]);

/** A single-segment, un-windowed, un-selected series — text cannot distinguish it from a bare
 * identifier operand (a plan-state literal), and ADR-0004 forbids un-round-trippable
 * distinctions. Shared with the parser's structural-event path. */
export function isSimpleSeries(op: Operand): op is SeriesRef {
  return (
    op.kind === "series" &&
    op.segments.length === 1 &&
    op.segments[0]!.selector === undefined &&
    op.window === undefined
  );
}

function markOperand(op: Operand): string | undefined {
  return isSimpleSeries(op) && MARK_ANCHORS.has(op.segments[0]!.name) ? op.segments[0]!.name : undefined;
}

/** The name of the first mark operand a trigger conditions on, or undefined. */
export function findMarkInTrigger(t: Trigger): string | undefined {
  switch (t.kind) {
    case "cmp":
    case "cross":
      return markOperand(t.left) ?? markOperand(t.right);
    case "event":
      return t.of === undefined ? undefined : markOperand(t.of);
    case "break-hold":
    case "within":
    case "until":
    case "at":
      return findMarkInTrigger(t.inner);
    case "nth":
      return findMarkInTrigger(t.event);
    case "not":
      return findMarkInTrigger(t.term);
    case "and":
    case "or":
      for (const term of t.terms) {
        const m = findMarkInTrigger(term);
        if (m !== undefined) return m;
      }
      return undefined;
    case "phase":
    case "time-window":
    case "fill":
    // A 0DTE time-stop conditions on the clock / hold-duration, never on an operand — so it can
    // carry no mark (and an EXIT time-stop is exactly the legitimate, mark-free exit these guard).
    case "held-stop":
    case "clock-stop":
      return undefined;
    default:
      return assertNever(t, "findMarkInTrigger");
  }
}

export function markMessage(mark: string): string {
  return (
    `EXIT may not condition on the mark \`${mark}\`: marks lie (the observed ${mark} is a ` +
    `health signal, never a value; ADR-0005). Condition the exit on \`fair\`, \`spot\`, a ` +
    `structural level, or an org fact instead`
  );
}

/** Refuse to CONSTRUCT/EMIT an EXIT whose trigger conditions on a mark. */
export function refuseExitOnMark(when: Trigger): void {
  const mark = findMarkInTrigger(when);
  if (mark !== undefined) throw new Error(markMessage(mark));
}

// ── 6. provenance may only narrow downward (ARCHITECTURE §6) ────────────────────

// Provenance ceiling: authority only narrows downward. When a module declares a provenance
// tier, a statement inside it may not claim a *higher* tier — that is elevation, which is
// always wrong. This is the one parse-time-checkable slice of the ceiling (the channel-
// authority slice is enforced at the harness boundary, not here).
export function provenanceMessage(planTier: ProvenanceTier, moduleTier: ProvenanceTier): string {
  return (
    `provenance may only narrow downward (ARCHITECTURE §6): a \`${planTier}\` plan cannot ` +
    `sit under a \`${moduleTier}\` module — authority narrows, it never elevates`
  );
}

/** Does statement `s` (at index) elevate above the module tier? Returns the offending plan tier
 * or undefined. Shared so parse (positioned) and builder/print (plain throw) apply one rule. */
export function elevatesProvenance(
  s: Statement,
  moduleRank: number,
): ProvenanceTier | undefined {
  if (s.kind === "plan" && s.provenance?.tier !== undefined && PROV_RANK[s.provenance.tier] > moduleRank) {
    return s.provenance.tier;
  }
  return undefined;
}

/** Refuse to CONSTRUCT/EMIT a module whose statement elevates above its declared provenance. */
export function refuseProvenanceElevation(
  moduleTier: ProvenanceTier | undefined,
  statements: readonly Statement[],
): void {
  if (moduleTier === undefined) return;
  const modRank = PROV_RANK[moduleTier];
  for (const s of statements) {
    const bad = elevatesProvenance(s, modRank);
    if (bad !== undefined) throw new Error(provenanceMessage(bad, moduleTier));
  }
}

// ── 7. the `because` citation is content-hash-only, Plan/Wake-only (kestrel-rtf) ─

// The `because` clause pre-registers a Plan or Wake against a platform-side Thesis by content
// hash — tamper-evident once armed, because the byte-stable clause binds into the armed-document
// hash (`sha256(print(module))`). Three fail-closed rules, symmetric on every surface:
//   (a) the digest has the fixed shape `sha256:<64 lowercase hex>` — a bad/absent hash is refused;
//   (b) it carries a content hash ONLY — an inline thesis body is refused (the body lives
//       platform-side); and
//   (c) it may only attach to a Plan or Wake — `because` on a View or Grade is refused (never a
//       fifth statement kind, ADR-0001).

/** A Thesis content hash is exactly 64 lowercase hex chars (the repo's ubiquitous sha256 digest
 * shape — same as `bus_sha256`/`plans_sha256`/`ConfigId`; no second scheme). */
const CITATION_HASH_HEX = /^[0-9a-f]{64}$/;

/** Is `hash` a well-formed Thesis digest (64 lowercase hex chars)? Shared by parse (positional
 * throw), and the builder/printer `refuse*` guards, so one rule governs every surface. */
export function isCitationHash(hash: string): boolean {
  return CITATION_HASH_HEX.test(hash);
}

/** The rich parse-time refusal for a malformed/absent content hash — names the fixed shape and
 * the offending token. */
export function citationBadHashMessage(got: string): string {
  return (
    `a \`because\` citation needs a content hash of the fixed shape \`sha256:<64 hex>\` ` +
    `(got \`${got}\`): a Thesis digest is 64 lowercase hex chars`
  );
}

/** The rich parse-time refusal for an inline thesis body — the citation is content-hash ONLY. */
export const CITATION_INLINE_BODY_MESSAGE =
  "a `because` citation carries a content hash only — no inline body: the Thesis prose lives " +
  "platform-side (referenced by `sha256:<64 hex>`), so drop the inline text after the digest";

/** The rich parse-time refusal for `because` on a View or Grade — a citation pre-registers a
 * Plan or Wake only (never a fifth statement kind, ADR-0001). */
export function citationOnMessage(kind: "View" | "Grade"): string {
  return (
    `a \`because\` citation may only pre-register a Plan or Wake, not a ${kind}: pre-registration ` +
    `binds an executable expectation to its thesis, which a ${kind} does not author`
  );
}

/** The terse refusal the builder and printer share byte-for-byte (object surfaces, no prose). */
export const CITATION_BAD_HASH_RESERVED =
  "a `because` citation needs a content hash of the fixed shape `sha256:<64 hex>`: a Thesis digest is 64 lowercase hex chars";

/** Refuse to CONSTRUCT/EMIT a malformed citation (bad algo or a non-64-hex digest), so a hand-
 * built citation throws exactly where the parser does and its printed text always round-trips. */
export function refuseBadCitation(c: Citation): void {
  if (c.algo !== "sha256" || !isCitationHash(c.hash)) throw new Error(CITATION_BAD_HASH_RESERVED);
}
