/**
 * # frame/session-scheme — the SessionScheme series-registry attribute (ADR-0041 §1/§3, kestrel-wa0j.44)
 *
 * A `SessionScheme` is a SERIES-REGISTRY attribute (the ADR-0036/0038 registry slot — the same
 * surface ADR-0038 runs one axis over for Attribution): it declares an instrument's **monotone
 * boundary stream** — the ordered `open` / `close` / session-split boundaries that `SessionOrdinal`'s
 * `d-N` addresses index (ADR-0041 §1: scheme-relative ordinals). 24/7 and multi-session markets
 * therefore extend the *value inventory* of the existing `SessionOrdinal` sort WITHOUT touching the
 * closed roster — a perp's UTC-day convention and an equity RTH day are two schemes over one sort.
 *
 * ## Which boundaries a scheme carries is DATA (ADR-0041 §3)
 * Each scheme declares WHICH boundary kinds it carries ({@link SchemeBoundaries}). A scheme with no
 * `close` boundary (a perp's rolling UTC day) makes the `prior-context` pane's close address a
 * first-class ABSENCE: "this instrument's scheme has no such boundary" is a rendering, never an
 * invented midnight "close" (ADR-0041 §3 — absent-not-hidden reaches the address space). The
 * scheme-conditioned paradigm cell (`src/frame/paradigm-ledger.ts`) reads THIS table to decide
 * whether `(prior-context × SessionOrdinal)` is DEFECTIVE or ATTESTED-as-today for an instrument.
 *
 * ## Absent-default = `equity-rth` (no-churn)
 * `InstrumentSpec.sessionScheme` is OPTIONAL; an absent value is {@link DEFAULT_SESSION_SCHEME}
 * (`equity-rth`, which carries open+close). Every existing frame input — which declares no scheme —
 * therefore resolves to `equity-rth` and renders BYTE-IDENTICALLY (the empty-ledger no-churn lesson).
 *
 * ## ABSENT is the default; an out-of-roster STRING fails closed (never a silent default)
 * A DECLARED-but-out-of-roster scheme string is NOT the absent case: coercing it to `equity-rth` would
 * be a silent default (the fail-closed doctrine's cardinal sin — a made-up boundary stream rendered as
 * if declared). {@link resolveScheme} therefore REFUSES an out-of-roster string by name (naming the
 * served value + the closed roster), throwing a {@link ../frame/types.ts KernelHonestyError}; only the
 * ABSENT case (undefined/null) resolves to the default. The typed `InstrumentSpec.sessionScheme` path
 * cannot reach the refusal (its values are members of the roster or absent) — this is defence-in-depth
 * for an untyped caller, per the fail-closed non-negotiable.
 */

import { KernelHonestyError } from "./types.ts";

/** The three boundary KINDS a SessionScheme's monotone stream may carry (ADR-0041 §1). */
export const BOUNDARY_KINDS = ["open", "close", "session-split"] as const;
export type BoundaryKind = (typeof BOUNDARY_KINDS)[number];

/**
 * The CLOSED v1 SessionScheme roster (a series-registry attribute — ADR-0036/0038 slot). Opened only
 * by adding a scheme + its {@link SchemeBoundaries} declaration (the registry-attribute analogue of the
 * sort-introduction ceremony). `equity-rth` is the absent-default.
 */
export const SESSION_SCHEMES = [
  "equity-rth", // US equity/index Regular Trading Hours — open + close, one session/day (the default).
  "cme-rth-overnight", // CME futures — an RTH session split from the overnight (globex) session.
  "perp-utc-day", // crypto perpetual — a rolling UTC-day convention; NO daily close in the trading sense.
  "fx-week", // spot FX — the Wellington-open → NY-Friday-close trading week, split into daily sessions.
] as const;
export type SessionScheme = (typeof SESSION_SCHEMES)[number];

/** The scheme an instrument that DECLARES none resolves to (ADR-0041 §3 no-churn: existing inputs are
 * byte-identical). `equity-rth` carries both open and close, so every legacy equity frame is unchanged. */
export const DEFAULT_SESSION_SCHEME: SessionScheme = "equity-rth";

