/**
 * # The IBKR PAPER **DRY-RUN ORDER TICKET** (kestrel-7o2.8) — env-gated, TRANSMITS NOTHING
 *
 * Not a test (it is not a `*.test.ts` and never runs under `bun test`): a hand-run script that drives
 * the whole paper order face against a LIVE, client-launched IB Gateway and **prints the order ticket
 * a human then reviews** — the contract identity, the side/qty, the `@fair`-anchored limit price WITH
 * ITS RECEIPT, the observed bid/ask (a HEALTH SIGNAL, never a price), the max loss, the budget, and
 * the L0 clamp verdict.
 *
 * ## THE HARD RULE: THE DEFAULT PATH TRANSMITS NOTHING
 * It resolves the contract, pulls the quotes, computes the price, runs the L0 clamp, prints the
 * ticket — and STOPS. `IbkrBroker.preflight` is what it calls: the full guard chain WITHOUT the
 * `placeOrder`. Transmission happens ONLY if `KESTREL_IBKR_TRANSMIT=1` is explicitly set, which is a
 * separate, deliberate, HUMAN act. The dry run is the default and says so loudly.
 *
 * ## What it is (and is not) allowed to be
 * - The instrument is **SPY** — a generic, publication-safe ticker. Nothing here reveals an
 *   application or a strategy (ARCHITECTURE §7).
 * - The action is a **DEFINED-RISK LONG**: BUY 1 option contract. Max loss = the premium, so
 *   never-naked holds trivially and the ticket can be checked by eye.
 * - `mode` is **paper**. The order face refuses `live` at construction.
 *
 * ## The quote source is INJECTED
 * kestrel-7o2.7 (the `ibkrFeed` FeedSource) is a parallel bead and does not exist on this branch, so
 * this script pulls its two-sided quotes directly through the transport's shared socket
 * ({@link reqMktDataQuotes}). That path is behind an injected {@link QuoteSource}, so the real feed
 * drops in later without touching this script.
 *
 * ## Run it
 * ```bash
 * KESTREL_IBKR_PAPER_E2E=1 KESTREL_IBKR_CLIENT_ID=14 bun run src/adapters/broker/ibkr/order-dryrun.ts
 * ```
 */

import { EventName, SecType, OptionType } from "@stoqey/ib";
import type { Contract } from "@stoqey/ib";

import { IbkrTransport } from "./transport.ts";
import { resolveIbkrConfig, describeIbkrConfig } from "./config.ts";
import { loadEnvFallback } from "../../../cli/credentials.ts";
import { resolveContract, resolveOptionChain, listOptionStrikes, contractClientOf } from "./contract.ts";
import type { IbkrContract, IbkrOptionContract, IbkrEquityContract } from "./contract.ts";
import { ibkrBroker, contractBook, orderClientOf } from "./broker.ts";
import type { OrderTicket, PriceAnchor, QuoteFreshness } from "./broker.ts";
import { executionFair, executionFairSpot, intrinsic, tauYearsToClose } from "../../../fair/index.ts";
import type { OptionQuote } from "../../../bus/index.ts";
import type { OrderIntent } from "../../../engine/index.ts";
import type { RiskLimits } from "../../broker.ts";

// ─────────────────────────────────────────────────────────────────────────────
// The injected quote seam — a two-sided quote, and nothing more.
// ─────────────────────────────────────────────────────────────────────────────

/**
 * A two-sided quote for one contract, WITH ITS PROVENANCE.
 *
 * `bid`/`ask` are a HEALTH SIGNAL (is this thing tradeable?) — never a value, and never a price on
 * their own. But they are not decoration either: they BOUND `@fair`, which is the only thing that
 * prices an order. And `@fair` may only be computed from LIVE input, so `freshness` travels WITH the
 * numbers — a quote that cannot say where it came from cannot anchor anything (kestrel-7o2.8).
 */
export interface TwoSidedQuote {
  readonly bid: number | null;
  readonly ask: number | null;
  readonly last: number | null;
  /** What the VENUE served. Only `live` may ANCHOR a price; the rest are health signals only. */
  readonly freshness: QuoteFreshness;
}

/**
 * Where the dry run gets its quotes (kestrel-7o2.8). INJECTED so the real `ibkrFeed` FeedSource
 * (kestrel-7o2.7, a parallel bead) substitutes for {@link reqMktDataQuotes} later without this script
 * changing at all.
 */
