/**
 * # session/market-pane — the acting frame's market pane + its `@fair` chain column
 *
 * The date-blind MARKET half of every frame the Simulate driver ({@link import("./simulate.ts")}) serves:
 * spot/levels/tape + the option chain, with the chain's `fair` column priced through the SAME pure
 * {@link import("../fair/index.ts").executionFair} the fill engine prices `@fair` with. Extracted from the
 * driver so the pane projection lives beside its own small interface.
 *
 *  - {@link marketPaneOf} — `(AuthorFrame, instrument, options?, fairCtx?) → MarketPane`;
 *  - {@link chainLegFair} / {@link ChainFairContext} — ExecutionFair for ONE near-money chain leg;
 *  - {@link buildFrameOptions} / {@link OptionsOverlayConfig} — the perception arm's live `market.options`,
 *    folded causally to a frame cutoff and relabeled date-blind ({@link dateBlindOptions}).
 *
 * PURE + injected-time: no wall clock, no RNG. Every projection is a byte-identical function of its
 * inputs; the `fair` column is intrinsic-floored with a receipt or `—` (fail-closed — NEVER a fabricated
 * fair, a silent mid is forbidden, RUNTIME §4), and the options fold reads only the causal ≤-cutoff
 * prefix (no look-ahead) and relabels expiry DATE labels to relative-day tags before any token reaches
 * the date-blind agent frame.
 */

import type { BusEvent, OptionQuote } from "../bus/index.ts";
import { executionFair } from "../fair/index.ts";
import type { ChainRow, LevelSet, MarketPane, TapeRow } from "../frame/types.ts";
import {
  buildOptionsAnalytics,
  reduceOptionSurface,
  type OptionsAnalytics,
} from "../frame/options-analytics.ts";
import type { AuthorFrame } from "./day.ts";
import type { FairTauProvider } from "./clock.ts";

/** The perception arm's options-analytics overlay input (kestrel-4gl.13.13). Its presence is the
 * config gate that populates `market.options` in the live frame; absent ⇒ the minimal arm (no
 * options panes, no fold). The projection is a PURE function of the overlay events folded to the
 * frame cutoff (no wall clock, no RNG), with the MODEL honesty (stale-spot → UNKNOWN taint) intact. */
export interface OptionsOverlayConfig {
  /** A RE-READABLE source of the OPRA options-overlay tape (SPOT + BOOK events). The driver folds it
   * ONCE per frame cutoff (a lazy generator that breaks at the cutoff reads only the causal prefix),
   * so this MUST be re-iterable — pass a thunk (e.g. `() => readBusFile(path)`), not a spent iterator. */
  readonly events: () => Iterable<BusEvent>;
  /** `${expiry}:${strike}:${right}` → open interest, from the OI sidecar via `openInterestMap`. */
  readonly openInterest: ReadonlyMap<string, number>;
  /** Expiry label → close epoch-ms (the caller computes it from the session's calendar; the projection
   * only ever diffs it vs the cutoff to derive tau/dte durations, and the label never reaches the
   * agent — it is relabeled to a relative-day tag at the frame boundary). */
  readonly expiryCloseTs: ReadonlyMap<string, number>;
  /** Per-contract multiplier (default 100). */
  readonly multiplier?: number;
  /** Staleness backstop in ms: a spot older than this at the cutoff taints the GEX pane (default 120_000). */
  readonly staleMs?: number;
}

/** The fair-pricing context {@link marketPaneOf} anchors the chain `fair` column on (kestrel-121): the
 * injected time-to-expiry resolver (`fairTauYears`) + the frame cutoff `nowTs` it is evaluated at — the
 * SAME `(fairTauYears, cutoffTs)` the fill engine prices `@fair` through (`sim.ts` `fairFor`). Absent ⇒
 * the chain `fair` stays `—` (byte-identical to a pre-fair frame). */
export interface ChainFairContext {
  readonly tauYears: FairTauProvider;
  readonly nowTs: number;
  /** kestrel-wcnd: the chain's OWN expiry token (`BookState.expiry` — an ISO date or `0dte`), the
   * 2nd argument τ is resolved against. Sourced from the RAW snapshot's chain, NOT from the frame
   * (the frame is date-blind by construction and carries only a relative `dte`), and it never
   * reaches the author — it feeds the pricer, and the chain header still renders the relative tag.
   * Absent / unresolvable ⇒ τ is `null` ⇒ `fair` renders `—` (fail-closed, never a same-day guess). */
  readonly expiry?: string;
}

