/**
 * [NEEDS-LIVE-GATEWAY] LIVE PERCEPT RENDERING for the IBKR feed face (kestrel-7o2.7).
 *
 * The owner acceptance, run against a RUNNING IB **PAPER** gateway. It is NOT a unit test and is
 * NEVER exercised by `bun test` (it lives under `src/`, not `*.test.ts`, and only runs when invoked
 * directly with the env gate set), so CI stays green with no gateway.
 *
 * It **PLACES NO ORDERS.** It is market data only — `reqMktData` / `cancelMktData` and nothing else.
 * The narrow client surface the feed speaks through declares no order method at all.
 *
 * What it proves, end to end, on a LIVE tape:
 *   1. the shared TWS transport (7o2.5/.16) connects and hands out ONE client — never a second socket;
 *   2. the 7o2.6 contract layer resolves the venue's OWN definitions (equity + a listed chain slice);
 *   3. the feed face (7o2.7) turns IB's tick stream into the EXISTING BusEvent union;
 *   4. those BusEvents drive the EXISTING canonical state and materialize a Frame;
 *   5. the Frame RENDERS — the agent-facing, token-efficient percept, printed below;
 *   6. `@fair` prints WITH its receipt (model / fit quality / freshness);
 *   7. the staleness watermark prints — including the frozen-re-print fingerprint.
 *
 * Nothing under `src/session/**` or `src/frame/**` is edited: the renderer and the canonical state
 * are IMPORTED and driven.
 *
 * Run (paper only):
 *   KESTREL_IBKR_FEED_SMOKE=1 KESTREL_IBKR_MODE=paper KESTREL_IBKR_PORT=4002 \
 *     KESTREL_IBKR_CLIENT_ID=13 bun run src/adapters/broker/ibkr/feed-smoke.ts
 */

import { describeIbkrConfig, resolveIbkrConfig } from "./config.ts";
import { loadEnvFallback } from "../../../cli/credentials.ts";
import { IbkrTransport } from "./transport.ts";
import { contractClientOf } from "./contract.ts";
import type { IbkrEquityContract } from "./contract.ts";
import { feedClientOf, ibkrFeed, resolveFeedContracts, stalenessGatedProvider, IbkrFeedError } from "./feed.ts";
import type { FeedWatermark } from "./feed.ts";
import { DEFAULT_SURFACE_HALF_WIDTH } from "./surface-window.ts";
import { CanonicalSeriesProvider, CanonicalState, FakeOrgFacts, isUnknown } from "../../../series/index.ts";
import { projectAuthorFrame, machineSummary } from "../../../session/index.ts";
import type { DaySnapshot } from "../../../session/index.ts";
// The ONE canonical renderer (kestrel-wa0j.2) + the shared AuthorFrame → frame/types mappers — the
// live screen is the SAME `renderBriefing` Rendering an in-process agent perceives, never a fork.
import { kernelOf, marketPaneOf } from "../../../session/simulate.ts";
import { renderBriefing } from "../../../frame/render.ts";
import type { BriefingInput } from "../../../frame/types.ts";
import type { BusEvent, BookEvent, SpotEvent } from "../../../bus/index.ts";

/** A publication-safe generic ticker (ARCHITECTURE §7 — nothing here reveals an application). */
const SYMBOL = process.env.KESTREL_IBKR_FEED_SYMBOL ?? "SPY";
/** How many pump cycles to watch the live tape for. */
const CYCLES = Number(process.env.KESTREL_IBKR_FEED_CYCLES ?? "12");
const PUMP_MS = 1_000;
/** How many cycles to watch the UNDERLIER alone for, before a chain slice is chosen — the money must
 * be OBSERVED before a window can be centred on it (kestrel-7o2.19). */
const SPOT_PROBE_CYCLES = 10;
const SPOT_PROBE_MS = 500;

const log = (line: string): void => {
  console.log(`[feed] ${line}`);
};

/** Fail closed, loudly and typed — never a smoke that carries on with a percept it cannot vouch for. */
function feedFailClosed(reason: string): never {
  throw new IbkrFeedError(reason);
}

/** The chain expiry to watch: the caller's, or the venue's own nearest listed one. NEVER guessed off
 * a clock — it is READ from the gateway's chain (7o2.6), which is the only honest source. */
async function pickExpiry(transport: IbkrTransport, conId: number): Promise<string | undefined> {
  const override = process.env.KESTREL_IBKR_FEED_EXPIRY;
  if (override !== undefined && override.trim().length > 0) return override.trim();
  const { resolveOptionChain } = await import("./contract.ts");
  const chain = await resolveOptionChain(
    { symbol: SYMBOL, underlyingConId: conId, tradingClass: SYMBOL },
    { client: contractClientOf(transport), account: transport.config.account, log },
  );
  const soonest = chain.expirations[0];
  log(`chain lists ${chain.expirations.length} expirations, ${chain.strikes.length} strikes — taking the soonest LISTED: ${soonest ?? "(none)"}`);
  return soonest;
}

