/**
 * # DATA PROBE (kestrel-ku99) — is the OPTION feed delayed while the EQUITY is real-time?
 *
 * READ-ONLY. Subscribes to the SPY equity and to SPY option legs, and reports WHICH IB tick field ids
 * the venue actually sends. IB uses 1/2/4 for REAL-TIME bid/ask/last and 66/67/68 for their DELAYED
 * mirrors. If the option legs arrive DELAYED while the equity is REAL-TIME, the two are snapshots of
 * DIFFERENT MOMENTS — and that is exactly how put-call parity breaks and how buildSurface got poisoned
 * (an @fair 28% above the ask on an ATM leg).
 *
 * It also computes the parity residual directly from the observed legs:
 *   at r = 0,  C - P  must equal  S - K.
 * A non-zero residual is the smoking gun, and solving it for S gives the OPTION-IMPLIED spot, which we
 * compare against the EQUITY-quoted spot.
 *
 * PLACES NO ORDERS.
 *
 * KESTREL_IBKR_PROBE=1 KESTREL_IBKR_MODE=paper KESTREL_IBKR_PORT=4002 KESTREL_IBKR_CLIENT_ID=17 \
 *   bun run src/adapters/broker/ibkr/tick-probe.ts
 */

import { EventName, SecType, OptionType, 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 { IbkrEquityContract } from "./contract.ts";
import type { OrderIntent } from "../../../engine/index.ts";

const TICK_NAMES: Record<number, string> = {
  1: "BID  real-time",
  2: "ASK  real-time",
  4: "LAST real-time",
  66: "BID  *DELAYED*",
  67: "ASK  *DELAYED*",
  68: "LAST *DELAYED*",
};

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

interface Probe {
  readonly label: string;
  readonly ticks: Map<number, number>;
}

function subscribe(transport: IbkrTransport, reqId: number, contract: Contract, label: string): Probe {
  const client = transport.client() as unknown as {
    on(e: EventName, l: (...a: never[]) => void): unknown;
    reqMktData(id: number, c: Contract, g: string, snap: boolean, regSnap: boolean, opts: unknown[]): unknown;
  };
  const ticks = new Map<number, number>();
  client.on(EventName.tickPrice, ((id: number, field: number, price: number) => {
    if (id !== reqId) return;
    if (typeof price !== "number" || price < 0) return;
    ticks.set(field, price);
  }) as never);
  client.reqMktData(reqId, contract, "", false, false, []);
  return { label, ticks };
}

function report(p: Probe): { delayed: boolean; realtime: boolean; bid: number | null; ask: number | null } {
  const rows = [...p.ticks.entries()].filter(([f]) => TICK_NAMES[f] !== undefined).sort((a, b) => a[0] - b[0]);
  console.log(`  ${p.label}`);
  for (const [f, px] of rows) console.log(`      ${String(f).padStart(2)}  ${TICK_NAMES[f]}  = ${px}`);
  const realtime = rows.some(([f]) => f <= 4);
  const delayed = rows.some(([f]) => f >= 66);
  console.log(`      => ${realtime ? "REAL-TIME" : ""}${realtime && delayed ? " + " : ""}${delayed ? "*** DELAYED ***" : ""}${!realtime && !delayed ? "no price ticks" : ""}\n`);
  const bid = p.ticks.get(1) ?? p.ticks.get(66) ?? null;
  const ask = p.ticks.get(2) ?? p.ticks.get(67) ?? null;
  return { delayed, realtime, bid, ask };
}

async function run(): Promise<void> {
  const cfg = resolveIbkrConfig();
  console.log(`\n[probe] ${describeIbkrConfig(cfg)} — READ-ONLY, PLACES NO ORDERS\n`);
  const transport = new IbkrTransport(cfg, { log: () => {} });
  const status = await transport.connect();
  console.log(`  connected: account=${status.account}\n`);

  try {
    const cdeps = { client: contractClientOf(transport), log: () => {} };
    const base = { plan: "probe", plan_instance: "probe#1", role: "entry", instrument: "SPY", sourceAnnotation: "probe" };
    const equity = (await resolveContract({ ...base, ref: "p-eq", side: "buy", qty: 1, px: 0 } as OrderIntent, cdeps)) as IbkrEquityContract;

    const chain = await resolveOptionChain({ symbol: "SPY", underlyingConId: equity.conId, tradingClass: "SPY" }, cdeps);
    const expiry = [...chain.expirations].sort()[0]!;
    const listed = await listOptionStrikes({ symbol: "SPY", expiry, right: "C", tradingClass: "SPY" }, cdeps);

    const eqQ: Contract = { conId: equity.conId, symbol: "SPY", secType: SecType.STK, exchange: equity.exchange, currency: equity.currency };
    const optQ = (strike: number, right: "C" | "P"): Contract => ({
      symbol: "SPY",
      secType: SecType.OPT,
      exchange: "SMART",
      currency: "USD",
      lastTradeDateOrContractMonth: expiry,
      strike,
      right: right === "C" ? OptionType.Call : OptionType.Put,
      tradingClass: "SPY",
      multiplier: 100,
    });

    // Subscribe equity first so we can pick the ATM strike from a real spot.
    const eqP = subscribe(transport, 7001, eqQ, "SPY equity (STK)");
    await settle(4_000);
    const eq = report(eqP);
    const spot = eq.bid !== null && eq.ask !== null ? (eq.bid + eq.ask) / 2 : null;
    if (spot === null) throw new Error("no two-sided equity quote — cannot pick an ATM strike");

    const atm = listed.reduce((best, s) => (Math.abs(s - spot) < Math.abs(best - spot) ? s : best), listed[0]!);
    console.log(`  spot (from equity quote) = ${spot.toFixed(2)}   ATM strike = ${atm}   expiry = ${expiry}\n`);

    const cP = subscribe(transport, 7002, optQ(atm, "C"), `SPY ${expiry} ${atm}C (OPT)`);
    const pP = subscribe(transport, 7003, optQ(atm, "P"), `SPY ${expiry} ${atm}P (OPT)`);
    await settle(6_000);
    const c = report(cP);
    const p = report(pP);

    // ── THE PARITY TEST. At r = 0:  C - P  must equal  S - K.
    console.log("  ── PUT-CALL PARITY (the ku99 smoking gun) ──");
    if (c.bid === null || c.ask === null || p.bid === null || p.ask === null) {
      console.log("      one of the legs is one-sided/dark — cannot test parity.\n");
    } else {
      const cMid = (c.bid + c.ask) / 2;
      const pMid = (p.bid + p.ask) / 2;
      const lhs = cMid - pMid;
      const rhs = spot - atm;
      const residual = lhs - rhs;
      const impliedSpot = atm + lhs;
      console.log(`      C mid = ${cMid.toFixed(4)}   P mid = ${pMid.toFixed(4)}   K = ${atm}`);
      console.log(`      C - P            = ${lhs.toFixed(4)}`);
      console.log(`      S - K            = ${rhs.toFixed(4)}   (S = ${spot.toFixed(4)} from the EQUITY quote)`);
      console.log(`      PARITY RESIDUAL  = ${residual.toFixed(4)}   ${Math.abs(residual) > 0.05 ? "*** VIOLATED ***" : "(ok)"}`);
      console.log(`      spot implied by the OPTION legs = ${impliedSpot.toFixed(4)}`);
      console.log(`      spot quoted by the EQUITY       = ${spot.toFixed(4)}`);
      console.log(`      DISAGREEMENT                    = ${(spot - impliedSpot).toFixed(4)}\n`);
    }

    const mixed = eq.realtime && (c.delayed || p.delayed);
    console.log("  ── VERDICT ──");
    console.log(`      equity: ${eq.realtime ? "REAL-TIME" : "delayed"}   options: ${c.delayed || p.delayed ? "*** DELAYED ***" : "real-time"}`);
    console.log(
      mixed
        ? "      MIXED ENTITLEMENTS — the equity and the option legs are snapshots of DIFFERENT MOMENTS.\n" +
            "      That is the ku99 root cause: buildSurface averaged a call-IV and a put-IV across quotes\n" +
            "      that do not describe the same instant, and put-call parity broke.\n"
        : "      Entitlements match — the parity break (if any) is NOT a delayed/real-time mismatch.\n",
    );
  } finally {
    transport.disconnect();
  }
}

if (import.meta.main) {
  loadEnvFallback(); // KESTREL_IBKR_* identifiers from the shared secrets home (OSS-ADR-0054)
  if (process.env["KESTREL_IBKR_PROBE"] !== "1") console.log("[probe] set KESTREL_IBKR_PROBE=1 to run.");
  else run().catch((e: unknown) => { console.error(`[probe] FAILED: ${(e as Error).message}`); process.exitCode = 1; });
}