/** ExecutionFair for ONE near-money chain leg, reusing the SAME pure {@link executionFair} the fill
 * engine prices `@fair` through (`sim.ts` `fairFor`): the chain's own `underlierPx` as the underlier, the
 * WHOLE chain as the liquid-quote surface the vol is backed out of, and tau from the injected
 * `fairTauYears` at the frame cutoff. Returns the intrinsic-floored value + a `b76 nLiq=<n>` receipt
 * note, or **`null`** when unbuildable (no injected tau, no usable underlier, or zero liquid strikes) —
 * the caller then leaves `fair` unset so the chain renders `—` (fail-closed, honest — NEVER a fabricated
 * fair; a silent mid is forbidden, RUNTIME §4). Pure + injected-time: no wall clock, no RNG. */
function chainLegFair(
  chain: NonNullable<AuthorFrame["chain"]>,
  leg: OptionQuote,
  fairCtx: ChainFairContext,
): { readonly value: number; readonly note: string } | null {
  // kestrel-wcnd: τ off the CHAIN's own expiry — the SAME (provider, expiry) pair the fill engine
  // prices `@fair` through, so the served `fair` column can never diverge from the graded anchor.
  const tau = fairCtx.tauYears(fairCtx.nowTs, fairCtx.expiry);
  if (tau === null) return null;
  const r = executionFair({
    underlyingSpot: chain.underlierPx,
    strike: leg.strike,
    right: leg.right,
    tauYears: tau,
    liquidQuotes: chain.legs,
    asof: fairCtx.nowTs,
  });
  return r === null ? null : { value: r.value, note: `b76 nLiq=${r.receipt.nLiquid}` };
}

/** `HHMM` → `HH:MM` (the day runner's tape labels are colon-less; the frame convention carries a colon). */
function colonize(hhmm: string): string {
  return hhmm.length === 4 ? `${hhmm.slice(0, 2)}:${hhmm.slice(2)}` : hhmm;
}

/** The width (minutes) this driver's market pane serves tape rows at — CONSTANT for a session (each
 * `af.tape` sample is one 1-minute single-sample bucket). Named ONCE so `marketPaneOf` (which stamps it
 * onto every frame's `market.tapeBucketMin`) and the schedule-time View validator (`scheduleTimeViewDefect`,
 * which checks a `tape <window>` arg against it via `windowServableBy`) read the SAME width — the two can
 * never drift (kestrel-wa0j.19 §1). */
export const SERVED_TAPE_BUCKET_MIN = 1;

export function marketPaneOf(
  af: AuthorFrame,
  instrument: string,
  options?: OptionsAnalytics,
  // kestrel-121: the fair-pricing context, when the driver can source it. Absent ⇒ the chain `fair`
  // column stays `—` (byte-identical to a pre-fair frame) — every other `marketPaneOf` caller is safe.
  fairCtx?: ChainFairContext,
): MarketPane {
  // kestrel-rs4: carry the spot's AGE onto the levels the author reads. A feed that stopped printing
  // and a market that stopped moving hand the pane the same `spot`; without this the pane can only
  // render the dead one with the confidence of the live one. Absent age ⇒ omitted (byte-identical).
  const levels: LevelSet = {
    spot: af.spot,
    ...(af.spotAgeMs !== undefined ? { spotStaleSeconds: Math.floor(af.spotAgeMs / 1000) } : {}),
    priorClose: af.priorClose,
    hod: af.hod,
    lod: af.lod,
    vwap: af.vwap,
    // kestrel-wa0j.58: carry the frozen opening range onto the levels the author reads. The state computes
    // it, but this seam used to omit it, so `failed-breaks` rendered all-dashes on every one of the 11,809
    // corpus frames (the ORB fake-out level was blinded). `null` (pre-anchor) ⇒ the pane fails closed to `—`.
    orHigh: af.orHigh,
    orLow: af.orLow,
  };
  // Each tape sample is a single-sample bucket (O=H=L=C) — honest (no invented OHLC); a finer bucketer is later.
  const tape: TapeRow[] = af.tape.map((t) => ({ clock: colonize(t.label), open: t.px, high: t.px, low: t.px, close: t.px }));
  const chainSrc = af.chain;
  const chain: ChainRow[] =
    chainSrc === null
      ? []
      : chainSrc.legs.map((l) => {
          // kestrel-121: populate `fair` (+ its `b76` receipt note) from the SAME `executionFair` the fill
          // engine prices `@fair` through; `null`/absent (unbuildable) ⇒ the render shows `—` (fail-closed).
          const f = fairCtx === undefined ? null : chainLegFair(chainSrc, l, fairCtx);
          return { strike: l.strike, right: l.right, bid: l.bid, ask: l.ask, ...(f !== null ? { fair: f.value, fairNote: f.note } : {}) };
        });
  // The options-analytics projection (kestrel-4gl.13.13) is populated ONLY for the perception arm (an
  // overlay was supplied + folded to this cutoff); absent, `market.options` stays undefined and the
  // options panes (off-default) never render — byte-identical to the minimal arm.
  // kestrel-wa0j.19 §4: thread the RELATIVE days-to-expiry the machine channel already carries
  // (`AuthorFrame.chain.dte`) onto the frame, so the chain-pane header regains the `0dte` the
  // one-renderer swap dropped. `null` (unresolvable expiry) / no chain ⇒ omitted — byte-identical.
  const chainDte = chainSrc !== null && chainSrc.dte !== null ? { chainDte: chainSrc.dte } : {};
  return { instrument, levels, tape, tapeBucketMin: SERVED_TAPE_BUCKET_MIN, chain, ...chainDte, ...(options !== undefined ? { options } : {}) };
}

