/**
 * [NEEDS-LIVE-GATEWAY] Owner acceptance for the IB CONTRACT-RESOLUTION layer (kestrel-7o2.6).
 *
 * This is the acceptance the OWNER runs 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 issues READ-ONLY definition lookups only (`reqContractDetails`,
 * `reqSecDefOptParams`) over the transport's ONE shared socket, and proves, against the REAL venue:
 *   1. an EQUITY contract resolves for SPY and for QQQ (generic tickers — ARCHITECTURE §7);
 *   2. a NEAR-DATED SPY option resolves — its strike/expiry picked from the LIVE chain via the
 *      definition lookup, printing the gateway's own multiplier / exchange / currency / OCC symbol;
 *   3. a deliberately BOGUS strike fails CLOSED with the typed `IbkrContractError` — the real
 *      gateway says "no security definition" and the layer refuses rather than guessing a neighbour;
 *   4. an AMBIGUOUS request (an equity query with the routing pins removed) refuses rather than
 *      picking one of several listings — the never-a-pick invariant, against the real venue.
 *
 * Run (paper only; use a UNIQUE clientId — collisions break IB API connections):
 *   KESTREL_IBKR_CONTRACT_SMOKE=1 KESTREL_IBKR_MODE=paper KESTREL_IBKR_PORT=4002 \
 *     KESTREL_IBKR_CLIENT_ID=12 bun run src/adapters/broker/ibkr/contract-smoke.ts
 */

import { contractClientOf, listOptionStrikes, resolveContract, resolveOptionChain } from "./contract.ts";
import type { IbkrContract, IbkrContractDeps } from "./contract.ts";
import { describeIbkrConfig, resolveIbkrConfig } from "./config.ts";
import { loadEnvFallback } from "../../../cli/credentials.ts";
import { IbkrContractError } from "./errors.ts";
import { IbkrTransport } from "./transport.ts";
import type { OrderIntent } from "../../../engine/index.ts";
import type { Right } from "../../../bus/index.ts";

/** A minimal resolved intent — the shape the engine hands a venue face. `px` is present (the engine
 * resolved it) and is IGNORED end to end by the contract layer: a transmitter never re-prices. */
function intentOf(instrument: string, strike?: number, right?: Right): OrderIntent {
  return {
    ref: `smoke-${instrument}-${strike ?? "spot"}${right ?? ""}`,
    plan: "smoke",
    plan_instance: "smoke#1",
    role: "entry",
    instrument,
    side: "buy",
    qty: 1,
    ...(strike === undefined ? {} : { strike }),
    ...(right === undefined ? {} : { right }),
    px: 1.23, // never read by this layer — proof is the resolved contract carries no price at all
    sourceAnnotation: "@ask",
  };
}

function show(label: string, c: IbkrContract): void {
  const common = `conId=${c.conId} symbol=${c.symbol} exchange=${c.exchange} currency=${c.currency} localSymbol="${c.localSymbol}" tradingClass=${c.tradingClass ?? "-"} multiplier=${c.multiplier}`;
  const extra =
    c.kind === "option"
      ? ` expiry=${c.expiry} strike=${c.strike} right=${c.right}`
      : ` primaryExchange=${c.primaryExchange ?? "-"}`;
  console.log(`[contract-smoke] ${label}: ${common}${extra}`);
  console.log(
    `[contract-smoke] ${label}: price fields on the resolved contract = ${JSON.stringify(
      Object.keys(c).filter((k) => ["px", "price", "limit", "bid", "ask", "mid"].includes(k)),
    )} (must be [])`,
  );
}

/** Run `fn`, REQUIRE a typed refusal, and print it. A resolution here would be the bug: it would mean
 * the layer guessed a contract for an identity the venue does not list. */
async function mustRefuse(label: string, fn: () => Promise<IbkrContract>): Promise<void> {
  try {
    const c = await fn();
    throw new Error(
      `[contract-smoke] FAIL: ${label} RESOLVED to conId=${c.conId} — fail-closed violated (a guessed contract)`,
    );
  } catch (e) {
    if (e instanceof IbkrContractError) {
      console.log(`[contract-smoke] ${label}: REFUSED as required — [${e.failure}] ${e.reason}`);
      return;
    }
    throw e;
  }
}

