/**
 * # The IBKR PAPER **EQUITY ORDER** (kestrel-7o2.8) — env-gated; DRY-RUN by default
 *
 * The OPTION ticket is BLOCKED: `@fair` for options is corrupted (kestrel-ku99 — `buildSurface`
 * averages a call-IV and a put-IV across an UNCHECKED put-call-parity assumption, and live IB data
 * violates parity, producing an `@fair` 28% ABOVE the ask on an ATM leg). We do not trade on a price
 * we know is a lie.
 *
 * The EQUITY path has NO vol surface and NO parity dependency: `@fair(spot)` is `exec-fair-quote-v1`
 * off a clean, tight, two-sided quote. That anchor is honest, so THIS is the path we validate live.
 *
 * It proves the whole order chain end-to-end on real infrastructure:
 *   resolve contract → two-sided quote → @fair(spot) + receipt → floor-snapped limit
 *   → the FOUR L0 WALLS (kill-switch, order-size, position/never-naked, notional)
 *   → placeOrder → the venue's own ORDER callbacks → the reconciliation ledger → broker-truth positions.
 *
 * ## THE HARD RULE
 * The default path TRANSMITS NOTHING. `preflight()` runs the full guard chain WITHOUT `placeOrder`.
 * Transmission happens ONLY under an explicit human `KESTREL_IBKR_TRANSMIT=1`.
 *
 * ## Also: the DATA PROBE (kestrel-ku99 root cause)
 * It logs the RAW IB tick field ids that arrive. 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 — which is exactly how put-call parity breaks and how the vol
 * surface got poisoned. This prints the evidence rather than guessing.
 *
 * ## Run it
 * ```bash
 * # DRY RUN (default — transmits nothing):
 * KESTREL_IBKR_EQUITY=1 KESTREL_IBKR_MODE=paper KESTREL_IBKR_PORT=4002 KESTREL_IBKR_CLIENT_ID=15 \
 *   bun run src/adapters/broker/ibkr/equity-order.ts
 *
 * # TRANSMIT (a separate, deliberate, HUMAN act):
 * KESTREL_IBKR_TRANSMIT=1 KESTREL_IBKR_EQUITY=1 ... bun run src/adapters/broker/ibkr/equity-order.ts
 * ```
 */

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

import { IbkrTransport } from "./transport.ts";
import { resolveIbkrConfig, describeIbkrConfig } from "./config.ts";
import { loadEnvFallback } from "../../../cli/credentials.ts";
import { resolveContract, contractClientOf } from "./contract.ts";
import type { IbkrEquityContract } from "./contract.ts";
import { ibkrBroker, contractBook, orderClientOf } from "./broker.ts";
import { positionKeyOf } from "../../broker.ts";
import { reqMktDataQuotes, priceAnchorFrom } from "./order-dryrun.ts";
import { executionFairSpot } from "../../../fair/index.ts";
import type { OrderIntent } from "../../../engine/index.ts";
import type { RiskLimits } from "../../broker.ts";

/** A BUY never bids ABOVE fair (engine/pricing.ts's own direction). */
const TICK_SIZE = 0.01;
const floorSnap = (px: number, tick: number): number => Math.floor(px / tick + 1e-9) * tick;

/** The L0 pre-transmit ceilings. Deliberately tiny — this is a proof, not a position. */
const LIMITS: RiskLimits = { maxOrderQty: 1, maxPositionQty: 1, maxNotionalUsd: 2_000 };

/** IB tick field ids: REAL-TIME vs their DELAYED mirrors. The whole ku99 question in six numbers. */
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)",
};

function intentOf(over: Partial<OrderIntent> & Pick<OrderIntent, "ref" | "side" | "qty" | "px">): OrderIntent {
  return {
    plan: "equity-proof",
    plan_instance: "equity-proof#1",
    role: "entry",
    instrument: "SPY",
    sourceAnnotation: "equity-proof",
    ...over,
  } as OrderIntent;
}

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) => {
    if (next === undefined || id > next) next = id;
  }) as never);
  client.reqIds(1);
  return () => {
    if (next === undefined) {
      throw new Error("the gateway issued no nextValidId — refusing to invent an order id. Fail-closed.");
    }
    const id = next;
    next += 1;
    return id;
  };
}

const ibEquityQuery = (e: IbkrEquityContract): Contract => ({
  conId: e.conId,
  symbol: e.symbol,
  secType: SecType.STK,
  exchange: e.exchange,
  currency: e.currency,
});

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