export interface QuoteSource {
  quote(contract: Contract): Promise<TwoSidedQuote>;
}

/**
 * IB's `tickPrice` field ids. `@stoqey/ib` exports its root `TickType` as a TYPE union, not as an enum
 * VALUE, so the ids are named here rather than deep-imported or left as bare literals at the branch —
 * the same discipline `contract.ts` applies to `IB_NO_SECURITY_DEFINITION`.
 */
const IB_TICK_BID = 1;
const IB_TICK_ASK = 2;
const IB_TICK_LAST = 4;
/** The DELAYED mirrors of the three above. Their ARRIVAL is itself proof the data is not live. */
const IB_TICK_DELAYED_BID = 66;
const IB_TICK_DELAYED_ASK = 67;
const IB_TICK_DELAYED_LAST = 68;

/** IB market-data types: 1 live · 2 frozen · 3 delayed · 4 delayed-frozen. */
const IB_MARKET_DATA_LIVE = 1;

/** IB's `marketDataType` id → what it means for a PRICE ANCHOR. An unrecognised id is `unknown`, and
 * `unknown` is refused just as loudly as `delayed` — provenance we cannot establish is not provenance. */
function freshnessOf(marketDataType: number): QuoteFreshness {
  switch (marketDataType) {
    case 1:
      return "live";
    case 2:
      return "frozen";
    case 3:
      return "delayed";
    case 4:
      return "delayed-frozen";
    default:
      return "unknown";
  }
}

/** The narrow MARKET-DATA surface of the shared IB client. Read-only — it has no order method at all. */
interface IbQuoteClient {
  reqMktData(reqId: number, contract: Contract, genericTickList: string | null, snapshot: boolean, regulatorySnapshot: boolean): unknown;
  cancelMktData(reqId: number): unknown;
  reqMarketDataType(marketDataType: number): unknown;
  on(event: EventName, listener: (...args: never[]) => void): unknown;
  removeListener(event: EventName, listener: (...args: never[]) => void): unknown;
}

/**
 * The default {@link QuoteSource}: `reqMktData` over the transport's ONE shared socket. It collects
 * `tickPrice` until BOTH sides are present or the deadline lapses, then cancels the subscription.
 *
 * ## It asks for LIVE, and it REPORTS what it actually got (kestrel-7o2.8, blocker 4a)
 * The shipped version asked for `DELAYED_FROZEN` and fed the result to `@fair` as a PRICE ANCHOR. That
 * is the defect: a price anchor must never accept delayed/frozen input. So we request **LIVE** (type
 * 1) — and because IB SUBSTITUTES rather than refusing when the account lacks a live subscription, we
 * also LISTEN: the venue's own `marketDataType` callback, and the arrival of the DELAYED tick ids
 * (66/67/68), each stamp the quote's {@link TwoSidedQuote.freshness}. A delayed quote is still returned
 * — it is an honest health signal, and a MISSING one would look like a dark book — but it is returned
 * WEARING ITS LABEL, and everything that PRICES an order refuses it (the broker's price-anchor wall).
 */