/**
 * OBSERVE THE MONEY before slicing the chain (kestrel-7o2.19). A strike window is centred on the
 * OBSERVED spot, so the spot must exist first: an underlier-only feed is opened on the SAME shared
 * session, pumped until the venue PRINTS a trade, and closed. No mid is ever synthesized into a spot —
 * if nothing trades, nothing is known, and the smoke fails closed rather than slicing a chain around a
 * price nobody made. (`KESTREL_IBKR_FEED_SPOT` lets an operator pin the centre explicitly — an
 * INJECTED observation, which is exactly what determinism at the edge means.)
 */
async function observeSpot(
  transport: IbkrTransport,
  underlier: IbkrEquityContract,
  sessionDate: string,
): Promise<number | undefined> {
  const pinned = Number(process.env.KESTREL_IBKR_FEED_SPOT ?? "");
  if (Number.isFinite(pinned) && pinned > 0) {
    log(`spot PINNED by the operator: ${pinned} (the window centres on this)`);
    return pinned;
  }
  const probe = ibkrFeed(
    {
      instrument: SYMBOL,
      sessionDate,
      mode: "paper",
      phase: "regular",
      ...(process.env.KESTREL_IBKR_FEED_DELAYED === "1" ? { marketDataType: 3 } : {}),
      ...(transport.config.account === undefined ? {} : { account: transport.config.account }),
    },
    { client: feedClientOf(transport), underlier, now: Date.now, log },
  );
  await probe.open();
  let spot: number | undefined;
  for (let i = 0; i < SPOT_PROBE_CYCLES && spot === undefined; i++) {
    await new Promise((r) => setTimeout(r, SPOT_PROBE_MS));
    transport.pulse();
    probe.pump();
    for (const ev of probe.drain()) {
      if (ev.stream === "TICK" && ev.type === "SPOT") spot = ev.px;
    }
  }
  probe.close();
  log(
    spot === undefined
      ? `the underlier PRINTED NO TRADE in ${SPOT_PROBE_CYCLES * SPOT_PROBE_MS}ms — there is no money to centre a strike window on (a mid is never a spot; fail-closed)`
      : `observed spot: ${spot} (the OBSERVED last trade — the window centres on this, never on the grid's middle)`,
  );
  return spot;
}

/** Build the DaySnapshot the EXISTING Frame renderer consumes — from the feed's own BusEvents folded
 * through the EXISTING canonical state. No session/frame file is edited; both are driven. */
function snapshotOf(state: CanonicalState, events: readonly BusEvent[], nowTs: number): DaySnapshot {
  const spots = events.filter((e): e is SpotEvent => e.stream === "TICK" && e.type === "SPOT");
  const lastBook = [...events].reverse().find((e): e is BookEvent => e.stream === "TICK" && e.type === "BOOK");
  const num = (v: number | symbol): number | null => (isUnknown(v) ? null : (v as number));
  return {
    kind: "keyframe",
    n: 0,
    label: "LIVE",
    nowTs,
    instruments: [SYMBOL],
    phase: "regular",
    spot: num(state.spot),
    priorClose: num(state.priorClose),
    hod: num(state.hod),
    lod: num(state.lod),
    vwap: num(state.vwap),
    // kestrel-wa0j.58: carry the frozen opening range through the live-feed snapshot too, consistent with
    // the sim driver — the canonical state computes it, the projection must not drop it.
    orHigh: num(state.orHigh),
    orLow: num(state.orLow),
    chain:
      lastBook === undefined
        ? null
        : {
            underlierPx: lastBook.underlier_px,
            ...(lastBook.expiry === undefined ? {} : { expiry: lastBook.expiry }),
            legs: lastBook.legs,
          },
    tape: spots.map((s) => ({ ts: s.ts, px: s.px })),
    plans: [],
    resting: [],
    fills: [],
    committedUsd: 0,
    rUsd: 1_000,
  };
}

function printWatermark(wm: FeedWatermark): void {
  console.log(`\n── STALENESS WATERMARK ─────────────────────────────────────────────`);
  console.log(`health: ${wm.health}${wm.reason === null ? "" : `  reason: ${wm.reason}`}`);
  console.log(`staleAfterMs: ${wm.staleAfterMs}   seq: ${wm.seq}   underlierStale: ${wm.underlierStale}`);
  for (const s of wm.series) {
    console.log(
      `  ${s.key.padEnd(12)} kind=${s.kind.padEnd(6)} asOfSeq=${String(s.asOfSeq).padStart(4)} ageMs=${String(s.ageMs).padStart(6)} reprints=${String(s.reprints).padStart(3)} stale=${s.stale} dark=${s.dark} failed=${s.failed}`,
    );
  }
}