/**
 * The boundary declaration for one scheme (ADR-0041 §3: which boundary kinds it carries is DATA). A
 * scheme lacking `close` carries a {@link rollingAnchor} descriptor instead — the sanctioned
 * periphrasis names it ("anchor to the declared rolling extreme") so the pane renders that declared
 * anchor, never an invented midnight close.
 */
export interface SchemeBoundaries {
  readonly open: boolean;
  readonly close: boolean;
  readonly sessionSplit: boolean;
  /** The declared rolling extreme a no-close scheme anchors to instead of a close (e.g. a perp's
   * `rolling 24h extreme`). Present iff `close` is false — the periphrasis has something to name. */
  readonly rollingAnchor?: string;
}

/** The per-scheme boundary declarations (the DATA ADR-0041 §3 conditions the paradigm cell on). */
export const SESSION_SCHEME_BOUNDARIES: Readonly<Record<SessionScheme, SchemeBoundaries>> = {
  "equity-rth": { open: true, close: true, sessionSplit: false },
  "cme-rth-overnight": { open: true, close: true, sessionSplit: true },
  "perp-utc-day": { open: false, close: false, sessionSplit: false, rollingAnchor: "rolling 24h extreme" },
  "fx-week": { open: true, close: true, sessionSplit: true },
};

/** True iff `s` is a member of the closed {@link SESSION_SCHEMES} roster. */
export function isSessionScheme(s: string): s is SessionScheme {
  return (SESSION_SCHEMES as readonly string[]).includes(s);
}

/**
 * The typed refusal for a DECLARED-but-out-of-roster SessionScheme string (ADR-0041 §1/§3): it names
 * the served value + the closed {@link SESSION_SCHEMES} roster (the unknown-value-naming-roster idiom),
 * so the fix is legible — declare the scheme + its boundaries, or correct the typo. Fail-closed: an
 * unknown scheme has no boundary declaration, so its `open`/`close`/split stream cannot be honestly
 * rendered — refusing is the only truthful answer (never a silent `equity-rth`).
 */
export function unknownSchemeRefusal(served: string): KernelHonestyError {
  return new KernelHonestyError(
    `SessionScheme "${served}" is not in the closed roster {${SESSION_SCHEMES.join(", ")}} — a ` +
      `series-registry attribute must name a DECLARED scheme (each carries its own boundary stream). ` +
      `Judgment REFUSED (fail-closed, never a silent equity-rth default): an undeclared scheme has no ` +
      `open/close/session-split declaration, so its boundaries cannot be honestly rendered. Add the ` +
      `scheme + its SchemeBoundaries to open the roster, or correct the value (ADR-0041 §1/§3).`,
  );
}

/**
 * Resolve a declared-or-absent scheme to a concrete one. ABSENT (undefined/null) ⇒
 * {@link DEFAULT_SESSION_SCHEME} (`equity-rth`, no-churn). A DECLARED value OUTSIDE the closed roster
 * FAILS CLOSED — it throws {@link unknownSchemeRefusal} (naming the value + the roster) rather than
 * fabricate an unknown scheme's boundaries or silently coerce to the default. ABSENT is not UNKNOWN:
 * only an explicit out-of-roster string refuses; a missing declaration stays the default.
 */
export function resolveScheme(scheme: string | undefined | null): SessionScheme {
  if (scheme === undefined || scheme === null) return DEFAULT_SESSION_SCHEME;
  if (!isSessionScheme(scheme)) throw unknownSchemeRefusal(scheme);
  return scheme;
}

/** True iff `scheme` carries the given boundary kind (ADR-0041 §3 — read from the declaration DATA). */
export function schemeHasBoundary(scheme: SessionScheme, kind: BoundaryKind): boolean {
  const b = SESSION_SCHEME_BOUNDARIES[scheme];
  return kind === "open" ? b.open : kind === "close" ? b.close : b.sessionSplit;
}

/** The declared rolling extreme a no-close scheme anchors to, or `undefined` when the scheme carries a
 * close (the periphrasis is only reachable for a no-close scheme). */
export function rollingAnchorOf(scheme: SessionScheme): string | undefined {
  return SESSION_SCHEME_BOUNDARIES[scheme].rollingAnchor;
}