export function reqMktDataQuotes(transport: IbkrTransport, timeoutMs = 6_000): QuoteSource {
  const client = transport.client() as unknown as IbQuoteClient;
  let reqId = 9_000;
  // LIVE. Anything that prices an order requires it; if the venue serves something else, the quote
  // below says so and the price anchor fails closed rather than quietly pricing off stale data.
  client.reqMarketDataType(IB_MARKET_DATA_LIVE);

  return {
    quote(contract: Contract): Promise<TwoSidedQuote> {
      const id = ++reqId;
      return new Promise<TwoSidedQuote>((resolve) => {
        let bid: number | null = null;
        let ask: number | null = null;
        let last: number | null = null;
        // Until the venue tells us otherwise, its provenance is UNKNOWN — never assumed live.
        let freshness: QuoteFreshness = "unknown";
        let done = false;

        const finish = (): void => {
          if (done) return;
          done = true;
          clearTimeout(deadline);
          client.removeListener(EventName.tickPrice, onTick as never);
          client.removeListener(EventName.marketDataType, onType as never);
          try {
            client.cancelMktData(id);
          } catch {
            // A cancel on a subscription the gateway already dropped is a no-op, never a crash.
          }
          resolve({ bid, ask, last, freshness });
        };

        /** The venue's own word on what it is serving THIS request. */
        const onType = (rid: number, marketDataType: number): void => {
          if (rid !== id) return;
          freshness = freshnessOf(marketDataType);
        };

        const onTick = (rid: number, field: number, value: number): void => {
          if (rid !== id) return;
          // IB reports "no quote" as -1 (and sometimes 0 for an untraded option). Treat both as DARK
          // rather than as a price — a fabricated level is exactly what fail-closed forbids.
          const v = value > 0 ? value : null;
          switch (field) {
            case IB_TICK_BID:
              bid = v;
              break;
            case IB_TICK_ASK:
              ask = v;
              break;
            case IB_TICK_LAST:
              last = v;
              break;
            // A DELAYED tick id is ITSELF the evidence: whatever the venue said (or did not say), data
            // arriving on 66/67/68 is not live, and it can never be laundered into a price anchor.
            case IB_TICK_DELAYED_BID:
              bid = v;
              freshness = "delayed";
              break;
            case IB_TICK_DELAYED_ASK:
              ask = v;
              freshness = "delayed";
              break;
            case IB_TICK_DELAYED_LAST:
              last = v;
              freshness = "delayed";
              break;
            default:
              return;
          }
          if (bid !== null && ask !== null) finish();
        };

        const deadline = setTimeout(finish, timeoutMs);
        client.on(EventName.tickPrice, onTick as never);
        client.on(EventName.marketDataType, onType as never);
        client.reqMktData(id, contract, null, false, false);
      });
    },
  };
}

/**
 * Build the broker's {@link PriceAnchor} from an OBSERVED quote and the `@fair` derived from it —
 * fail-closed: a quote that is not LIVE and two-sided yields NO anchor at all, and the broker's
 * price-anchor wall then refuses the order. This is the ONE place a script turns a quote into
 * something that may price an order, and it is deliberately unable to launder a stale one.
 */
export function priceAnchorFrom(quote: TwoSidedQuote, fair: number): PriceAnchor | undefined {
  if (quote.bid === null || quote.ask === null) return undefined; // dark / one-sided: not a book
  if (quote.freshness !== "live") return undefined; // a HEALTH SIGNAL, never a price anchor
  // A LIVE two-sided quote for the EQUITY path (a fresh, tight SPY-spot book) VOUCHES for the fair the
  // anchor carries (kestrel-ltrf): the fair is derived from exactly this live book, so its receipt is
  // trusted. The precise per-instrument trust for the (index-option) fair belongs to the fair redesign
  // in src/fair (kestrel-ku99/ltrf) — this script builds only from a fresh live book.
  return { bid: quote.bid, ask: quote.ask, fair, fairTrusted: true, freshness: quote.freshness };
}

// ─────────────────────────────────────────────────────────────────────────────
// The ticket.
// ─────────────────────────────────────────────────────────────────────────────

/** How wide an ATM window of strikes the vol surface is built from. A `@fair` with a receipt needs
 * real liquid neighbours — one leg cannot imply its own surface. */
const SURFACE_HALF_WIDTH = 4;

/** SPY options quote in pennies. Used ONLY to floor-snap a BUY (a BUY never bids ABOVE fair). */
const TICK_SIZE = 0.01;

/** The L0 ceilings this dry run is bounded by. One contract, a few hundred dollars — small enough
 * that a human can check the arithmetic by eye. */
const LIMITS: RiskLimits = { maxOrderQty: 1, maxPositionQty: 1, maxNotionalUsd: 2_000 };

/** The dry run's budget/R — what `size x max_loss <= budget` is checked against. */
const BUDGET_USD = 2_000;

interface DryRunResult {
  readonly ticket: OrderTicket;
  /** The guarded face. `preflight` has already run; `submit` is the ONLY thing that would transmit,
   * and the entrypoint calls it ONLY under an explicit human `KESTREL_IBKR_TRANSMIT=1`. */
  readonly broker: ReturnType<typeof ibkrBroker>;
  readonly intent: OrderIntent;
  readonly fair: number;
  readonly fairReceipt: string;
  readonly spot: number;
  readonly spotReceipt: string;
  readonly optionQuote: TwoSidedQuote;
  readonly underlierQuote: TwoSidedQuote;
  readonly intrinsicUsd: number;
  readonly expiry: string;
  readonly tauYears: number;
  readonly surfaceLegs: number;
}