async function runFeedSmoke(): Promise<void> {
  const config = resolveIbkrConfig();
  log(`resolving transport → ${describeIbkrConfig(config)} (MARKET DATA ONLY — PLACES NO ORDERS)`);
  if (config.mode !== "paper") {
    throw new Error("feed smoke refuses any mode but `paper` (fail-closed) — set KESTREL_IBKR_MODE=paper");
  }

  const sessionDate = new Date().toISOString().slice(0, 10);
  const transport = new IbkrTransport(config, { heartbeatIntervalMs: 5_000, heartbeatStalenessMs: 20_000, log });
  const status = await transport.connect();
  log(`connected: account=${status.account} (the ONE shared session — the feed multiplexes it)`);

  // (2) The 7o2.6 contract layer — the gateway's OWN definitions, never hand-rolled.
  const contractDeps = { client: contractClientOf(transport), account: config.account, log };
  const { underlier: resolvedUnderlier } = await resolveFeedContracts({ symbol: SYMBOL }, contractDeps);
  // This script drives the EQUITY path (SYMBOL is a generic ETF and no `underlierSecType` is stated,
  // so `resolveUnderlier` asked for `STK`). The narrowing is a fail-closed assertion of that, not a
  // cast: if the union ever widens under it, this refuses loudly rather than reading fields off the
  // wrong asset class.
  if (resolvedUnderlier.kind !== "equity") {
    feedFailClosed(`${SYMBOL} resolved to a ${resolvedUnderlier.kind} contract, but this smoke script drives the equity/ETF path — refusing to probe an identity it did not ask for`);
  }
  const equityOnly = resolvedUnderlier;
  log(`resolved equity: conId=${equityOnly.conId} ${equityOnly.localSymbol}@${equityOnly.exchange}/${equityOnly.primaryExchange ?? "-"}`);

  const expiry = await pickExpiry(transport, equityOnly.conId);

  // THE MONEY FIRST (kestrel-7o2.19). The window is centred on the OBSERVED spot — never on the middle
  // of whatever grid the venue happened to answer with, which is how a percept ends up showing five
  // deep-ITM calls and five 1-cent puts while the money sits eleven points away.
  const spot = expiry === undefined ? undefined : await observeSpot(transport, equityOnly, sessionDate);
  if (expiry !== undefined && spot === undefined) {
    feedFailClosed(
      `no observed spot for ${SYMBOL} — a chain slice with no money at its centre is the kestrel-7o2.19 defect. Pin one with KESTREL_IBKR_FEED_SPOT, or run while the tape is printing.`,
    );
  }

  const { underlier, legs } = await resolveFeedContracts(
    {
      symbol: SYMBOL,
      ...(expiry === undefined ? {} : { expiry }),
      ...(spot === undefined ? {} : { spot }),
      halfWidth: DEFAULT_SURFACE_HALF_WIDTH, // 4 each side ⇒ 9 strikes ⇒ 18 two-sided legs (the order path's window)
      tradingClass: SYMBOL,
    },
    contractDeps,
  );
  log(`resolved ${legs.length} option legs @ ${expiry ?? "(equity-only)"}: ${legs.map((l) => `${l.strike}${l.right}`).join(" ")}`);
  if (spot !== undefined && legs.length > 0) {
    const strikes = [...new Set(legs.map((l) => l.strike))].sort((a, b) => a - b);
    const below = strikes.filter((s) => s <= spot).length;
    const above = strikes.filter((s) => s >= spot).length;
    log(
      `the window BRACKETS the money: ${below} listed strike(s) at/below ${spot}, ${above} at/above — ATM-centred, off the venue's LISTED grid for ${expiry ?? "-"} (never the union)`,
    );
  }

  // (3) The FEED FACE over the SAME shared client — never a second market-data connection.
  const feed = ibkrFeed(
    {
      instrument: SYMBOL,
      sessionDate,
      mode: "paper",
      ...(expiry === undefined ? {} : { expiry }),
      tauYears: 1 / 365, // INJECTED τ — the smoke's own input, never read off the runtime path
      staleAfterMs: 8_000,
      phase: "regular",
      ...(process.env.KESTREL_IBKR_FEED_DELAYED === "1" ? { marketDataType: 3 } : {}),
      ...(config.account === undefined ? {} : { account: config.account }),
    },
    { client: feedClientOf(transport), underlier, legs, now: Date.now, log },
  );

  const opened = await feed.open();
  log(`subscribed: 1 underlier + ${opened.legs.length} legs; ${opened.failed.length} refused`);
  for (const f of opened.failed) log(`REFUSED (surfaced to the requester, session unharmed): ${f.message}`);

  // (4) Drive the EXISTING canonical state with the feed's OWN BusEvents (the real ingress).
  const state = new CanonicalState({ instrument: SYMBOL });
  const all: BusEvent[] = [];
  for (let i = 0; i < CYCLES; i++) {
    await new Promise((r) => setTimeout(r, PUMP_MS));
    transport.pulse();
    feed.pump();
    for (const ev of feed.drain()) {
      state.applyEvent(ev);
      all.push(ev);
    }
  }

  const counts = new Map<string, number>();
  for (const e of all) {
    const k = e.stream === "TICK" ? `TICK/${e.type}` : e.stream;
    counts.set(k, (counts.get(k) ?? 0) + 1);
  }
  log(`produced ${all.length} BusEvents: ${[...counts].map(([k, v]) => `${k}=${v}`).join(" ")}`);

  // (5) LIVE PERCEPT RENDERING — the agent-facing Frame, materialized and rendered through THE
  // canonical `renderBriefing` (one renderer, kestrel-wa0j.2) over the same date-blind projection.
  const snap = snapshotOf(state, all, Date.now());
  const openTs = all[0]?.ts ?? Date.now();
  const frame = projectAuthorFrame(snap, openTs, sessionDate);
  const briefing: BriefingInput = {
    timeToCloseMin: null, // a live smoke pins no session horizon — rendered `T-— to close`, never a guess
    phase: "regular",
    instruments: [{ symbol: SYMBOL, assetClass: "equity" }],
    market: marketPaneOf(frame, SYMBOL),
    kernel: kernelOf(frame, SYMBOL),
  };
  console.log(`\n══ LIVE PERCEPT — RENDERED FRAME ═══════════════════════════════════`);
  console.log(renderBriefing(briefing));
  console.log(`── machine summary (the token-efficient agent read) ─────────────────`);
  console.log(machineSummary(frame));

  // (6) @fair WITH its receipt — INCLUDING the ATM-coverage receipt (kestrel-7o2.19): a surface with
  // no liquid strike near the money does not get to vouch for an at-the-money valuation.
  console.log(`── @fair (off IB's two-sided quote, with its RECEIPT) ───────────────`);
  for (const leg of opened.legs.slice(0, 6)) {
    for (const side of ["buy", "sell"] as const) {
      const f = feed.fair({ strike: leg.strike, right: leg.right, side });
      const receipt =
        f.receipt === null
          ? "receipt=—"
          : `receipt={model=${f.receipt.model} nLiquid=${f.receipt.nLiquid} iv=${f.receipt.ivAtStrike.toFixed(4)} asof=${f.receipt.asof ?? "—"}}`;
      const value = f.value === null ? `fair=NULL  fallback={px=${f.fallback?.px ?? "—"} "${f.fallback?.annotation ?? ""}"}` : `fair=${f.value.toFixed(4)}`;
      const c = f.coverage;
      const cov = `atm={covers=${c.supportsAtm} brackets=${c.bracketsSpot} nNear=${c.nNearMoney}/${c.nLiquid} d=${c.distanceUsd === null ? "—" : c.distanceUsd.toFixed(2)}}`;
      console.log(
        `  ${String(leg.strike).padStart(6)}${leg.right} ${side.padEnd(4)} ${value.padEnd(52)} ${receipt}  ${cov}  health={${f.health.spreadRegime} spread=${f.health.spread ?? "—"}}`,
      );
      if (f.taint !== null) console.log(`         TAINTED: ${f.taint}`);
    }
  }

  // (7) The watermark — and the taint it puts on canonical state.
  printWatermark(feed.watermark());
  const gated = stalenessGatedProvider(new CanonicalSeriesProvider(state, new FakeOrgFacts()), feed, { log });
  const spotRead = gated.resolve({ kind: "series", segments: [{ name: "spot" }] }, Date.now());
  console.log(
    `\ngated read of \`spot\`: ${isUnknown(spotRead) ? "UNKNOWN (the feed is DEGRADED — dependents tainted, plans cannot fire)" : String(spotRead)}`,
  );

  feed.close();
  transport.disconnect();
  console.log(`\n[feed] done — subscriptions closed, transport closed. NO ORDERS WERE PLACED.`);
}

if (import.meta.main) {
  loadEnvFallback(); // KESTREL_IBKR_* identifiers from the shared secrets home (OSS-ADR-0054)
  if (process.env.KESTREL_IBKR_FEED_SMOKE !== "1") {
    console.log("[feed] skipped: set KESTREL_IBKR_FEED_SMOKE=1 (and run a paper IB Gateway) to exercise this. Places no orders.");
  } else {
    runFeedSmoke().catch((e: unknown) => {
      const err = e as Error;
      console.error(`[feed] FAILED (fail-closed): ${err.name}: ${err.message}`);
      process.exitCode = 1;
    });
  }
}

export { runFeedSmoke };