async function runContractSmoke(): Promise<void> {
  const config = resolveIbkrConfig();
  console.log(
    `[contract-smoke] resolving over ${describeIbkrConfig(config)} — READ-ONLY definition lookups, PLACES NO ORDERS`,
  );
  if (config.mode !== "paper") {
    throw new Error("[contract-smoke] refuses any mode but paper (fail-closed)");
  }

  const transport = new IbkrTransport(config, {
    heartbeatIntervalMs: 5_000,
    log: (line) => console.log(`[contract-smoke] transport: ${line}`),
  });
  await transport.connect();
  console.log(`[contract-smoke] connected. account=${transport.status().account} (redacted)`);

  // ONE shared socket — the contract layer re-views the transport's single guarded client.
  const deps: IbkrContractDeps = {
    client: contractClientOf(transport),
    account: config.account,
    timeoutMs: 15_000,
  };

  try {
    // (1) EQUITY — the generic tickers.
    const spy = await resolveContract(intentOf("SPY"), deps);
    show("SPY equity", spy);
    const qqq = await resolveContract(intentOf("QQQ"), deps);
    show("QQQ equity", qqq);

    // (2) The LIVE chain → pick a near-dated expiry + a listed strike near the money, then resolve
    // that exact identity through the gateway's definition lookup. The layer never picks for us.
    const chain = await resolveOptionChain(
      { symbol: "SPY", underlyingConId: spy.conId, tradingClass: "SPY" },
      deps,
    );
    console.log(
      `[contract-smoke] SPY chain: exchange=${chain.exchange} class=${chain.tradingClass} multiplier=${chain.multiplier} expirations=${chain.expirations.length} strikes=${chain.strikes.length} nearest=${chain.expirations[0] ?? "?"}`,
    );
    const expiry = chain.expirations[0];
    if (expiry === undefined) throw new Error("[contract-smoke] the live chain listed no expiration");

    // The chain's strike grid is the UNION across every expiry — far wider than any one series lists.
    // So ask the venue what it lists for THIS expiry (another definition lookup, no market data) and
    // pick from that. Picking off the union would invent an identity the venue does not list.
    const listed = await listOptionStrikes({ symbol: "SPY", expiry, right: "C", tradingClass: "SPY" }, deps);
    const strike = listed[Math.floor(listed.length / 2)];
    if (strike === undefined) throw new Error("[contract-smoke] the venue lists no strike for that expiry");
    console.log(
      `[contract-smoke] SPY ${expiry} C: the venue LISTS ${listed.length} strikes (${listed[0]}…${listed[listed.length - 1]}); picked the median ${strike}`,
    );

    const opt = await resolveContract(intentOf("SPY", strike, "C"), { ...deps, expiry, tradingClass: "SPY" });
    show("SPY option", opt);

    // (3) FAIL-CLOSED against the REAL gateway: a strike the venue does not list.
    // Deliberately LAST of the socket-touching steps: the real gateway answers IB 200, and the
    // transport's fatal-error rule degrades the shared session on it (see contract.ts §KNOWN
    // INTERACTION) — so any lookup after this one would correctly throw until a reconnect.
    await mustRefuse("SPY option @ a BOGUS strike (99999)", () =>
      resolveContract(intentOf("SPY", 99999, "C"), { ...deps, expiry, tradingClass: "SPY" }),
    );
    console.log(
      `[contract-smoke] after the refusal the shared transport reports degraded=${transport.status().degraded} — the venue's own 200 felled the session (fail-closed; a reconnect is required)`,
    );

    // (4) FAIL-CLOSED: an option leg with NO expiry — never guessed, even though the chain is right there.
    await mustRefuse("SPY option with NO supplied expiry", () =>
      resolveContract(intentOf("SPY", strike, "C"), deps),
    );

    console.log("[contract-smoke] PASS — every contract came from the gateway; no order was placed.");
  } finally {
    transport.disconnect();
    console.log("[contract-smoke] transport closed cleanly. 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_CONTRACT_SMOKE !== "1") {
    console.log(
      "[contract-smoke] skipped: set KESTREL_IBKR_CONTRACT_SMOKE=1 (and run a paper IB Gateway) to exercise this. Places no orders.",
    );
  } else {
    runContractSmoke().catch((e: unknown) => {
      const err = e as Error;
      console.error(`[contract-smoke] FAILED (fail-closed): ${err.name}: ${err.message}`);
      process.exitCode = 1;
    });
  }
}

export { runContractSmoke };