/** Subscribe raw and record WHICH tick ids the venue actually sends. Read-only. Places no orders. */
function probeTickIds(transport: IbkrTransport, contract: Contract, label: string, ms = 4_000): Promise<void> {
  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 reqId = 9000 + Math.floor(label.length); // deterministic-ish, unique per label
  const seen = new Map<number, number>();
  client.on(EventName.tickPrice, ((id: number, field: number, price: number) => {
    if (id !== reqId) return;
    if (price === undefined || price === null || price < 0) return;
    seen.set(field, price);
  }) as never);
  client.reqMktData(reqId, contract, "", false, false, []);
  return settle(ms).then(() => {
    const rows = [...seen.entries()]
      .filter(([f]) => TICK_NAMES[f] !== undefined)
      .map(([f, p]) => `      ${String(f).padStart(2)} ${TICK_NAMES[f]!.padEnd(18)} = ${p}`);
    const delayed = [...seen.keys()].some((f) => f >= 66 && f <= 68);
    const realtime = [...seen.keys()].some((f) => f >= 1 && f <= 4);
    console.log(`  [probe] ${label}:`);
    console.log(rows.length ? rows.join("\n") : "      (no price ticks)");
    console.log(`      => ${realtime ? "REAL-TIME" : ""}${realtime && delayed ? " + " : ""}${delayed ? "DELAYED" : ""}${!realtime && !delayed ? "NEITHER" : ""}`);
  });
}