/** The gateway's own last-trading-day → the epoch-ms of that session's 16:00 ET close. `tau` is
 * computed against the VENUE's clock (its `currentTime` heartbeat), never this machine's. */
function closeTsOf(expiry: string): number {
  const y = Number(expiry.slice(0, 4));
  const m = Number(expiry.slice(4, 6));
  const d = Number(expiry.slice(6, 8));
  // 16:00 ET == 20:00 UTC (EDT) — the half-hour of DST error this can carry is far below the
  // precision `tau` needs for a same-week option, and it is EXPLICIT rather than hidden.
  return Date.UTC(y, m - 1, d, 20, 0, 0);
}

/** Floor-snap a BUY to the tick — a BUY never bids ABOVE fair (engine/pricing.ts's own direction). */
function floorSnap(px: number, tick: number): number {
  return Math.floor(px / tick + 1e-9) * tick;
}

/**
 * Resolve → quote → price → clamp → PRINT. Transmits NOTHING unless `transmit` is `true`, and the
 * ONLY caller that can pass `true` is the entrypoint below, under an explicit human
 * `KESTREL_IBKR_TRANSMIT=1`. `preflight` (never `submit`) is what the dry path calls.
 */
async function dryRun(transmit: boolean): Promise<DryRunResult> {
  const cfg = resolveIbkrConfig({});
  if (cfg.mode !== "paper") {
    throw new Error(`the dry run is PAPER-ONLY (got mode=${cfg.mode}) — fail-closed`);
  }
  const transport = new IbkrTransport(cfg, { log: (l) => console.log(`  [transport] ${l}`) });

  console.log(`\nconnecting: ${describeIbkrConfig(cfg)}`);
  const status = await transport.connect();
  console.log(`  connected. account=${status.account} serverTime=${new Date(status.serverTime ?? 0).toISOString()}`);

  try {
    const cdeps = { client: contractClientOf(transport), log: (l: string) => console.log(`  [contract] ${l}`) };
    const quotes = reqMktDataQuotes(transport);
    // The VENUE's clock, not this machine's — the same discipline the transport's heartbeat holds to.
    const nowTs = status.serverTime ?? Date.now();

    // ── 1. The underlier: SPY equity → its conId, and its own two-sided quote → @fair(spot).
    const spotIntent = intentOf({ ref: "dry-spot", side: "buy", qty: 1, px: 0 });
    const equity = (await resolveContract(spotIntent, cdeps)) as IbkrEquityContract;
    console.log(`  SPY equity: conId=${equity.conId} ${equity.exchange}/${equity.primaryExchange ?? "?"}`);

    const underlierQuote = await quotes.quote(ibEquityQuery(equity));
    const spotFair = executionFairSpot({ bid: underlierQuote.bid, ask: underlierQuote.ask, asof: nowTs });
    if (spotFair === null) {
      throw new Error(
        `the underlier's book is one-sided/dark/crossed (bid=${underlierQuote.bid} ask=${underlierQuote.ask}) — @fair(spot) is UNRESOLVABLE and a mid is NEVER substituted. Fail-closed.`,
      );
    }
    const spot = spotFair.value;
    const spotReceipt = `${spotFair.receipt.model}(bid=${spotFair.receipt.bid},ask=${spotFair.receipt.ask})`;
    console.log(`  @fair(spot) = ${spot.toFixed(4)}  [${spotReceipt}]`);

    // ── 2. The chain: pick a NEAR-DATED expiry from what the venue actually lists.
    const chain = await resolveOptionChain({ symbol: "SPY", underlyingConId: equity.conId, tradingClass: "SPY" }, cdeps);
    const today = ymd(nowTs);
    const expiry = chain.expirations.find((e) => e >= today);
    if (expiry === undefined) {
      throw new Error(`the venue lists no expiry on/after ${today} — an expiry is NEVER invented. Fail-closed.`);
    }
    console.log(`  chain: ${chain.expirations.length} expiries, ${chain.strikes.length} strikes (union). near-dated = ${expiry}`);

    // ── 3. The strike: the ATM one the venue LISTS for THAT expiry (the union grid would invent one).
    const listed = await listOptionStrikes({ symbol: "SPY", expiry, right: "C", tradingClass: "SPY" }, cdeps);
    const atm = nearest(listed, spot);
    console.log(`  listed strikes for ${expiry}C: ${listed.length}. ATM = ${atm}`);

    // ── 4. The contract: the gateway's OWN definition (conId, OCC localSymbol, MULTIPLIER, expiry).
    const legIntent = intentOf({ ref: "dry-1", side: "buy", qty: 1, px: 0, strike: atm, right: "C" });
    const option = (await resolveContract(legIntent, { ...cdeps, expiry, tradingClass: "SPY" })) as IbkrOptionContract;
    console.log(`  option: ${option.localSymbol} conId=${option.conId} x${option.multiplier}`);

    // ── 5. The surface: an ATM window of LISTED strikes, quoted. `@fair` needs liquid neighbours —
    //      one leg cannot imply its own vol.
    const window = atmWindow(listed, atm, SURFACE_HALF_WIDTH);
    const tauYears = tauYearsToClose(nowTs, closeTsOf(expiry));
    const legs: OptionQuote[] = [];
    let staleLegs = 0;
    for (const strike of window) {
      for (const right of ["C", "P"] as const) {
        const q = await quotes.quote(ibOptionQuery(option, strike, right));
        // ONE-SIDED / DARK legs are DROPPED, never mid-filled — a fabricated level would poison the
        // surface, and the whole point of the receipt is that it cannot be.
        if (q.bid === null || q.ask === null) continue;
        // …and so are DELAYED/FROZEN ones. The surface PRICES the order; every input to it is a price
        // input, and a price input is never stale (kestrel-7o2.8). A stale leg is not a cheap leg.
        if (q.freshness !== "live") {
          staleLegs += 1;
          continue;
        }
        legs.push({ strike, right, bid: q.bid, ask: q.ask, bsz: null, asz: null, ...(q.last === null ? {} : { last: q.last }) });
      }
    }
    console.log(
      `  surface: ${legs.length} LIVE two-sided legs across ${window.length} strikes (tau=${tauYears.toFixed(6)}y)` +
        (staleLegs > 0 ? `  [${staleLegs} leg(s) DROPPED as delayed/frozen — never a price input]` : ""),
    );

    const optionQuote = await quotes.quote(ibOptionQuery(option, atm, "C"));

    // ── 5b. THE PRICE-ANCHOR GATE (kestrel-7o2.8, blocker 4a). Everything that PRICES an order must
    //      come from LIVE data. The venue SUBSTITUTES delayed/frozen data rather than refusing, so the
    //      substitution is checked HERE, before a single number reaches `@fair`. Delayed/frozen input
    //      is a HEALTH SIGNAL — it may be printed, it may never anchor — so the anchor is UNRESOLVABLE
    //      and the run fails closed rather than producing an authorizable ticket off stale prices.
    const staleInputs = [
      ["underlier", underlierQuote.freshness] as const,
      [`${atm}C`, optionQuote.freshness] as const,
    ].filter(([, f]) => f !== "live");
    if (staleInputs.length > 0) {
      throw new Error(
        `the PRICE ANCHOR requires LIVE market data, and the venue served ${staleInputs
          .map(([what, f]) => `${what}=${f.toUpperCase()}`)
          .join(", ")} — delayed/frozen data is a HEALTH SIGNAL, never a price anchor. @fair is UNRESOLVABLE. Fail-closed; NOTHING transmitted.`,
      );
    }

    // ── 6. @fair — Black-76 at the interpolated strike IV, floored at intrinsic, WITH ITS RECEIPT.
    const fair = executionFair({
      underlyingSpot: spot,
      strike: atm,
      right: "C",
      tauYears,
      liquidQuotes: legs,
      asof: nowTs,
    });
    if (fair === null) {
      throw new Error(
        `@fair is UNRESOLVABLE (no usable underlier, or ZERO liquid strikes backed the surface) — and a mid is NEVER a price anchor, so there is nothing to fall back to here. Fail-closed.`,
      );
    }
    const fairReceipt = `${fair.receipt.model}(nLiquid=${fair.receipt.nLiquid},ivAtStrike=${fair.receipt.ivAtStrike.toFixed(4)},asof=${fair.receipt.asof ?? "-"})`;
    console.log(`  @fair(${atm}C) = ${fair.value.toFixed(4)}  [${fairReceipt}]`);

    // ── 7. The limit price: @fair, floor-snapped. A BUY NEVER bids above fair.
    const limitPx = floorSnap(fair.value, TICK_SIZE);
    const intrinsicUsd = intrinsic(spot, atm, "C");

    // ── 8. The L0 clamp — via preflight(), which runs ALL FOUR WALLS and TRANSMITS NOTHING.
    const orderClient = orderClientOf(transport);
    const broker = ibkrBroker(cfg, {
      client: orderClient,
      contracts: contractBook([option, equity]),
      limits: LIMITS,
      intrinsicOf: (leg) => (leg.strike === undefined || leg.right === undefined ? undefined : intrinsic(spot, leg.strike, leg.right)),
      // THE PRICE ANCHOR (wall 6). The book we OBSERVED, beside the `@fair` we derived from it — the
      // broker refuses outright if fair falls outside it, or if the book was not live. A leg we never
      // quoted has NO anchor, so it cannot be ordered at all: fail-closed, never an assumed book.
      priceAnchorOf: (leg) =>
        leg.strike === undefined || leg.right === undefined
          ? priceAnchorFrom(underlierQuote, spot)
          : leg.strike === atm && leg.right === "C"
            ? priceAnchorFrom(optionQuote, fair.value)
            : undefined,
      expectedPositions: () => ({}),
      // The id sequence is the GATEWAY's (`nextValidId`), never ours to invent. It is only ever drawn
      // on the transmit path — the dry run never reaches it, and if the gateway never issued one, a
      // transmit fails closed rather than guessing an id.
      nextOrderId: mintOrderId(orderClient),
      drain: () => {},
    });

    const intent: OrderIntent = intentOf({
      ref: "dry-1",
      side: "buy",
      qty: 1,
      px: limitPx,
      strike: atm,
      right: "C",
      sourceAnnotation: `fair=${fairReceipt}`,
    });

    const ticket = broker.preflight(intent); // ← the guard chain. NOTHING is transmitted.

    const result: DryRunResult = {
      ticket,
      broker,
      intent,
      fair: fair.value,
      fairReceipt,
      spot,
      spotReceipt,
      optionQuote,
      underlierQuote,
      intrinsicUsd,
      expiry,
      tauYears,
      surfaceLegs: legs.length,
    };
    printTicket(result);

    // ── THE WALL. `submit` is the ONE call that reaches `placeOrder`, and it is reached ONLY here,
    // ONLY when a human set KESTREL_IBKR_TRANSMIT=1. The default path below simply does not call it.
    if (transmit) {
      console.log("KESTREL_IBKR_TRANSMIT=1 — a human authorized the ticket above. Transmitting...\n");
      broker.submit(intent);
      await settle(3_000); // let the venue's own ORDER callbacks land before we drop the socket
      for (const rec of broker.ledger()) {
        console.log(`  ledger: ${rec.ref} [ibOrderId=${rec.ibOrderId}] phase=${rec.phase} filled=${rec.filledQty}/${rec.submittedQty} avgPx=${rec.avgFillPx ?? "-"} commission=${rec.commissionUsd}`);
      }
    } else {
      console.log("KESTREL_IBKR_TRANSMIT is NOT set. placeOrder was NEVER called. This is the default and correct outcome.\n");
    }

    return result;
  } finally {
    transport.disconnect();
  }
}

