/**
 * # adapters/broker/ibkr/feed — the FEED FACE (kestrel-7o2.7)
 *
 * The {@link FeedSource} reference implementation over the ONE shared IB session: it opens
 * `reqMktData` subscriptions on the contracts the 7o2.6 layer resolved (the spot/equity underlier +
 * an option-chain slice) and turns the venue's tick stream into the **EXISTING** {@link BusEvent}
 * union — a `META` header, `TICK/SPOT` (`px`/`bid`/`ask`), `TICK/BOOK` (real `OptionQuote` legs +
 * `underlier_px`), and `TICK/HEARTBEAT` proof-of-life. No new event kind is invented, so
 * `SessionCore.step` folds this feed with zero changes and the deterministic core downstream is
 * untouched.
 *
 * It is MARKET DATA ONLY. There is no order path here — not in the module, and not even reachable
 * through the client: the narrow {@link IbFeedClient} surface declares nothing but `reqMktData` /
 * `cancelMktData` / `reqMarketDataType` and the event bus. Orders are 7o2.8 (paper) / 7o2.10 (live),
 * behind the human-signed arm envelope (7o2.9).
 *
 * ## The price doctrine (ARCHITECTURE §4, RUNTIME §4) — the heart of this face
 *
 * **The observed MID is a HEALTH SIGNAL, never a value and never a price this feed publishes.** An
 * option's book can be fictional: a maker quoting 1.00 × 3.00 on a contract with 5.10 of intrinsic is
 * not offering you 2.00, and a feed that carried that 2.00 forward as a number would have authored a
 * naked sale nobody wrote. So:
 *
 *  - `TICK/SPOT.px` is the **observed last trade** the venue actually printed. When no trade has
 *    printed, the feed emits **no SPOT at all** (fail-closed) — it never synthesizes a spot out of a
 *    quote mid.
 *  - `TICK/BOOK` legs carry the **observed sides verbatim** (`bid`/`ask`/`bsz`/`asz`), with a dark
 *    side as `null` — never `0`, never omitted — so a downstream reducer can tell a genuinely
 *    one-sided book (the market-maker-pull fingerprint of a real move) from a two-sided one.
 *  - `@fair` ({@link feedFair}) is the ENGINE's `executionFair` — underlying-anchored Black-76 backed
 *    out of the LIQUID two-sided quotes and **floored at intrinsic always** — and it carries a
 *    {@link FairReceipt}: the model VERSION, the FIT QUALITY (how many liquid strikes backed the
 *    surface, and the IV it priced at), and the FRESHNESS (`asof`). Belief is the author's; fair is
 *    the engine's, and it says how it was earned.
 *  - A **one-sided or dark book** yields `fair === null` and FAILS CLOSED to an **annotated** book
 *    value — a SELL degrades to its intrinsic floor (`fair=fallback(intrinsic;mid-dark)`, never
 *    naked), a BUY with no two-sided mid is genuinely unresolvable. The annotation ALWAYS names the
 *    fallback, so a silent mid can never slip through (RUNTIME §4).
 *  - The mid survives ONLY inside {@link QuoteHealth}: a spread WIDTH and a dimensionless spread
 *    REGIME (`tight | wide | one-sided | dark`). There is deliberately no `mid` field anywhere on
 *    this module's surface, so a caller cannot read one off it as a price even by accident.
 *
 * ## Staleness — the watermark (the rs4/8kc bug class, first-class here)
 *
 * A **frozen, re-printed quote does NOT advance `asOfSeq`.** IB will happily re-send an identical
 * bid/ask forever while the market underneath it is gone; a feed that treated each re-print as news
 * would keep a dead tape looking alive. So the watermark advances on a **value CHANGE**, never on a
 * print: a re-print increments a `reprints` counter (the frozen fingerprint) and refreshes only
 * `lastPrintTs` (the LINE is talking), while `asOfSeq`/`asOfTs` (the VALUE) stay pinned. Past the
 * staleness threshold the line is STALE and the feed's health goes **DEGRADED** with a logged reason
 * — never a silent one.
 *
 * That degradation has TEETH, and this is the part that matters: canonical state cannot un-know a
 * price it has already folded, so the frozen 445.10 sits in {@link CanonicalState} looking perfectly
 * alive and would happily fire an armed plan. {@link stalenessGatedProvider} wraps the real
 * {@link SeriesProvider} and, while the underlier line is dead, resolves **every market-side series to
 * UNKNOWN** rather than to its last-known value. Only a definite `true` fires (RUNTIME §3), so an
 * UNKNOWN operand taints every dependent trigger and the affected plans **cannot fire off a dead
 * feed**. A dead feed must never read as live.
 *
 * ## Determinism at the edge
 *
 * The IB client, the clock, the request-id minter and the deadline scheduler are all INJECTED (as the
 * transport and the contract layer inject them), so unit tests drive an in-memory double with no
 * socket and no timer. Events carry a monotone `seq` from 0 and the INJECTED `ts` — so the BusEvents
 * this edge produces are causally ordered and replay-stable, which is exactly what the deterministic
 * core downstream requires of them.
 */

// `TickType` is a TYPE-only alias in `@stoqey/ib` (a union over the two tick vocabularies); the
// runtime enum is `IBApiTickType`. Import each in its own register rather than conflating them.
import { EventName, OptionType, SecType, IBApiTickType } from "@stoqey/ib";
import type { Contract, TickType } from "@stoqey/ib";

import { redactAccount } from "./config.ts";
import { IbRequestRouter, IbkrRequestError } from "./requests.ts";
import {
  atmCoverage,
  atmTaint,
  surfaceWindow,
  DEFAULT_ATM_BAND_FRACTION,
  DEFAULT_SURFACE_HALF_WIDTH,
} from "./surface-window.ts";
import type { AtmCoverage, AtmPolicy } from "./surface-window.ts";
import { resolveContract, resolveUnderlier, listOptionStrikes, IBKR_DEFAULT_EXCHANGE } from "./contract.ts";
import type {
  IbkrContractDeps,
  IbkrIndexContract,
  IbkrOptionContract,
  IbkrQuotedContract,
  IbkrUnderlierContract,
  UnderlierSecType,
} from "./contract.ts";
import type { IbkrTransport, NowFn, Scheduler, TimerHandle } from "./transport.ts";
import type {
  BusEvent,
  BookEvent,
  HeartbeatEvent,
  MetaEvent,
  Mode,
  OptionQuote,
  Right,
  SessionInstrument,
  SessionPhase,
  SpotEvent,
} from "../../../bus/index.ts";
import { BUS_SCHEMA } from "../../../bus/index.ts";
import { executionFair, intrinsic, EXEC_FAIR_MODEL } from "../../../fair/index.ts";
import type { FairReceipt } from "../../../fair/index.ts";
import type { FeedSource } from "../../broker.ts";
import {
  UNKNOWN,
  defaultSeriesRegistry,
  isUnknown,
  type Resolved,
  type SeriesProvider,
  type SeriesRegistry,
  type TriState,
  type Unknown,
} from "../../../series/index.ts";
import type { SeriesRef, Baseline, StructuralEvent, FillEvent } from "../../../lang/index.ts";

// ─────────────────────────────────────────────────────────────────────────────
// Defaults
// ─────────────────────────────────────────────────────────────────────────────

/** How long a line's VALUE may go unchanged before it is STALE. A quote that has not MOVED in this
 * long is not evidence of a calm market — it is a line we can no longer vouch for. */
export const DEFAULT_STALE_AFTER_MS = 15_000;
/** How long one `reqMktData` may go completely unanswered before the subscription FAILS. A gateway
 * that never answers is a FAILED request, never an infinite wait (fail-closed). */
export const DEFAULT_SUBSCRIBE_TIMEOUT_MS = 10_000;
/** Spread-to-mid ratio above which a two-sided book is a WIDE one — the dimensionless regime
 * boundary. A RATIO, never a price: the mid appears here only as a denominator. */
export const WIDE_SPREAD_RATIO = 0.15;

// ─────────────────────────────────────────────────────────────────────────────
// The narrow, order-free market-data surface of the shared IB client
// ─────────────────────────────────────────────────────────────────────────────

/** A listener over the IB event bus (the transport's convention). */
type IbListener = (...args: never[]) => void;

/**
 * The NARROW structural surface of the shared IB client the feed speaks through: the streaming
 * market-data requests and the event bus. It declares NO order method of any kind — the feed cannot
 * reach one even through the client. A test injects a double implementing exactly this; production
 * passes {@link IbkrTransport}'s ONE shared client via {@link feedClientOf} — never a second socket,
 * and never a second market-data connection.
 */
export interface IbFeedClient {
  /** Open a streaming market-data subscription for one contract. */
  reqMktData(
    reqId: number,
    contract: Contract,
    genericTickList: string | null,
    snapshot: boolean,
    regulatorySnapshot: boolean,
  ): unknown;
  /** Close a streaming market-data subscription. */
  cancelMktData(reqId: number): unknown;
  /** Choose the market-data flavour (1 real-time, 2 frozen, 3 delayed, 4 delayed-frozen). OPTIONAL:
   * a client that cannot switch simply omits it, and the feed never pretends it did. */
  reqMarketDataType?(marketDataType: number): unknown;
  on(event: EventName, listener: IbListener): unknown;
  removeListener(event: EventName, listener: IbListener): unknown;
}