// ─────────────────────────────────────────────────────────────────────────────
// Options-analytics overlay fold (kestrel-4gl.13.13) — the perception arm's live `market.options`
//
// The driver (the impure orchestrator) folds the OPRA overlay tape causally to each frame cutoff and
// hands the pure frame builders the finished, date-blind {@link OptionsAnalytics}. Kept OUT of the
// pure `projectWakeFrame`/`briefingInputOf` seams (they receive the built projection, never the tape),
// so those stay pure functions of their inputs.
// ─────────────────────────────────────────────────────────────────────────────

/** Stream `events` up to and including `tsMax` — the causal T-5m cutoff (no look-ahead). A lazy
 * generator over a lazy source reads only the prefix (the fold stops at the first later event). */
function* untilTs(events: Iterable<BusEvent>, tsMax: number): Generator<BusEvent> {
  for (const e of events) {
    if (e.ts > tsMax) return;
    yield e;
  }
}

/** Relabel a projection's expiry DATE labels (`2024-03-05`) to relative-day tags (`+0d`, `+1d`, from
 * `daysToExpiry`) so NO calendar token reaches the date-blind agent frame (the iso-date fence,
 * `scanDateLeak`). The panes read `tau`/`dte` + front-first ordering, never the label, so the
 * rendered pane text is byte-identical to the raw-label projection. Pure. */
function dateBlindOptions(o: OptionsAnalytics): OptionsAnalytics {
  const expiries = o.expiries.map((e, i) => ({
    ...e,
    expiry: e.daysToExpiry === null ? `exp${i}` : `+${e.daysToExpiry}d`,
  }));
  return { ...o, expiries };
}

/** Fold the options overlay to `cutoffTs` (causal, no look-ahead) and build the date-blind
 * options-analytics projection for one live frame. Returns `undefined` when nothing folds (an empty /
 * pre-open cutoff) — the pane then fails closed on its own. Pure over (overlay events ≤ cutoffTs,
 * config): no wall clock, no RNG, and the MODEL honesty (stale-spot → UNKNOWN) rides in
 * {@link buildOptionsAnalytics}. */
export function buildFrameOptions(cfg: OptionsOverlayConfig, cutoffTs: number): OptionsAnalytics | undefined {
  const surface = reduceOptionSurface(untilTs(cfg.events(), cutoffTs));
  if (surface.cutoffSeq < 0) return undefined; // nothing folded (cutoff before the overlay's first event)
  const raw = buildOptionsAnalytics(surface, {
    expiryCloseTs: cfg.expiryCloseTs,
    openInterest: cfg.openInterest,
    ...(cfg.multiplier !== undefined ? { multiplier: cfg.multiplier } : {}),
    ...(cfg.staleMs !== undefined ? { staleMs: cfg.staleMs } : {}),
  });
  return dateBlindOptions(raw);
}