const settle = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms));

// ─────────────────────────────────────────────────────────────────────────────
// Printing.
// ─────────────────────────────────────────────────────────────────────────────

function printTicket(r: DryRunResult): void {
  const t = r.ticket;
  const c = t.contract as IbkrOptionContract;
  const w = (label: string, value: string): string => `  ${label.padEnd(22)} ${value}`;

  console.log(`
╔══════════════════════════════════════════════════════════════════════════════╗
║  ORDER TICKET — kestrel-7o2.8 · IBKR PAPER                                    ║
║  *** DRY RUN — NO ORDER TRANSMITTED ***                                       ║
╚══════════════════════════════════════════════════════════════════════════════╝

CONTRACT (the gateway's OWN definition — nothing hand-rolled)
${w("localSymbol", c.localSymbol)}
${w("conId", String(c.conId))}
${w("secType / kind", `${c.secType} (${c.kind})`)}
${w("strike / right", `${c.strike} ${c.right}`)}
${w("expiry", `${c.expiry}  (tau = ${r.tauYears.toFixed(6)}y)`)}
${w("multiplier", `x${c.multiplier}  (the VENUE's, never an assumed 100)`)}
${w("exchange / currency", `${c.exchange} / ${c.currency}`)}
${w("tradingClass", c.tradingClass ?? "(none)")}

ORDER
${w("side / qty", `${t.side.toUpperCase()} ${t.qty}`)}
${w("type", "LIMIT (LMT, DAY)")}
${w("limit price", `$${t.limitPx.toFixed(2)}`)}

PRICE — @fair is the ANCHOR (the mid is NEVER a price and NEVER a value)
${w("@fair", `$${r.fair.toFixed(4)}`)}
${w("  receipt", r.fairReceipt)}
${w("  surface", `${r.surfaceLegs} two-sided legs`)}
${w("limit = floor-snap(fair)", `$${t.limitPx.toFixed(2)}  (a BUY never bids ABOVE fair)`)}
${w("@fair(spot)", `$${r.spot.toFixed(4)}`)}
${w("  receipt", r.spotReceipt)}
${w("intrinsic", `$${r.intrinsicUsd.toFixed(4)}  (the SELL floor; not binding on a BUY)`)}

THE OBSERVED BOOK — a health signal that also BOUNDS the anchor (wall 6, kestrel-7o2.8)
${w("option bid / ask", `${fmt(r.optionQuote.bid)} / ${fmt(r.optionQuote.ask)}   [${r.optionQuote.freshness.toUpperCase()}]`)}
${w("underlier bid / ask", `${fmt(r.underlierQuote.bid)} / ${fmt(r.underlierQuote.ask)}   [${r.underlierQuote.freshness.toUpperCase()}]`)}
${w("PRICE ANCHOR", `@fair ${t.anchor.fair.toFixed(4)} INSIDE the LIVE book ${t.anchor.bid} / ${t.anchor.ask}  ✓`)}
${w("  (a stale book, or a fair outside it, produces NO ticket at all — the run fails closed)", "")}

RISK — DEFINED-RISK LONG (never-naked holds trivially: max loss = the premium)
${w("notional", `$${t.notionalUsd.toFixed(2)}  = ${t.limitPx.toFixed(2)} x ${t.qty} x ${t.multiplier}`)}
${w("MAX LOSS", `$${t.maxLossUsd.toFixed(2)}`)}
${w("budget / R", `$${BUDGET_USD.toFixed(2)}`)}
${w("size x max_loss <= R", `$${t.maxLossUsd.toFixed(2)} <= $${BUDGET_USD.toFixed(2)}  ${t.maxLossUsd <= BUDGET_USD ? "OK" : "VIOLATED"}`)}
${w("position before/after", `${t.heldBefore} -> ${t.heldAfter}  (never negative — never-naked)`)}

L0 CLAMP (7o2.9 — the pre-transmit ceilings, checked BEFORE the wire)
${w("maxOrderQty", `${t.qty} <= ${LIMITS.maxOrderQty}`)}
${w("maxPositionQty", `${Math.abs(t.heldAfter)} <= ${LIMITS.maxPositionQty}`)}
${w("maxNotionalUsd", `${t.notionalUsd.toFixed(2)} <= ${LIMITS.maxNotionalUsd}`)}
${w("VERDICT", t.clamp.toUpperCase())}

╔══════════════════════════════════════════════════════════════════════════════╗
║  DRY RUN — NO ORDER TRANSMITTED. placeOrder was NEVER called.                 ║
║  To transmit, a human sets KESTREL_IBKR_TRANSMIT=1 and re-runs.               ║
╚══════════════════════════════════════════════════════════════════════════════╝
`);
}

const fmt = (v: number | null): string => (v === null ? "DARK" : `$${v.toFixed(2)}`);

// ─────────────────────────────────────────────────────────────────────────────
// Helpers.
// ─────────────────────────────────────────────────────────────────────────────

/** A SPY intent skeleton. The dry run never authors a strategy — this is a bare, publication-safe leg. */
function intentOf(over: Partial<OrderIntent> & Pick<OrderIntent, "ref" | "side" | "qty" | "px">): OrderIntent {
  return {
    plan: "dry-run",
    plan_instance: "dry-run#1",
    role: "entry",
    instrument: "SPY",
    sourceAnnotation: "dry-run",
    ...over,
  } as OrderIntent;
}

/**
 * The order-id minter. The sequence is the GATEWAY's (`nextValidId`) — never ours to invent, and never
 * an RNG. It is drawn ONLY on the transmit path; the dry run never reaches it. If the gateway never
 * issued a seed, a transmit FAILS CLOSED rather than guessing an id that might collide with a live one.
 *
 * EXPORTED (kestrel-7o2.12) so the paper session driver's venue composition draws its order ids from
 * this ONE rule rather than a second copy of it — a rule that exists twice is a rule that diverges.
 */
export function mintOrderId(client: { on(event: EventName, listener: (...args: never[]) => void): unknown; reqIds(n?: number): unknown }): () => number {
  let next: number | undefined;
  client.on(EventName.nextValidId, ((id: number) => {
    // IB re-issues this on connect and after `reqIds`; only ever move FORWARD (an id is never reused).
    if (next === undefined || id > next) next = id;
  }) as never);
  client.reqIds(1);
  return () => {
    if (next === undefined) {
      throw new Error(
        "the IB Gateway has issued no nextValidId — refusing to invent an order id (it could collide with a live order). Fail-closed.",
      );
    }
    const id = next;
    next += 1;
    return id;
  };
}

function ibEquityQuery(e: IbkrEquityContract): Contract {
  return { conId: e.conId, symbol: e.symbol, secType: SecType.STK, exchange: e.exchange, currency: e.currency };
}

function ibOptionQuery(o: IbkrOptionContract, strike: number, right: "C" | "P"): Contract {
  return {
    symbol: o.symbol,
    secType: SecType.OPT,
    exchange: o.exchange,
    currency: o.currency,
    lastTradeDateOrContractMonth: o.expiry,
    strike,
    right: right === "C" ? OptionType.Call : OptionType.Put,
    ...(o.tradingClass === undefined ? {} : { tradingClass: o.tradingClass }),
    multiplier: o.multiplier,
  };
}

const ymd = (ts: number): string => new Date(ts).toISOString().slice(0, 10).replace(/-/g, "");

function nearest(xs: readonly number[], target: number): number {
  let best = xs[0] as number;
  for (const x of xs) if (Math.abs(x - target) < Math.abs(best - target)) best = x;
  return best;
}

function atmWindow(listed: readonly number[], atm: number, half: number): readonly number[] {
  const i = listed.indexOf(atm);
  if (i < 0) return [atm];
  return listed.slice(Math.max(0, i - half), Math.min(listed.length, i + half + 1));
}

// ─────────────────────────────────────────────────────────────────────────────
// Entrypoint — env-gated, dry by default.
// ─────────────────────────────────────────────────────────────────────────────

// `import.meta.main` first: this module EXPORTS the quote source, so a unit test (or the equity
// script) importing it must never dial a gateway or print a banner — only running it as a script does.
if (import.meta.main) {
  loadEnvFallback(); // KESTREL_IBKR_* identifiers from the shared secrets home (OSS-ADR-0054)
  if (process.env.KESTREL_IBKR_PAPER_E2E === "1") {
    // THE WALL. Transmission is a separate, deliberate, HUMAN act — never a default, and never a thing
    // a script decides for itself. DRY is the default: absent this env var, `submit` is never called.
    const transmit = process.env.KESTREL_IBKR_TRANSMIT === "1";
    if (!transmit) console.log("\n*** DRY RUN — no order will be transmitted (KESTREL_IBKR_TRANSMIT is not set) ***");

    dryRun(transmit).catch((e: unknown) => {
      console.error(`\nDRY RUN FAILED (nothing transmitted): ${e instanceof Error ? e.message : String(e)}\n`);
      process.exitCode = 1;
    });
  } else {
    console.log("kestrel-7o2.8 dry-run ticket: set KESTREL_IBKR_PAPER_E2E=1 to run it against a live PAPER gateway.");
  }
}