/**
 * The ONE shared IB session, viewed through the READ-ONLY market-data surface (kestrel-7o2.7). The
 * transport hands out its single guarded client (throwing the typed connection error if the session
 * is not connected or has gone degraded, so a quote can never be read over a dead socket); this
 * re-views it as an {@link IbFeedClient}. The cast NARROWS toward the market-data requests — the real
 * `IBApi` is a structural superset, and this view can place nothing. There is never a second socket.
 */
export function feedClientOf(transport: IbkrTransport): IbFeedClient {
  return transport.client() as unknown as IbFeedClient;
}

// ─────────────────────────────────────────────────────────────────────────────
// Typed failures
// ─────────────────────────────────────────────────────────────────────────────

/** Why the FEED itself failed (kestrel-7o2.7) — distinct from a single refused subscription
 * ({@link IbkrRequestError}) and from a dead socket (`IbkrConnectionError`). A feed with no underlier
 * has no tape at all: there is no `underlier_px` to quote a book against and no spot to drive
 * canonical state, so it fails CLOSED rather than emitting a chain floating in a vacuum. */
export class IbkrFeedError extends Error {
  override readonly name = "IbkrFeedError";
  readonly reason: string;
  constructor(reason: string, options: { cause?: unknown } = {}) {
    super(`IBKR feed refused: ${reason} (fail-closed; STAND_DOWN)`, options.cause === undefined ? undefined : { cause: options.cause });
    this.reason = reason;
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// @fair — the two-sided quote, with a receipt; the mid only ever a health signal
// ─────────────────────────────────────────────────────────────────────────────

/** The spread REGIME of an observed book — the mid's ONLY legitimate role (a fingerprint, never a
 * price). `dark` = both sides gone; `one-sided` = a maker pulled one side (the real-move fingerprint);
 * `wide` = two-sided but the spread is a large fraction of the book (a book you cannot trust to a
 * cent); `tight` = a book worth reading. */
export type SpreadRegime = "tight" | "wide" | "one-sided" | "dark";

/**
 * The health of one observed book (kestrel-7o2.7). This is where — and ONLY where — the observed mid
 * is allowed to matter, and even here it never escapes as a number: it appears solely as the
 * denominator of a dimensionless {@link SpreadRegime}. There is deliberately NO `mid` field: a caller
 * cannot lift a mid off this surface and price against it, because the surface does not carry one.
 */
export interface QuoteHealth {
  /** Both sides present ⇒ a real, two-sided book. */
  readonly twoSided: boolean;
  /** Both sides gone ⇒ dark. */
  readonly dark: boolean;
  /** The spread WIDTH in dollars (`ask − bid`) — an observed distance, not a price. `null` unless
   * the book is two-sided. */
  readonly spread: number | null;
  /** The dimensionless spread regime — the fingerprint the mid is reduced to. */
  readonly spreadRegime: SpreadRegime;
}

/**
 * The fail-closed ANNOTATED book value a caller falls back to when `@fair` is unbuildable (RUNTIME
 * §4). The `annotation` ALWAYS names the fallback — that is the whole point: **a silent mid is
 * forbidden**, so a number that did not come from the model arrives wearing a label that says so.
 * `px` is `null` when there is no honest number at all (a BUY into a dark book) — an annotated
 * refusal, never a guess.
 */
export interface FairFallback {
  readonly px: number | null;
  readonly annotation: string;
}

/**
 * `@fair` for one option leg (kestrel-7o2.7) — the engine's value, with its receipt, plus the honest
 * fail-closed alternative when it cannot be built. Exactly one of `value` / `fallback` is present.
 * There is no `mid` on this object, and there never will be.
 */
export interface FeedFair {
  /** The ExecutionFair value — Black-76 at the interpolated IV, FLOORED AT INTRINSIC. `null` when
   * unbuildable (no usable underlier, or a book with zero liquid strikes to back a vol out of). */
  readonly value: number | null;
  /** The trust envelope: model VERSION + FIT QUALITY (`nLiquid`, `ivAtStrike`) + FRESHNESS (`asof`).
   * `null` exactly when `value` is — a receipt is never fabricated for a number that does not exist. */
  readonly receipt: FairReceipt | null;
  /** The annotated book value when `value` is `null`; `null` when fair resolved. */
  readonly fallback: FairFallback | null;
  /** The observed book's health — the mid's only role. */
  readonly health: QuoteHealth;
  /**
   * THE FIT QUALITY THE RECEIPT COULD NOT EXPRESS (kestrel-7o2.19): does the surface this value came
   * off actually KNOW WHERE THE MONEY IS? `nLiquid` is a COUNT, not a LOCATION — five deep-ITM strikes
   * are five liquid strikes and zero evidence about an at-the-money price. Always present, whether or
   * not a value resolved.
   */
  readonly coverage: AtmCoverage;
  /** Non-`null` exactly when a value is returned off a surface that does NOT cover the money (the
   * `taint` {@link AtmPolicy}) — the number then travels WEARING the reason it cannot be trusted.
   * `null` when the surface covers the money, and `null` when the feed failed closed instead (there is
   * then no number to taint, and the fallback's own annotation carries the reason). */
  readonly taint: string | null;
}

/** The inputs to one `@fair` resolution off a recorded/observed book. Pure: no clock, no socket. */
export interface FeedFairInput {
  /** The OBSERVED underlier price (a real print). `null` ⇒ no usable underlying ⇒ fair is unbuildable
   * (never a spot synthesized out of a quote). */
  readonly underlierPx: number | null;
  readonly strike: number;
  readonly right: Right;
  /** The side decides the fallback SHAPE: a SELL degrades to its intrinsic floor (never naked); a BUY
   * with no two-sided mid is genuinely unresolvable. */
  readonly side: "buy" | "sell";
  /** Time to expiry in YEARS — injected, never read off a clock (RUNTIME §0). */
  readonly tauYears: number;
  /** The chain slice the vol surface is backed out of. One-sided/dark legs contribute nothing (their
   * mid is a health signal, never a price) — that is `buildSurface`'s own rule, reused verbatim. */
  readonly legs: readonly OptionQuote[];
  /** The observation time of the quotes being valued (epoch ms) — stamped onto the receipt. */
  readonly asof: number;
  /** What "NEAR the money" means, as a fraction of spot (default {@link DEFAULT_ATM_BAND_FRACTION}) —
   * a named, injectable parameter, never a magic number at a call site. */
  readonly bandFraction?: number | undefined;
  /** What to do when the surface does NOT know the money (default `fail-closed`). */
  readonly atmPolicy?: AtmPolicy | undefined;
}

/**
 * Resolve `@fair` for one option leg off an observed two-sided book (kestrel-7o2.7).
 *
 * Delegates the VALUE to the engine's own {@link executionFair} — this module does not own a second
 * pricing model, and it does not re-price. It owns exactly the fail-closed half the price doctrine
 * demands: when fair is unbuildable, produce the **annotated** book value (mirroring
 * `engine/pricing.ts` `resolveFair` field-for-field, including its annotation strings — those strings
 * ARE the audit trail RUNTIME §4 requires), and reduce the observed mid to a health signal so it can
 * never be mistaken for a price.
 *
 * **AND IT REFUSES TO VOUCH FOR A SURFACE THAT DOES NOT KNOW THE MONEY (kestrel-7o2.19).** `fair` is
 * receipt-gated — it CARRIES WHETHER IT CAN BE TRUSTED (ARCHITECTURE §4) — and a receipt reporting
 * `nLiquid=5` off five deep-ITM strikes while the money sits eleven points higher is a receipt that
 * does not know it is lying. So {@link atmCoverage} is measured on EVERY resolution, off the same legs
 * and the same liquidity rule the surface itself uses; a surface with no liquid strike NEAR the money
 * (or none BRACKETING it, which makes an at-the-money read a flat extrapolation of a wing's IV) either
 * FAILS CLOSED to the annotated fallback — the default — or returns its value TAINTED. Never a
 * confident number off a surface that does not know the money.
 */
export function feedFair(input: FeedFairInput): FeedFair {
  const { underlierPx, strike, right, side, tauYears, legs, asof } = input;
  const policy: AtmPolicy = input.atmPolicy ?? "fail-closed";
  const target = legs.find((l) => l.strike === strike && l.right === right);
  const health = quoteHealthOf(target);

  // The one usable-underlier test, computed once (and narrowing, so no cast is needed below): a spot
  // is a real, finite, positive print, or it is not a spot at all.
  const px =
    underlierPx !== null && Number.isFinite(underlierPx) && underlierPx > 0 ? underlierPx : null;

  // The HONEST fit quality, measured off the SAME legs the model is handed, with the SAME liquidity
  // rule the surface is built by — always, so a caller can read WHY even when a value did resolve.
  const coverage = atmCoverage({
    legs,
    spot: px,
    tauYears,
    ...(input.bandFraction === undefined ? {} : { bandFraction: input.bandFraction }),
  });

  const built =
    px === null
      ? null
      : executionFair({ underlyingSpot: px, strike, right, tauYears, liquidQuotes: legs, asof });

  if (built !== null && coverage.supportsAtm) {
    // The model spoke, floored at intrinsic, with a receipt — off a surface that KNOWS THE MONEY. Note
    // what this value is NOT: it is not the target leg's mid — a wide/crushed book's mid is exactly the
    // number the floor exists to refuse (a maker will not sell you 5.10 of intrinsic for a 2.00 mid).
    return { value: built.value, receipt: built.receipt, fallback: null, health, coverage, taint: null };
  }

  if (built !== null && policy === "taint") {
    // The number survives, but it travels WEARING the reason it cannot be trusted. A caller that reads
    // `value` and ignores `taint` has been told, in words, that it is reading an unvouched-for number.
    return { value: built.value, receipt: built.receipt, fallback: null, health, coverage, taint: atmTaint(coverage) };
  }

  // FAIL CLOSED to an ANNOTATED book value. The annotation always names the fallback — and, when the
  // model DID speak and we refused it anyway, it names THAT too (`atm-uncovered`): the value was not
  // MISSING, it was UNTRUSTWORTHY, and those are different failures with different remedies.
  const uncovered = built !== null; // reaching here WITH a model value ⇒ the refusal is the coverage one
  const because = uncovered ? ` — ${atmTaint(coverage)}` : "";
  const tag = (base: string): string =>
    uncovered ? `fair=fallback(${base};atm-uncovered)${because}` : `fair=fallback(${base})`;

  const mid = midOf(target); // used ONLY to decide the fallback SHAPE — see the annotations below
  const floor = px === null ? null : intrinsic(px, strike, right);

  if (side === "buy") {
    if (mid === null) {
      return {
        value: null,
        receipt: null,
        fallback: {
          px: null,
          annotation:
            "fair unbuildable: the book is one-sided/dark and a BUY has no two-sided mid to fall back to — price off @bid/@ask/@last (fail-closed, RUNTIME §4)" +
            (uncovered ? ` [atm-uncovered]${because}` : ""),
        },
        health,
        coverage,
        taint: null,
      };
    }
    return {
      value: null,
      receipt: null,
      fallback: { px: mid, annotation: tag("mid") },
      health,
      coverage,
      taint: null,
    };
  }

  // SELL — NEVER NAKED. Floored at intrinsic, whatever the book says.
  if (floor === null) {
    return {
      value: null,
      receipt: null,
      fallback: {
        px: null,
        annotation:
          "fair unbuildable: no usable underlier, so even the intrinsic floor is unknowable — a SELL is refused rather than sent naked (fail-closed, RUNTIME §4)",
      },
      health,
      coverage,
      taint: null,
    };
  }
  if (mid === null) {
    return {
      value: null,
      receipt: null,
      fallback: { px: floor, annotation: tag("intrinsic;mid-dark") },
      health,
      coverage,
      taint: null,
    };
  }
  return {
    value: null,
    receipt: null,
    fallback: { px: Math.max(mid, floor), annotation: tag("max(mid,intrinsic)") },
    health,
    coverage,
    taint: null,
  };
}

/** The observed book's health. The mid enters ONLY as the denominator of a dimensionless ratio and
 * never leaves this function as a number. */
function quoteHealthOf(q: OptionQuote | undefined): QuoteHealth {
  const bid = q?.bid ?? null;
  const ask = q?.ask ?? null;
  const dark = bid === null && ask === null;
  const twoSided = bid !== null && ask !== null;
  if (!twoSided) {
    return { twoSided: false, dark, spread: null, spreadRegime: dark ? "dark" : "one-sided" };
  }
  const spread = ask - bid;
  const denom = 0.5 * (bid + ask);
  const wide = denom <= 0 || spread / denom > WIDE_SPREAD_RATIO;
  return { twoSided: true, dark: false, spread, spreadRegime: wide ? "wide" : "tight" };
}

/** The two-sided mid — the ONE internal use the doctrine allows: deciding the SHAPE of an annotated
 * fallback (RUNTIME §4 defines the fallback in terms of it). It never becomes a `@fair` value, never
 * reaches a BusEvent, and never appears on this module's public surface. */
function midOf(q: OptionQuote | undefined): number | null {
  if (q === undefined || q.bid === null || q.ask === null) return null;
  if (!Number.isFinite(q.bid) || !Number.isFinite(q.ask) || q.ask < q.bid) return null;
  return 0.5 * (q.bid + q.ask);
}

// ─────────────────────────────────────────────────────────────────────────────
// The watermark
// ─────────────────────────────────────────────────────────────────────────────

/** The feed's overall verdict on itself. `DEGRADED` is never silent — it always carries a reason. */
export type FeedHealth = "LIVE" | "DEGRADED";

/**
 * The staleness watermark of ONE subscribed line (kestrel-7o2.7). The distinction the whole bead
 * turns on lives here: `lastPrintTs` is when the LINE last spoke (IB re-sent something, anything),
 * while `asOfSeq`/`asOfTs` are when the VALUE last CHANGED. A frozen line re-printing the same
 * bid/ask forever keeps `lastPrintTs` moving and `asOfSeq` PINNED — which is exactly how a dead tape
 * is caught rendering as a live one.
 */
export interface FeedSeriesWatermark {
  /** `SPY` for the underlier, `SPY|445C` for an option leg. */
  readonly key: string;
  readonly kind: "spot" | "option";
  /** The emitted `seq` at which this line's VALUE last CHANGED. A frozen re-print does NOT advance
   * it. `null` before the line has ever carried a value. */
  readonly asOfSeq: number | null;
  /** The injected-clock `ts` at which this line's VALUE last CHANGED. */
  readonly asOfTs: number | null;
  /** The injected-clock `ts` at which IB last PRINTED anything on this line, changed or not — proof
   * the line is talking, which is NOT proof the market is moving. */
  readonly lastPrintTs: number | null;
  /** Identical re-prints since the last value CHANGE — the frozen-quote fingerprint. */
  readonly reprints: number;
  /** `now − asOfTs` — how old the VALUE is. `null` before the first value. */
  readonly ageMs: number | null;
  /** `ageMs > staleAfterMs`, or no value has ever landed ⇒ STALE (fail-closed on absence). */
  readonly stale: boolean;
  /** Both sides gone. */
  readonly dark: boolean;
  /** A refused subscription (a request-scoped IB error / a deadline lapse) — the line is dead by
   * refusal rather than by silence. */
  readonly failed: boolean;
}

/** The feed's staleness watermark (kestrel-7o2.7) — the canonical freshness truth a consumer gates
 * on. Read it, do not infer it: canonical state cannot tell you a price is stale, only what it last
 * saw. */
export interface FeedWatermark {
  /** The injected clock at the moment the watermark was read. */
  readonly now: number;
  /** The last `seq` the feed emitted. */
  readonly seq: number;
  readonly staleAfterMs: number;
  readonly health: FeedHealth;
  /** The logged reason for DEGRADED, or `null` while LIVE. Never a silent degrade. */
  readonly reason: string | null;
  readonly series: readonly FeedSeriesWatermark[];
  /** Is the UNDERLIER line stale/dead? It is the canonical coordinate every market series hangs off,
   * so its death taints all of them. */
  readonly underlierStale: boolean;
}

// ─────────────────────────────────────────────────────────────────────────────
// Config / deps / the FeedSource surface
// ─────────────────────────────────────────────────────────────────────────────

export interface IbkrFeedConfig {
  /** The underlier symbol (generic tickers only in anything shipped — ARCHITECTURE §7). */
  readonly instrument: string;
  /** The session header's opaque calendar token (`YYYY-MM-DD`). */
  readonly sessionDate: string;
  /** The Kestrel mode this tape is stamped with. */
  readonly mode: Mode;
  /** The chain's expiry, carried onto every `TICK/BOOK` (the venue's own `YYYYMMDD`, or a tag). */
  readonly expiry?: string | undefined;
  /** Time to expiry in YEARS for `@fair` — INJECTED, never derived from a clock (RUNTIME §0). */
  readonly tauYears?: number | undefined;
  /** How long a line's VALUE may go unchanged before it is STALE (default {@link DEFAULT_STALE_AFTER_MS}). */
  readonly staleAfterMs?: number | undefined;
  /** How long one subscription may go unanswered before it FAILS (default {@link DEFAULT_SUBSCRIBE_TIMEOUT_MS}). */
  readonly subscribeTimeoutMs?: number | undefined;
  /** What "NEAR the money" means for the fit-quality receipt, as a fraction of spot (default
   * {@link DEFAULT_ATM_BAND_FRACTION}) — a named, injectable parameter (kestrel-7o2.19). */
  readonly atmBandFraction?: number | undefined;
  /** What `@fair` does when the surface does NOT know the money: FAIL CLOSED (`fair => null` + the
   * annotated fallback — the default) or return the value TAINTED. Never a silent confident number. */
  readonly atmPolicy?: AtmPolicy | undefined;
  /** The session phase stamped on the proof-of-life heartbeats, when the caller pins one. */
  readonly phase?: SessionPhase | undefined;
  /** IB market-data flavour (1 real-time, 3 delayed). Absent ⇒ the gateway's own default is left
   * alone — the feed never silently downgrades a caller to delayed data. */
  readonly marketDataType?: number | undefined;
  /** The session account — used ONLY to stamp a REDACTED marker on diagnostics. */
  readonly account?: string | undefined;
}

export interface IbkrFeedDeps {
  /** The shared IB client — the transport's ONE session in production ({@link feedClientOf}). */
  readonly client: IbFeedClient;
  /** The resolved UNDERLIER contract (kestrel-7o2.6) — an equity/ETF (`STK`) or an INDEX (`IND`,
   * the underlier of a cash-settled index option, kestrel-7o2.24). The gateway's OWN definition,
   * never hand-rolled: the feed does not resolve identities, it subscribes to resolved ones. Typed as
   * the UNDERLIER union, not the TRADABLE one — a feed quotes an index, it never orders one. */
  readonly underlier: IbkrUnderlierContract;
  /** The resolved OPTION legs (kestrel-7o2.6). Empty/absent ⇒ an equity-only tape: no `TICK/BOOK` is
   * ever emitted, which is a first-class shape on this bus. */
  readonly legs?: readonly IbkrOptionContract[] | undefined;
  /** The injected clock (epoch ms). */
  readonly now: NowFn;
  /** Mints a unique IB request id per subscription — a counter, never an RNG. */
  readonly nextReqId?: (() => number) | undefined;
  /** The deadline scheduler — injected so tests trip it by hand (no real timer, no flake). */
  readonly scheduler?: Scheduler | undefined;
  /** Redacted-diagnostic sink (default no-op). NOTE: failures never depend on this — that was the
   * 7o2.16 gap. A refused subscription reaches the CALLER through {@link FeedSubscription.ready} and
   * {@link FeedOpenResult.failed}, whether or not anyone is listening to the log. */
  readonly log?: ((line: string) => void) | undefined;
}

/** The lifecycle of one `reqMktData` subscription. */
export type SubscriptionState = "pending" | "live" | "failed";

/**
 * One market-data subscription on the shared socket (kestrel-7o2.7). {@link ready} is the
 * caller-visible channel the 7o2.16 follow-up demanded: it RESOLVES on the line's first print and
 * REJECTS with a typed {@link IbkrRequestError} when the gateway refuses the request (IB 200 / 321 /
 * 354 / 10197 …) or when the deadline lapses. It never simply hangs.
 */
export interface FeedSubscription {
  readonly reqId: number;
  /** `SPY` for the underlier, `SPY|445C` for an option leg. */
  readonly key: string;
  readonly kind: "spot" | "option";
  readonly contract: IbkrQuotedContract;
  readonly state: SubscriptionState;
  /** The typed refusal, once this subscription has failed. `null` otherwise. */
  readonly error: IbkrRequestError | null;
  /** Resolves on the first print; REJECTS on a request-scoped refusal or a deadline lapse. */
  readonly ready: Promise<void>;
}

/** What {@link IbkrFeed.open} reports. Failures are SURFACED, never swallowed — but a refused LEG is
 * not a refused feed: the leg is dropped (it will never be quoted as a phantom) and the tape carries
 * on, while a refused UNDERLIER throws {@link IbkrFeedError} because there is then no tape at all. */
export interface FeedOpenResult {
  readonly underlier: IbkrUnderlierContract;
  /** The legs that actually subscribed. */
  readonly legs: readonly IbkrOptionContract[];
  /** Every per-subscription refusal, surfaced to the caller. */
  readonly failed: readonly IbkrRequestError[];
}

/** What a `@fair` is asked for. */
export interface FeedFairQuery {
  readonly strike: number;
  readonly right: Right;
  readonly side: "buy" | "sell";
}

/**
 * The IBKR feed face (kestrel-7o2.7) — a {@link FeedSource} producing the EXISTING {@link BusEvent}
 * union off the ONE shared IB session. Market data only; it exposes no order path.
 */
export interface IbkrFeed extends FeedSource {
  /** Emit `META`, open every subscription, and settle. Never hangs: each subscription carries its own
   * deadline, and a refusal surfaces as a typed error rather than silence. */
  open(): Promise<FeedOpenResult>;
  /** Fold whatever IB has said since the last pump into BusEvents. A value CHANGE produces a
   * `TICK/SPOT` / `TICK/BOOK`; a quiet interval produces a `TICK/HEARTBEAT` (proof of life on a quiet
   * tape) — and a frozen re-print produces the heartbeat too, because a re-print is not news. */
  pump(): void;
  /** The bus events produced so far, in order — the {@link FeedSource} face. Re-iterable and
   * byte-stable (`META` first, then causally-ordered ticks). */
  events(): Iterable<BusEvent>;
  /** The events produced since the previous drain (the live loop's incremental read). */
  drain(): readonly BusEvent[];
  /** The staleness watermark — read it rather than inferring freshness from canonical state. */
  watermark(): FeedWatermark;
  /** `@fair` for one leg off the CURRENT observed book, with its receipt (or the annotated
   * fail-closed fallback). */
  fair(query: FeedFairQuery): FeedFair;
  /** Every subscription, live and failed. */
  subscriptions(): readonly FeedSubscription[];
  /** Close every subscription and detach every listener from the shared socket. Idempotent. */
  close(): void;
}

// ─────────────────────────────────────────────────────────────────────────────
// Internals — one subscribed line
// ─────────────────────────────────────────────────────────────────────────────

/** The raw IB ticks for one line, as they land (price and size arrive on separate events, in either
 * order — so the QUOTE is derived on read, never assembled half-formed on write). */
interface RawTicks {
  bid?: number;
  ask?: number;
  bsz?: number;
  asz?: number;
  last?: number;
}

/** The derived quote of one line — the shape that actually reaches the bus. */
interface LineQuote {
  readonly bid: number | null;
  readonly ask: number | null;
  readonly bsz: number | null;
  readonly asz: number | null;
  readonly last: number | undefined;
}

class Line {
  readonly raw: RawTicks = {};
  /** The last DERIVED quote. `null` until the line's first print — which is why a first print that is
   * itself DARK still counts as news (the darkness is the news). */
  quote: LineQuote | null = null;
  asOfSeq: number | null = null;
  asOfTs: number | null = null;
  lastPrintTs: number | null = null;
  reprints = 0;
  dirty = false;
  state: SubscriptionState = "pending";
  error: IbkrRequestError | null = null;
  deadline: TimerHandle | undefined;
  settle!: () => void;
  refuse!: (e: IbkrRequestError) => void;
  readonly ready: Promise<void>;

  constructor(
    readonly reqId: number,
    readonly key: string,
    readonly kind: "spot" | "option",
    readonly contract: IbkrQuotedContract,
  ) {
    this.ready = new Promise<void>((resolve, reject) => {
      this.settle = resolve;
      this.refuse = reject;
    });
    // A refusal is delivered to whoever awaits `ready`; nobody is REQUIRED to await it (the feed's
    // own `open()` collects it either way), so pre-empt Node's unhandled-rejection noise.
    this.ready.catch(() => {});
  }
}

/** Derive the honest quote from the raw ticks. IB's OWN no-data convention is honoured verbatim: a
 * price of −1 (or a 0 with a 0 size) means "there is no data for this field", which is a DARK side —
 * `null`, never `0`. Confusing IB's "no data" sentinel for a real 0.00 bid would fabricate a book. */
function deriveQuote(raw: RawTicks): LineQuote {
  const side = (px: number | undefined, sz: number | undefined): number | null => {
    if (px === undefined || !Number.isFinite(px)) return null;
    if (px < 0) return null; // IB's explicit no-data sentinel
    if (px === 0 && (sz === undefined || sz <= 0)) return null; // "0 price with 0 size" ⇒ no data
    return px;
  };
  const bid = side(raw.bid, raw.bsz);
  const ask = side(raw.ask, raw.asz);
  const sizeOf = (v: number | undefined, present: number | null): number | null =>
    present === null || v === undefined || !Number.isFinite(v) || v < 0 ? null : v;
  const last = raw.last !== undefined && Number.isFinite(raw.last) && raw.last > 0 ? raw.last : undefined;
  return { bid, ask, bsz: sizeOf(raw.bsz, bid), asz: sizeOf(raw.asz, ask), last };
}

function sameQuote(a: LineQuote, b: LineQuote): boolean {
  return a.bid === b.bid && a.ask === b.ask && a.bsz === b.bsz && a.asz === b.asz && a.last === b.last;
}

/** IB delivers a delayed line on a PARALLEL set of tick types (66–71). They mean exactly the same
 * things — a paper account without a real-time subscription sees only these — so they are folded onto
 * the same fields. The feed never pretends delayed data is real-time; the FRESHNESS it vouches for is
 * the `asof` on the receipt and the watermark, which are honest either way. */
function fieldOf(t: TickType): keyof RawTicks | null {
  switch (t) {
    case IBApiTickType.BID:
    case IBApiTickType.DELAYED_BID:
      return "bid";
    case IBApiTickType.ASK:
    case IBApiTickType.DELAYED_ASK:
      return "ask";
    case IBApiTickType.BID_SIZE:
    case IBApiTickType.DELAYED_BID_SIZE:
      return "bsz";
    case IBApiTickType.ASK_SIZE:
    case IBApiTickType.DELAYED_ASK_SIZE:
      return "asz";
    case IBApiTickType.LAST:
    case IBApiTickType.DELAYED_LAST:
      return "last";
    default:
      return null; // every other tick type (volume, greeks, 13-week highs …) is not a book fact
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// The feed
// ─────────────────────────────────────────────────────────────────────────────

class IbkrFeedImpl implements IbkrFeed {
  readonly #cfg: IbkrFeedConfig;
  readonly #client: IbFeedClient;
  readonly #now: NowFn;
  readonly #nextReqId: () => number;
  readonly #scheduler: Scheduler;
  readonly #log: (line: string) => void;
  readonly #router: IbRequestRouter;
  readonly #staleAfterMs: number;
  readonly #timeoutMs: number;
  readonly #tauYears: number;

  readonly #underlier: IbkrUnderlierContract;
  readonly #legContracts: readonly IbkrOptionContract[];
  /** reqId → line. The single dispatch table for every tick and every refusal. */
  readonly #lines = new Map<number, Line>();
  /** The underlier's line — the canonical coordinate everything else hangs off. */
  #spotLine!: Line;

  readonly #emitted: BusEvent[] = [];
  #drained = 0;
  #seq = 0;
  #opened = false;
  #closed = false;
  /** DEGRADED is latched with its FIRST reason (the root cause), and logged exactly once on the
   * rising edge — never a per-pump storm, and never silent. */
  #degradedReason: string | null = null;
  readonly #registered: Array<[EventName, IbListener]> = [];

  constructor(cfg: IbkrFeedConfig, deps: IbkrFeedDeps) {
    this.#cfg = cfg;
    this.#client = deps.client;
    this.#now = deps.now;
    this.#nextReqId = deps.nextReqId ?? defaultNextReqId;
    this.#scheduler = deps.scheduler ?? defaultScheduler;
    this.#log = deps.log ?? (() => {});
    this.#staleAfterMs = cfg.staleAfterMs ?? DEFAULT_STALE_AFTER_MS;
    this.#timeoutMs = cfg.subscribeTimeoutMs ?? DEFAULT_SUBSCRIBE_TIMEOUT_MS;
    this.#tauYears = cfg.tauYears ?? 0;
    this.#underlier = deps.underlier;
    this.#legContracts = deps.legs ?? [];
    this.#router = new IbRequestRouter(deps.client, { log: deps.log });
  }

  // ── open ───────────────────────────────────────────────────────────────────

  async open(): Promise<FeedOpenResult> {
    if (this.#opened) throw new IbkrFeedError("the feed is already open — a second open on one feed would double-subscribe the shared socket");
    this.#opened = true;

    // META FIRST — always. A tape whose header does not lead it is not a bus.
    this.emitMeta();

    // The market-data flavour, when the caller pinned one. Never chosen for them: a feed that
    // silently downgraded a caller to DELAYED data would be a feed lying about its own freshness.
    if (this.#cfg.marketDataType !== undefined) {
      this.#client.reqMarketDataType?.(this.#cfg.marketDataType);
    }

    this.#listen(EventName.tickPrice, (reqId: number, field: TickType, value: number) => {
      this.onTick(reqId, field, value);
    });
    this.#listen(EventName.tickSize, (reqId: number, field?: TickType, value?: number) => {
      if (field === undefined || value === undefined) return;
      this.onTick(reqId, field, value);
    });

    // Every subscription is issued SYNCHRONOUSLY here (deadline armed with it), so a caller that
    // trips the injected scheduler by hand — or a gateway that answers instantly — is never racing us.
    this.#spotLine = this.subscribeOne(this.#underlier, this.#underlier.symbol, "spot");
    for (const leg of this.#legContracts) {
      this.subscribeOne(leg, legKey(this.#underlier.symbol, leg.strike, leg.right), "option");
    }

    const lines = [...this.#lines.values()];
    await Promise.all(lines.map((l) => l.ready.then(() => undefined, () => undefined)));

    // FAIL CLOSED on the underlier: with no spot line there is no `underlier_px` to quote a book
    // against and no price to drive canonical state — a chain floating in a vacuum is not a tape.
    if (this.#spotLine.state === "failed") {
      const cause = this.#spotLine.error;
      this.close();
      throw new IbkrFeedError(
        `the UNDERLIER subscription (${this.#underlier.symbol}) was refused — no underlier means no tape at all${
          cause === null ? "" : `: ${cause.message}`
        } [account=${redactAccount(this.#cfg.account)}]`,
        { cause: cause ?? undefined },
      );
    }

    const failed = lines.filter((l) => l.state === "failed" && l.error !== null).map((l) => l.error as IbkrRequestError);
    for (const err of failed) {
      // A refused LEG is dropped, loudly. It is never quoted as a phantom — a leg the venue refused
      // to quote has no book, and an empty book is not a zero book.
      this.#log(`feed: dropping refused leg [reqId=${err.reqId}] ${err.label} — ${err.message}`);
    }
    const live = this.#legContracts.filter((c) => this.lineOf(c)?.state === "live");
    return { underlier: this.#underlier, legs: live, failed };
  }

  private subscribeOne(contract: IbkrQuotedContract, key: string, kind: "spot" | "option"): Line {
    const reqId = this.#nextReqId();
    const line = new Line(reqId, key, kind, contract);
    this.#lines.set(reqId, line);
    const label = `market data for ${key}`;

    // THE CHANNEL (the kestrel-7o2.16 follow-up): a request-scoped IB error on THIS reqId lands on
    // THIS subscription — not on a log sink that defaults to a no-op, and never on the shared session.
    this.#router.open(reqId, label, (err) => {
      this.failLine(line, err);
    });

    // The deadline: a gateway that never answers is a FAILED subscription, not an infinite wait.
    line.deadline = this.#scheduler.setInterval(() => {
      this.failLine(
        line,
        new IbkrRequestError(reqId, label, `the subscription timed out after ${this.#timeoutMs}ms — the gateway sent no tick at all, and a silent subscription is a FAILED one, never an infinite wait`),
      );
    }, this.#timeoutMs);

    try {
      this.#client.reqMktData(reqId, ibContractOf(contract), "", false, false);
    } catch (cause) {
      this.failLine(line, new IbkrRequestError(reqId, label, "the market-data request threw at the socket", { cause }));
    }
    return line;
  }

  private failLine(line: Line, err: IbkrRequestError): void {
    if (line.state === "failed") return; // first refusal wins (a latch — never a cascade)
    line.state = "failed";
    line.error = err;
    this.clearDeadline(line);
    this.#router.close(line.reqId);
    line.refuse(err);
  }

  private liveLine(line: Line): void {
    if (line.state !== "pending") return;
    line.state = "live";
    this.clearDeadline(line);
    line.settle();
  }

  private clearDeadline(line: Line): void {
    if (line.deadline !== undefined) {
      this.#scheduler.clearInterval(line.deadline);
      line.deadline = undefined;
    }
  }

  // ── ticks ──────────────────────────────────────────────────────────────────

  /**
   * One IB tick. THE STALENESS RULE LIVES HERE: a print refreshes `lastPrintTs` (the LINE spoke) but
   * only a CHANGED value marks the line dirty (the VALUE moved). An identical re-print increments
   * `reprints` and moves NOTHING else — which is why a frozen line's `asOfSeq` cannot advance, and
   * why a dead tape cannot pass itself off as a live one.
   */
  private onTick(reqId: number, field: TickType, value: number): void {
    const line = this.#lines.get(reqId);
    if (line === undefined) return; // another face's request on the shared socket — not ours
    if (line.state === "failed") return; // a refused line is dead; a late tick does not revive it
    const key = fieldOf(field);
    if (key === null) return; // not a book fact

    const now = this.#now();
    line.raw[key] = value;
    line.lastPrintTs = now; // the LINE spoke — which is not the same as the market having moved
    this.liveLine(line);

    const next = deriveQuote(line.raw);
    if (line.quote !== null && sameQuote(next, line.quote)) {
      line.reprints++; // the frozen fingerprint — recorded, never mistaken for news
      return;
    }
    // The VALUE moved. Its as-of is the moment the venue DELIVERED it — not the moment we happened
    // to fold it onto the bus. (`asOfSeq` is stamped at emission, below: the two answer different
    // questions — "how old is this observation" vs "which emitted event last carried it".)
    line.quote = next;
    line.asOfTs = now;
    line.reprints = 0;
    line.dirty = true;
  }

  // ── pump ───────────────────────────────────────────────────────────────────

  pump(): void {
    if (this.#closed) return;
    const now = this.#now();
    let emitted = false;

    // SPOT first — a BOOK is quoted AGAINST an underlier context, so that context must already be on
    // the bus when the book lands (causal order, not merely tidy order).
    if (this.#spotLine.dirty) {
      const q = this.#spotLine.quote;
      const px = q?.last;
      if (px === undefined) {
        // NO OBSERVED TRADE ⇒ NO SPOT. The feed will not synthesize a price out of a quote mid; a
        // spot that nobody traded at is a spot nobody authored. Fail-closed, with a reason.
        this.#log(
          `feed: ${this.#spotLine.key} has a quote but NO observed last trade — no SPOT emitted (a mid is never a price; fail-closed)`,
        );
      } else {
        this.emit<SpotEvent>({
          ts: now,
          stream: "TICK",
          type: "SPOT",
          instrument: this.#cfg.instrument,
          px, // the OBSERVED last trade — never a synthesized mid
          bid: q?.bid ?? null,
          ask: q?.ask ?? null,
        });
        this.#spotLine.asOfSeq = this.#seq - 1;
        this.#spotLine.dirty = false;
        emitted = true;
      }
    }

    // BOOK — emitted when ANY live leg's value changed. It carries the whole current slice (the bus's
    // own shape: a BOOK is a snapshot of the chain at one moment), but ONLY the legs that actually
    // moved advance their watermark. A clean leg riding along in a snapshot is not a fresh leg.
    const legLines = this.liveLegs();
    const movedLegs = legLines.filter((l) => l.dirty);
    if (movedLegs.length > 0) {
      const underlierPx = this.#spotLine.quote?.last;
      if (underlierPx === undefined) {
        // A book with no underlier context cannot be honestly written (`underlier_px` is not optional
        // on this bus, and inventing one would invent the context every downstream read depends on).
        this.#log(
          `feed: ${movedLegs.length} leg(s) moved but the underlier has NO observed price — no BOOK emitted (underlier_px is never invented; fail-closed)`,
        );
      } else {
        this.emit<BookEvent>({
          ts: now,
          stream: "TICK",
          type: "BOOK",
          instrument: this.#cfg.instrument,
          underlier_px: underlierPx,
          ...(this.#cfg.expiry === undefined ? {} : { expiry: this.#cfg.expiry }),
          legs: legLines.map((l) => optionQuoteOf(l)),
        });
        for (const l of movedLegs) {
          l.asOfSeq = this.#seq - 1;
          l.dirty = false;
        }
        emitted = true;
      }
    }

    // A quiet interval — including one full of FROZEN RE-PRINTS — is proof of life, not news. The
    // heartbeat advances `seq`/`ts` and NO line's `asOfSeq`, which is precisely what lets the
    // watermark age a frozen line out while the tape keeps ticking.
    if (!emitted) {
      this.emit<HeartbeatEvent>({
        ts: now,
        stream: "TICK",
        type: "HEARTBEAT",
        ...(this.#cfg.phase === undefined ? {} : { phase: this.#cfg.phase }),
      });
    }

    this.observeHealth(now);
  }

  /** Latch + LOG the degradation on its rising edge. A dead feed is never a silent feed. */
  private observeHealth(now: number): void {
    const wm = this.watermarkAt(now);
    if (wm.health === "DEGRADED" && this.#degradedReason === null) {
      this.#degradedReason = wm.reason;
      this.#log(`feed DEGRADED (fail-closed): ${wm.reason ?? "(no reason)"} [account=${redactAccount(this.#cfg.account)}]`);
    }
  }

  // ── the FeedSource face ────────────────────────────────────────────────────

  events(): Iterable<BusEvent> {
    return this.#emitted;
  }

  drain(): readonly BusEvent[] {
    const fresh = this.#emitted.slice(this.#drained);
    this.#drained = this.#emitted.length;
    return fresh;
  }

  subscriptions(): readonly FeedSubscription[] {
    return [...this.#lines.values()].map((l) => ({
      reqId: l.reqId,
      key: l.key,
      kind: l.kind,
      contract: l.contract,
      state: l.state,
      error: l.error,
      ready: l.ready,
    }));
  }

  watermark(): FeedWatermark {
    return this.watermarkAt(this.#now());
  }

  private watermarkAt(now: number): FeedWatermark {
    const series = [...this.#lines.values()].map((l) => this.seriesWatermark(l, now));
    const spot = series.find((s) => s.kind === "spot");
    const underlierStale = spot === undefined || spot.stale || spot.failed;

    // The underlier's death is the one that taints everything: every market series is a read off that
    // one causal coordinate. A dead leg degrades too — a chain we cannot vouch for is not a chain.
    const reasons: string[] = [];
    if (spot === undefined || spot.failed) {
      reasons.push(`the underlier line ${this.#underlier.symbol} is REFUSED/absent — there is no canonical coordinate`);
    } else if (spot.stale) {
      reasons.push(
        `the underlier line ${this.#underlier.symbol} is stale: its VALUE has not changed in ${spot.ageMs ?? "∞"}ms (> ${this.#staleAfterMs}ms), across ${spot.reprints} identical re-print(s) — the line is talking, the market is not`,
      );
    }
    const staleLegs = series.filter((s) => s.kind === "option" && s.stale && !s.failed);
    if (staleLegs.length > 0) {
      reasons.push(`${staleLegs.length} option leg(s) are stale (${staleLegs.map((s) => s.key).join(", ")})`);
    }
    const health: FeedHealth = reasons.length > 0 ? "DEGRADED" : "LIVE";
    return {
      now,
      seq: this.#seq - 1,
      staleAfterMs: this.#staleAfterMs,
      health,
      reason: health === "DEGRADED" ? reasons.join("; ") : null,
      series,
      underlierStale,
    };
  }

  private seriesWatermark(l: Line, now: number): FeedSeriesWatermark {
    const ageMs = l.asOfTs === null ? null : now - l.asOfTs;
    // FAIL CLOSED ON ABSENCE: a line that has never carried a value is STALE, not "fresh by default".
    const stale = l.state === "failed" || l.asOfTs === null || (ageMs !== null && ageMs > this.#staleAfterMs);
    return {
      key: l.key,
      kind: l.kind,
      asOfSeq: l.asOfSeq,
      asOfTs: l.asOfTs,
      lastPrintTs: l.lastPrintTs,
      reprints: l.reprints,
      ageMs,
      stale,
      dark: l.quote === null || (l.quote.bid === null && l.quote.ask === null),
      failed: l.state === "failed",
    };
  }

  fair(query: FeedFairQuery): FeedFair {
    const legs = this.liveLegs().map((l) => optionQuoteOf(l));
    const underlierPx = this.#spotLine.quote?.last ?? null;
    return feedFair({
      underlierPx,
      strike: query.strike,
      right: query.right,
      side: query.side,
      tauYears: this.#tauYears,
      legs,
      // The ATM-coverage policy the caller pinned (kestrel-7o2.19) — the receipt refuses to vouch for
      // an at-the-money valuation off a surface with no liquid strikes near the money.
      ...(this.#cfg.atmBandFraction === undefined ? {} : { bandFraction: this.#cfg.atmBandFraction }),
      ...(this.#cfg.atmPolicy === undefined ? {} : { atmPolicy: this.#cfg.atmPolicy }),
      // FRESHNESS, honestly: the OLDEST observation backing this valuation. A receipt that claimed the
      // freshest input would vouch for a freshness the surface as a whole does not have.
      asof: this.oldestAsOf(),
    });
  }

  private oldestAsOf(): number {
    const stamps = [this.#spotLine, ...this.liveLegs()]
      .map((l) => l.asOfTs)
      .filter((t): t is number => t !== null);
    return stamps.length === 0 ? this.#now() : Math.min(...stamps);
  }

  /** The live option lines, in a STABLE order (strike, then right). Sorted rather than taken in
   * subscription/arrival order: the socket's delivery order is not a fact about the market, and if it
   * leaked into the `legs` array the emitted bus would stop being replay-stable. */
  private liveLegs(): Line[] {
    return [...this.#lines.values()]
      .filter((l) => l.kind === "option" && l.state === "live")
      .sort((a, b) => {
        const ac = a.contract as IbkrOptionContract;
        const bc = b.contract as IbkrOptionContract;
        return ac.strike - bc.strike || ac.right.localeCompare(bc.right);
      });
  }

  private lineOf(c: IbkrQuotedContract): Line | undefined {
    return [...this.#lines.values()].find((l) => l.contract.conId === c.conId);
  }

  close(): void {
    if (this.#closed) return;
    this.#closed = true;
    for (const line of this.#lines.values()) {
      this.clearDeadline(line);
      if (line.state === "failed") continue; // the venue already refused it; nothing rests there
      try {
        this.#client.cancelMktData(line.reqId);
      } catch {
        // Closing an already-dead subscription is a no-op, never a crash (fail-closed).
      }
    }
    this.#router.dispose();
    for (const [event, listener] of this.#registered) this.#client.removeListener(event, listener);
    this.#registered.length = 0;
  }

  // ── emission ───────────────────────────────────────────────────────────────

  private emitMeta(): void {
    const instrument: SessionInstrument = {
      symbol: this.#cfg.instrument,
      assetClass: this.#legContracts.length > 0 ? "option-underlier" : "equity",
      role: "exec",
    };
    this.emit<MetaEvent>({
      ts: this.#now(),
      stream: "META",
      type: "session",
      session_date: this.#cfg.sessionDate,
      instruments: [instrument],
      mode: this.#cfg.mode,
      bus_schema: BUS_SCHEMA,
    });
  }

  /** Stamp `seq` and append. META must be first — a tape whose header does not lead it is not a bus,
   * and this is enforced rather than merely intended. */
  private emit<E extends BusEvent>(ev: Omit<E, "seq">): void {
    if (this.#seq === 0 && ev.stream !== "META") {
      throw new IbkrFeedError(`the first bus event must be META (got ${ev.stream}) — a tape without its header is not a bus`);
    }
    // `E` is pinned to a concrete variant at every call site (MetaEvent / SpotEvent / BookEvent /
    // HeartbeatEvent) and `seq` is the one field `Omit` removed, so re-adding it reconstitutes exactly
    // that variant. TS cannot see that through the distributed Omit, hence the widening step.
    this.#emitted.push({ ...ev, seq: this.#seq++ } as unknown as BusEvent);
  }

  #listen<A extends unknown[]>(event: EventName, listener: (...args: A) => void): void {
    const wrapped = listener as unknown as IbListener;
    this.#client.on(event, wrapped);
    this.#registered.push([event, wrapped]);
  }
}

/** The bus-shaped quote of one line. A dark side is `null` — NEVER `0`, never omitted — so the
 * reducer downstream can tell a genuinely one-sided book from a two-sided one. */
function optionQuoteOf(l: Line): OptionQuote {
  const c = l.contract as IbkrOptionContract;
  const q = l.quote;
  const last = q?.last;
  return {
    strike: c.strike,
    right: c.right,
    bid: q?.bid ?? null,
    ask: q?.ask ?? null,
    bsz: q?.bsz ?? null,
    asz: q?.asz ?? null,
    ...(last === undefined ? {} : { last }),
  };
}

/** `SPY|445C` — the stable per-leg watermark key. */
function legKey(symbol: string, strike: number, right: Right): string {
  return `${symbol}|${strike}${right}`;
}

/** The gateway's OWN definition, re-formed as an IB `Contract` for the subscription. Every field is
 * the one 7o2.6 read off the venue — the conId alone would do, but naming the full identity keeps the
 * request self-describing and impossible to confuse with a hand-rolled one. */
function ibContractOf(c: IbkrQuotedContract): Contract {
  if (c.kind === "equity") {
    return {
      conId: c.conId,
      symbol: c.symbol,
      secType: SecType.STK,
      exchange: c.exchange,
      currency: c.currency,
      ...(c.primaryExchange === undefined ? {} : { primaryExch: c.primaryExchange }),
    };
  }
  if (c.kind === "index") {
    // An INDEX line (kestrel-7o2.24) — the spot a cash-settled index option's strike window centres
    // on. `exchange` is the LISTING venue the gateway named (NASDAQ/CBOE), never SMART: SMART is an
    // order router and an index is not routed anywhere. No multiplier and no primaryExch exist on an
    // index to put on the wire.
    return {
      conId: c.conId,
      symbol: c.symbol,
      secType: SecType.IND,
      exchange: c.exchange,
      currency: c.currency,
    };
  }
  // EXHAUSTIVENESS, enforced by the compiler: this annotated binding only typechecks while every
  // non-OPTION member of IbkrQuotedContract has been branched away above. Add a member and forget a
  // branch, and `tsc` reds HERE — rather than the member silently falling through and being
  // subscribed to as an OPTION. (Before 7o2.24 this tail was a bare `else`, so an index underlier
  // would have gone onto the wire as an option with no strike and no expiry.)
  const opt: IbkrOptionContract = c;
  return {
    conId: opt.conId,
    symbol: opt.symbol,
    secType: SecType.OPT,
    exchange: opt.exchange,
    currency: opt.currency,
    lastTradeDateOrContractMonth: opt.expiry,
    strike: opt.strike,
    right: opt.right === "C" ? OptionType.Call : OptionType.Put,
    multiplier: opt.multiplier,
    ...(opt.tradingClass === undefined ? {} : { tradingClass: opt.tradingClass }),
  };
}

/**
 * Build the IBKR feed face (kestrel-7o2.7) — a {@link FeedSource} over the ONE shared IB session,
 * producing the EXISTING {@link BusEvent} union. Market data only: no order path exists here, and the
 * narrow client surface it speaks through declares none.
 */
export function ibkrFeed(cfg: IbkrFeedConfig, deps: IbkrFeedDeps): IbkrFeed {
  return new IbkrFeedImpl(cfg, deps);
}

// ─────────────────────────────────────────────────────────────────────────────
// The staleness gate — where a DEAD LINE becomes a DE-ARMED PLAN
// ─────────────────────────────────────────────────────────────────────────────

/** What the gate needs of a feed — just its watermark. (Injected as an interface so the gate is
 * testable against any freshness source, and so nothing here depends on a socket.) */
export interface WatermarkSource {
  watermark(): FeedWatermark;
}

/**
 * Wrap a {@link SeriesProvider} so that a DEAD FEED can never read as a live one (kestrel-7o2.7).
 *
 * The problem this exists to solve is not hypothetical, and it cannot be fixed inside canonical
 * state: `CanonicalState` folds every SPOT it is given and then *knows* that price forever. When the
 * line freezes — IB re-printing an identical quote while the market underneath it is gone — the last
 * value sits there looking perfectly healthy. An armed plan reading `spot` gets a number, the trigger
 * evaluates `true`, and the engine fires off a tape that died minutes ago. The frozen-feed forensics
 * (kestrel-rs4/8kc) are exactly this.
 *
 * So freshness is applied at the READ, from the one place that actually knows it — the feed's
 * {@link FeedWatermark}. While the feed is DEGRADED, every **market-side** series (the registry's own
 * market-vs-org routing decides which: `spot`, `vwap`, `hod`/`lod`, the opening range, every windowed
 * metric) resolves to **UNKNOWN** instead of to its last-known value. Only a definite `true` fires
 * (RUNTIME §3), so an UNKNOWN operand propagates through the trigger algebra and the affected plans
 * simply cannot fire. The taint is precise: **org** facts (the session's own bookkeeping — `pnl`, a
 * plan's state) are not market observations and are passed through untouched; the calendar is not a
 * market observation either.
 *
 * Every refusal is LOGGED with its reason (once per series while degraded — a reason, never a storm).
 */
export function stalenessGatedProvider(
  inner: SeriesProvider,
  feed: WatermarkSource,
  opts: { registry?: SeriesRegistry | undefined; log?: ((line: string) => void) | undefined } = {},
): SeriesProvider {
  const registry = opts.registry ?? defaultSeriesRegistry;
  const log = opts.log ?? (() => {});
  /** Series already refused during the CURRENT degradation — so the reason is logged once, not once
   * per sweep. Cleared when the feed comes back. */
  const announced = new Set<string>();

  const taint = (ref: SeriesRef): string | null => {
    const wm = feed.watermark();
    if (wm.health === "LIVE") {
      announced.clear();
      return null;
    }
    const rec = registry.classify(ref);
    // An UNREGISTERED name is an org path (the phonebook does not hold org facts) — not a market read.
    if (isUnknown(rec) || rec.kind !== "market") return null;
    return wm.reason ?? "the feed is DEGRADED";
  };

  const refuse = (ref: SeriesRef, reason: string): Unknown => {
    const name = ref.segments[0]?.name ?? "(empty path)";
    if (!announced.has(name)) {
      announced.add(name);
      log(
        `series "${name}" reads UNKNOWN — the feed is DEGRADED and a market series is only as alive as its line: ${reason} (fail-closed; the dependent trigger cannot fire, RUNTIME §3/§8)`,
      );
    }
    return UNKNOWN;
  };

  return {
    resolve(ref: SeriesRef, now: number): Resolved {
      const reason = taint(ref);
      return reason === null ? inner.resolve(ref, now) : refuse(ref, reason);
    },
    baseline(ref: SeriesRef, stat: Baseline): number | Unknown {
      const reason = taint(ref);
      return reason === null ? inner.baseline(ref, stat) : refuse(ref, reason);
    },
    phase(): SessionPhase | Unknown {
      // The session calendar is not a market observation — a dead line does not stop the clock.
      return inner.phase();
    },
    structural(ev: StructuralEvent, now: number): TriState {
      // A structural event (a failed break of HOD …) IS a market read — it is derived off the very
      // levels the dead line stopped feeding. It taints with them.
      if (feed.watermark().health === "DEGRADED") {
        log(`structural event reads UNKNOWN — the feed is DEGRADED (a level derived off a dead line is not a level)`);
        return UNKNOWN;
      }
      return inner.structural?.(ev, now) ?? UNKNOWN;
    },
    fillEvent(ev: FillEvent): TriState {
      // The session's OWN fills are its own bookkeeping, not a market observation — never tainted.
      return inner.fillEvent?.(ev) ?? UNKNOWN;
    },
    timeOfDayMinutes(now: number): number | Unknown {
      return inner.timeOfDayMinutes?.(now) ?? UNKNOWN;
    },
    quantify(ref: SeriesRef, now: number): readonly number[] | Unknown {
      return inner.quantify?.(ref, now) ?? UNKNOWN;
    },
  };
}

// ─────────────────────────────────────────────────────────────────────────────
// Contract resolution for a feed (the 7o2.6 layer, driven — never re-implemented)
// ─────────────────────────────────────────────────────────────────────────────

/** Which underlier + chain slice a feed wants to watch. */
export interface FeedContractQuery {
  readonly symbol: string;
  /** The chain expiry (`YYYYMMDD`). Absent ⇒ an equity-only tape (no legs are resolved at all — an
   * expiry is NEVER guessed, because "the nearest one" is a chain nobody asked for). */
  readonly expiry?: string | undefined;
  /**
   * The OBSERVED underlier price the strike window is CENTRED on — WHERE THE MONEY IS. **Required
   * whenever `expiry` is present** (kestrel-7o2.19): a chain slice picked without it is not a slice
   * of the market, it is a slice of the grid, and a grid whose middle is not the money hands the agent
   * five deep-ITM calls and five worthless puts (the live defect: 741–745 at a spot of 751.94).
   */
  readonly spot?: number | undefined;
  /** How many LISTED strikes to take EACH SIDE of the ATM one (default
   * {@link DEFAULT_SURFACE_HALF_WIDTH} = 4 ⇒ 9 strikes ⇒ 18 two-sided legs — the order path's own
   * window). A named, injectable parameter; never a magic number. */
  readonly halfWidth?: number | undefined;
  /** Pin a trading class (the only sanctioned way to break a two-class ambiguity). */
  readonly tradingClass?: string | undefined;
  /**
   * What the UNDERLIER is (kestrel-7o2.24): an equity/ETF (`STK`, the default — what every caller has
   * always meant) or an INDEX (`IND`, e.g. the NDX/XND class of cash-settled index options, whose
   * underlier is a published number and not an ETF).
   *
   * NEVER inferred from the symbol. An adapter that sniffed "this looks index-y" would be guessing an
   * identity, which is the one thing this layer exists not to do. Stating `STK` for an index symbol
   * does not silently mis-resolve: the gateway simply has no `STK` definition for it and the lookup
   * refuses `unresolvable`, loudly.
   */
  readonly underlierSecType?: UnderlierSecType | undefined;
}

/**
 * Resolve the contracts a feed subscribes to, by DRIVING the 7o2.6 contract layer (kestrel-7o2.7).
 * Nothing about identity resolution is re-implemented here: the equity comes from
 * {@link resolveUnderlier} (an equity/ETF or, since kestrel-7o2.24, an INDEX), the listed strikes
 * from {@link listOptionStrikes} (the venue's truth for THAT expiry, not the union across expiries),
 * and each leg from {@link resolveContract}. An
 * ambiguous or unresolvable identity raises the 7o2.6 typed refusal and reaches STAND_DOWN — the feed
 * never subscribes to a guessed contract.
 *
 * The WINDOW is {@link surfaceWindow} — the ONE rule (kestrel-7o2.19), the same one the order path
 * selects its vol surface with. It is centred on the ATM strike **the venue LISTS for this expiry**,
 * read from {@link listOptionStrikes} rather than from the chain's UNION grid: `reqSecDefOptParams`
 * returns the union of strikes across every expiry, and a strike in the union may simply not be listed
 * on the expiry asked for — selecting off it invents an identity the venue does not list.
 *
 * @throws {IbkrFeedError} when an option chain is asked for with no observed spot (fail-closed: no
 * money, no window).
 */
export async function resolveFeedContracts(
  query: FeedContractQuery,
  deps: IbkrContractDeps,
): Promise<{ underlier: IbkrUnderlierContract; legs: readonly IbkrOptionContract[] }> {
  // The underlier goes through the READ-path resolver (kestrel-7o2.24), which is the only entry
  // point that can return an INDEX. Its `secType` is the caller's statement of what the symbol IS —
  // defaulting to `STK`, which is what every pre-7o2.24 caller meant and still gets.
  const underlier = await resolveUnderlier(
    { symbol: query.symbol, secType: query.underlierSecType ?? "STK" },
    deps,
  );
  if (query.expiry === undefined) return { underlier, legs: [] };

  const spot = query.spot;
  if (spot === undefined || !Number.isFinite(spot) || spot <= 0) {
    // FAIL CLOSED. This is the kestrel-7o2.19 defect at its root: a chain slice chosen with no idea
    // where the money is degrades to "whatever the grid happens to start with" — and the Frame is the
    // agent's screen, its token budget scarce. Strikes it cannot trade are worse than no strikes.
    throw new IbkrFeedError(
      `a chain slice for ${query.symbol} ${query.expiry} was asked for with NO observed spot (got ${String(spot)}) — the strike window has no money to centre on, and a centre-less pick is the 741–745-at-751.94 defect. Subscribe to the underlier first and centre the window on the price it PRINTS.`,
    );
  }

  const legs: IbkrOptionContract[] = [];
  for (const right of ["C", "P"] as const) {
    // The venue's truth for THIS expiry — never the union grid across expiries.
    const listed = await listOptionStrikes(
      { symbol: query.symbol, expiry: query.expiry, right, ...(query.tradingClass === undefined ? {} : { tradingClass: query.tradingClass }) },
      { ...deps, expiry: query.expiry },
    );
    // THE ONE RULE (shared with the order path — a rule that exists twice is a rule that diverges).
    const window = surfaceWindow({
      listed,
      spot,
      ...(query.halfWidth === undefined ? {} : { halfWidth: query.halfWidth }),
    });
    deps.log?.(
      `feed: ${right} surface window @${query.expiry}: ATM=${window.atm} (spot=${spot}), ±${window.halfWidth} LISTED strikes ⇒ [${window.strikes.join(", ")}]`,
    );
    for (const strike of window.strikes) {
      const leg = await resolveContract(lookupIdentity(query.symbol, strike, right), {
        ...deps,
        expiry: query.expiry,
        ...(query.tradingClass === undefined ? {} : { tradingClass: query.tradingClass }),
      });
      if (leg.kind === "option") legs.push(leg);
    }
  }
  legs.sort((a, b) => a.strike - b.strike || a.right.localeCompare(b.right));
  return { underlier, legs };
}

/**
 * A contract-LOOKUP IDENTITY — deliberately NOT an order, and structurally incapable of becoming one.
 *
 * The 7o2.6 layer's entry point is typed on `OrderIntent` because an intent is the thing that carries
 * an instrument + (strike, right) identity in this runtime. It is PROVEN (7o2.6's own tests, at the
 * type level and with a runtime spy) never to READ a price, a size, or a side: it maps an identity
 * onto the gateway's definition and returns a contract carrying no price field of any kind. This feed
 * has no gate, no venue write path, and no order code — nothing it builds can reach one. So the
 * price/size/side fields below are structural filler for a type, never a trade: `0` × `0` at `0`,
 * annotated as what it is.
 */
function lookupIdentity(instrument: string, strike?: number, right?: Right) {
  return {
    ref: "ibkr-feed:lookup",
    plan: "ibkr-feed",
    plan_instance: "ibkr-feed#0",
    role: "entry" as const,
    instrument,
    side: "buy" as const,
    qty: 0,
    px: 0,
    sourceAnnotation:
      "ibkr-feed: a CONTRACT-LOOKUP IDENTITY, not an order — market data only (no gate, no venue write path, no order code anywhere in this face)",
    ...(strike === undefined ? {} : { strike }),
    ...(right === undefined ? {} : { right }),
  };
}

// ─────────────────────────────────────────────────────────────────────────────
// Production defaults — substituted wholesale in tests
// ─────────────────────────────────────────────────────────────────────────────

/** The default request-id minter — a process-local monotone counter, well clear of the contract
 * layer's 9_000 space. A counter, not an RNG: the edge may be async, but it is never random. */
let reqIdCounter = 20_000;
const defaultNextReqId = (): number => ++reqIdCounter;

/** The real deadline scheduler over global timers (the transport's own default, mirrored). Kept off
 * the deterministic core — only the edge adapter uses it. */
const defaultScheduler: Scheduler = {
  setInterval: (fn, ms) => setInterval(fn, ms),
  clearInterval: (handle) => {
    clearInterval(handle as ReturnType<typeof setInterval>);
  },
};

export { EXEC_FAIR_MODEL };