async function run(): Promise<void> {
  const transmit = process.env["KESTREL_IBKR_TRANSMIT"] === "1";
  const cfg = resolveIbkrConfig();
  console.log(`\n[equity] ${describeIbkrConfig(cfg)}  ${transmit ? "*** TRANSMIT ARMED ***" : "(DRY RUN — transmits nothing)"}\n`);

  const transport = new IbkrTransport(cfg, { log: (l) => console.log(`  [t] ${l}`) });
  const status = await transport.connect();
  console.log(`  connected: account=${status.account}\n`);

  try {
    const cdeps = { client: contractClientOf(transport), log: (l: string) => console.log(`  [contract] ${l}`) };
    const quotes = reqMktDataQuotes(transport);
    const nowTs = status.serverTime ?? Date.now();

    // ── 1. Resolve the SPY equity contract (the venue's own definition).
    const equity = (await resolveContract(intentOf({ ref: "eq-resolve", side: "buy", qty: 1, px: 0 }), cdeps)) as IbkrEquityContract;
    console.log(`\n  SPY equity: conId=${equity.conId} ${equity.exchange}/${equity.primaryExchange ?? "?"} ${equity.currency}\n`);

    // ── 2. DATA PROBE (ku99 root cause): which tick ids does the venue actually send?
    console.log("  ── DATA PROBE — real-time vs delayed (the ku99 parity question) ──");
    await probeTickIds(transport, ibEquityQuery(equity), "SPY equity");

    // ── 3. The two-sided quote → @fair(spot). exec-fair-quote-v1. No surface, no parity dependency.
    const q = await quotes.quote(ibEquityQuery(equity));
    // A PRICE ANCHOR never accepts delayed/frozen input (kestrel-7o2.8, blocker 4a). The venue
    // SUBSTITUTES stale data rather than refusing, so the substitution is caught here — before a
    // single number reaches @fair — and the anchor is UNRESOLVABLE rather than quietly wrong.
    if (q.freshness !== "live") {
      throw new Error(
        `the PRICE ANCHOR requires LIVE market data and the venue served ${q.freshness.toUpperCase()} — delayed/frozen data is a HEALTH SIGNAL, never a price. @fair(spot) is UNRESOLVABLE. Fail-closed; NOTHING transmitted.`,
      );
    }
    const fair = executionFairSpot({ bid: q.bid, ask: q.ask, asof: nowTs });
    if (fair === null) {
      throw new Error(
        `SPY's book is one-sided/dark/crossed (bid=${q.bid} ask=${q.ask}) — @fair(spot) is UNRESOLVABLE and a mid is NEVER substituted. Fail-closed.`,
      );
    }
    const limitPx = floorSnap(fair.value, TICK_SIZE);

    // ── 4. The order face + the L0 walls — INCLUDING the fair-vs-book bound, which now lives in the
    //     MODULE (broker.ts wall 6), not in this script. It was a script-local check here, which means
    //     every OTHER order path crossed no such wall at all: the option dry-run printed an
    //     authorizable ticket at @fair 0.9229 against a 0.73 offer. A bound only one caller performs is
    //     not a bound. preflight() below refuses on a fair outside the observed book, for everyone.
    const orderClient = orderClientOf(transport);
    const broker = ibkrBroker(cfg, {
      client: orderClient,
      contracts: contractBook([equity]),
      limits: LIMITS,
      intrinsicOf: () => undefined, // equity has no strike/right; the intrinsic floor is a SELL doctrine
      // Key the anchor by the LEG, returning it ONLY for the equity we actually priced — undefined for
      // anything else. A priceAnchorOf that IGNORED its leg and returned the same anchor for every key
      // is exactly the shape of bug the price-anchor wall exists to prevent (kestrel-7o2.8 minor).
      priceAnchorOf: (leg) =>
        positionKeyOf(leg) === positionKeyOf({ instrument: equity.symbol }) ? priceAnchorFrom(q, fair.value) : undefined,
      expectedPositions: () => ({}),
      nextOrderId: mintOrderId(orderClient),
      drain: () => {},
    });

    // The gateway's `nextValidId` is ASYNC: the listener above registers after connect (so it misses
    // IB's automatic connect-time broadcast) and `reqIds(1)` must round-trip. Wait for it — the broker
    // will (correctly) refuse to invent an order id, so give the venue time to issue one.
    await settle(2_500);

    const intent = intentOf({ ref: "eq-1", side: "buy", qty: 1, px: limitPx, sourceAnnotation: `fair=${fair.receipt.model}` });
    const ticket = broker.preflight(intent); // ← the guard chain. TRANSMITS NOTHING.

    // ── 6. The ticket.
    console.log(`
╔══════════════════════════════════════════════════════════════════════════════╗
║  ORDER TICKET — kestrel-7o2.8 · IBKR PAPER · EQUITY                           ║
║  ${transmit ? "*** TRANSMIT ARMED — a human authorized this ***                          " : "*** DRY RUN — NO ORDER TRANSMITTED ***                                  "}║
╚══════════════════════════════════════════════════════════════════════════════╝

CONTRACT (the gateway's OWN definition)
  symbol / conId         ${equity.symbol}  ${equity.conId}
  secType                STK (equity — no vol surface, no parity dependency)
  exchange / currency    ${equity.exchange} / ${equity.currency}
  multiplier             x${ticket.multiplier}

ORDER
  side / qty             ${ticket.side.toUpperCase()} ${ticket.qty}
  type                   LIMIT (DAY)
  limit price            $${ticket.limitPx.toFixed(2)}

PRICE — @fair is the ANCHOR (the mid is NEVER a price and NEVER a value)
  @fair(spot)            $${fair.value.toFixed(4)}
    receipt              ${fair.receipt.model}(bid=${fair.receipt.bid},ask=${fair.receipt.ask})
  limit = floor-snap     $${limitPx.toFixed(2)}   (a BUY never bids ABOVE fair)

THE OBSERVED BOOK — a health signal that also BOUNDS the anchor (broker.ts wall 6)
  bid / ask              $${q.bid ?? "-"} / $${q.ask ?? "-"}   [${q.freshness.toUpperCase()}]
  PRICE ANCHOR           @fair ${ticket.anchor.fair.toFixed(4)} INSIDE the LIVE book ${ticket.anchor.bid} / ${ticket.anchor.ask}  ✓
  FAIR-VS-BOOK GUARD     limit ${limitPx.toFixed(2)} <= ask ${q.ask ?? "-"}  ✓ (enforced in the MODULE, for every order path)

RISK — DEFINED-RISK LONG
  notional               $${ticket.notionalUsd.toFixed(2)}
  budget / R             $${LIMITS.maxNotionalUsd.toFixed(2)}
  position before/after  0 -> ${ticket.qty}   (never negative — never-naked)

L0 CLAMP (7o2.9 — checked BEFORE the wire)
  maxOrderQty            ${ticket.qty} <= ${LIMITS.maxOrderQty}
  maxPositionQty         ${ticket.qty} <= ${LIMITS.maxPositionQty}
  maxNotionalUsd         ${ticket.notionalUsd.toFixed(2)} <= ${LIMITS.maxNotionalUsd}
  VERDICT                CLEARED
`);

    // ── 7. THE WALL.
    if (!transmit) {
      console.log("KESTREL_IBKR_TRANSMIT is NOT set. placeOrder was NEVER called. This is the default.\n");
      return;
    }

    console.log("KESTREL_IBKR_TRANSMIT=1 — a human authorized the ticket above. TRANSMITTING...\n");
    broker.submit(intent);
    // The order rests PASSIVELY at fair (a BUY never bids above fair), so give the venue a real
    // window to fill it rather than declaring failure on a 6s stopwatch.
    for (let i = 0; i < 10; i++) {
      await settle(3_000);
      const rec = broker.ledger()[0];
      if (rec !== undefined) {
        console.log(`  t+${(i + 1) * 3}s  phase=${rec.phase} filled=${rec.filledQty}/${rec.submittedQty}`);
        if (rec.phase === "filled" || rec.phase === "cancelled" || rec.phase === "rejected") break;
      }
    }

    console.log("  ── THE VENUE'S OWN REPORT (broker truth, never our expectation) ──");
    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}`,
      );
    }
    console.log(`  positions (broker truth): ${JSON.stringify(broker.positions())}`);
    const trip = broker.reconcile();
    console.log(`  reconcile: ${trip === undefined || trip === null ? "OK — engine and broker agree" : JSON.stringify(trip)}`);
    console.log(`  kill-switch: tripped=${broker.killSwitch.tripped}${broker.killSwitch.reason ? ` (${broker.killSwitch.reason})` : ""}\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_EQUITY"] !== "1") {
    console.log("[equity] skipped: set KESTREL_IBKR_EQUITY=1 (and run a paper IB Gateway).");
  } else {
    run().catch((e: unknown) => {
      const err = e as Error;
      console.error(`\n[equity] FAILED (fail-closed): ${err.name}: ${err.message}\n`);
      process.exitCode = 1;
    });
  }
}

export { run };
