/**
 * # adapters/broker/ibkr/surface-window — THE ONE SURFACE-WINDOW RULE (kestrel-7o2.19)
 *
 * Which strikes back a vol surface, and whether that surface may be trusted to price the money.
 * There is exactly ONE definition of each here, because **a rule that exists twice is a rule that
 * will diverge** — and it had already diverged: the ORDER path centred its window on the ATM strike
 * the venue lists, while the FEED took whatever the listed grid happened to start with. Rendered
 * against the live paper gateway with SPY at 751.94, the feed resolved:
 *
 *     741C 741P 742C 742P 743C 743P 744C 744P 745C 745P
 *
 * Every call 7–11 points IN THE MONEY (nearly pure intrinsic — its price carries almost no vol
 * information) and every put a 1-to-4-cent lottery ticket (one tick is a huge relative move). The
 * agent's Frame — its screen, its scarce token budget — was spent on strikes it cannot trade, and the
 * vol surface built from those legs was then interpolated to price an AT-THE-MONEY leg while its
 * receipt reported `nLiquid=5` and looked perfectly healthy. A receipt that says `nLiquid=5` while all
 * five strikes are deep ITM is a receipt that does not know it is lying.
 *
 * So this module owns two things and nothing else:
 *
 *  1. {@link surfaceWindow} — the ATM-CENTRED window of the strikes the venue **LISTS FOR THAT
 *     EXPIRY**. Centre on the observed spot; take {@link SurfaceWindowInput.halfWidth} listed strikes
 *     each side. Never the chain's UNION grid (`reqSecDefOptParams` returns the union across every
 *     expiry, and a strike in the union may simply not be listed on the expiry you want — selecting
 *     off it invents an identity the venue does not list). Never a centre-less pick: without the
 *     money there is no window, only whatever the grid happens to start with.
 *
 *  2. {@link atmCoverage} — the HONEST fit-quality question `nLiquid` cannot answer: *does this
 *     surface know where the money is?* It is measured off the SAME liquidity rule the surface itself
 *     uses ({@link buildSurface} — imported, never re-implemented), and it is deliberately TWO facts,
 *     because either one alone can be gamed by a pathological chain:
 *       - **bracketing** — are there liquid strikes on BOTH sides of the money? `interpIv` FLAT-
 *         EXTRAPOLATES past the ends of the surface, so an ATM read off a surface whose liquid strikes
 *         all lie below the money is not an interpolation at all: it is the deepest wing's IV, wearing
 *         an at-the-money label. (This is the live case exactly.)
 *       - **distance** — is the nearest liquid strike actually NEAR the money (within
 *         {@link DEFAULT_ATM_BAND_FRACTION} of spot)? A surface that brackets the money from ±40
 *         points away brackets it in name only.
 *
 * Pure functions: no clock, no RNG, no socket (RUNTIME §0). Deterministic on the same inputs.
 *
 * ## PLACES NO ORDERS
 * This is selection and fit-quality arithmetic. It reaches no client, opens no request, and names no
 * order path — market data only, like every other module in the feed face.
 */

import { buildSurface, impliedForward } from "../../../fair/index.ts";
import type { OptionQuote } from "../../../bus/index.ts";

// ─────────────────────────────────────────────────────────────────────────────
// The named, injectable parameters (never a magic number at a call site)
// ─────────────────────────────────────────────────────────────────────────────

/**
 * How many LISTED strikes to take EACH SIDE of the at-the-money one. The default is the order path's
 * own (kestrel-7o2.8's dry run): 4 each side ⇒ **9 strikes ⇒ 18 two-sided legs**. A `@fair` with a
 * receipt needs real liquid neighbours on both wings — one leg cannot imply its own surface, and a
 * surface that stops at the money can only extrapolate across it.
 */
export const DEFAULT_SURFACE_HALF_WIDTH = 4;

/**
 * What "NEAR the money" means, as a fraction of spot — the band a liquid strike must fall inside to
 * count as covering an at-the-money valuation. Dimensionless on purpose: half a percent of spot is
 * the same statement about a 40-dollar name and a 750-dollar one. (At SPY 751.94 that is ±3.76 — so
 * the live case's nearest liquid strike, 745, sits 6.94 out and does NOT cover the money.)
 */
export const DEFAULT_ATM_BAND_FRACTION = 0.005;

/**
 * What to do with a valuation whose surface does not know the money:
 *  - `fail-closed` (the default) — `fair => null`, and the caller gets the ANNOTATED book fallback
 *    (a SELL still floored at intrinsic, never naked). No number, no receipt.
 *  - `taint` — the value and its receipt survive, but they travel WEARING the reason they cannot be
 *    trusted. Useful for a percept that wants to show the number and refuse to vouch for it.
 * There is no third option, and there is no silent one.
 */
export type AtmPolicy = "fail-closed" | "taint";

// ─────────────────────────────────────────────────────────────────────────────
// Typed refusal
// ─────────────────────────────────────────────────────────────────────────────

/** Why a surface window could not be built. Fail-closed: a window is never invented, and a window
 * with no money at its centre is not a window — it is the kestrel-7o2.19 defect. */
export class IbkrSurfaceWindowError extends Error {
  override readonly name = "IbkrSurfaceWindowError";
  readonly reason: string;
  constructor(reason: string) {
    super(`IBKR surface window refused: ${reason} (fail-closed; STAND_DOWN)`);
    this.reason = reason;
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// The window
// ─────────────────────────────────────────────────────────────────────────────

export interface SurfaceWindowInput {
  /** The strikes the venue **LISTS FOR THIS EXPIRY** (`listOptionStrikes`) — never the chain's UNION
   * grid across expiries (`resolveOptionChain().strikes`), which can contain strikes this expiry does
   * not list at all. */
  readonly listed: readonly number[];
  /** The OBSERVED underlier price — where the money actually is. */
  readonly spot: number;
  /** How many listed strikes each side of the ATM one (default {@link DEFAULT_SURFACE_HALF_WIDTH}). */
  readonly halfWidth?: number | undefined;
}

/** An ATM-centred slice of the venue's LISTED grid — the one thing both the feed and the order path
 * select their surface with. */
export interface SurfaceWindow {
  /** The observed spot the window was centred on. */
  readonly spot: number;
  /** The LISTED strike nearest the money — the venue's own, never a rounded guess. */
  readonly atm: number;
  readonly halfWidth: number;
  /** The window, ascending. Always a subset of `listed`: a strike the venue does not list can never
   * appear here, however close to the money it would have been. */
  readonly strikes: readonly number[];
}

/**
 * The LISTED strike nearest the money. Ties break LOW (deterministic — no clock, no RNG). `null` only
 * for an empty/unusable grid, which the caller fails closed on.
 */
export function atmListedStrike(listed: readonly number[], spot: number): number | null {
  if (!Number.isFinite(spot)) return null;
  let best: number | null = null;
  for (const s of listed) {
    if (!Number.isFinite(s)) continue;
    if (best === null) {
      best = s;
      continue;
    }
    const d = Math.abs(s - spot);
    const db = Math.abs(best - spot);
    if (d < db || (d === db && s < best)) best = s;
  }
  return best;
}

/**
 * THE ONE RULE (kestrel-7o2.19). Centre on the ATM strike the venue LISTS for this expiry, take
 * `halfWidth` listed strikes each side. Both the feed's chain slice and the order path's vol surface
 * are selected with this and nothing else.
 *
 * @throws {IbkrSurfaceWindowError} on an empty listed grid (a grid is never invented) or on a spot
 * that is not a usable price (a window with no money at its centre is exactly the defect this exists
 * to kill: it silently becomes "whatever the grid happens to start with").
 */
export function surfaceWindow(input: SurfaceWindowInput): SurfaceWindow {
  const { spot } = input;
  const halfWidth = input.halfWidth ?? DEFAULT_SURFACE_HALF_WIDTH;

  if (!Number.isFinite(spot) || spot <= 0) {
    throw new IbkrSurfaceWindowError(
      `a surface window was asked for with NO usable spot (got ${String(spot)}) — a window with no money at its centre is not a window, it is whatever the grid happens to start with (kestrel-7o2.19: 741–745 while the money was at 751.94)`,
    );
  }
  // Sorted + de-duplicated defensively: `listOptionStrikes` already returns the venue's grid this
  // way, and the window's determinism must not depend on that staying true elsewhere.
  const grid = [...new Set(input.listed.filter((s) => Number.isFinite(s)))].sort((a, b) => a - b);
  if (grid.length === 0) {
    throw new IbkrSurfaceWindowError(
      "the venue LISTS no strikes for this expiry — a strike grid is never invented, and a surface is never built off a grid nobody published",
    );
  }
  if (!Number.isFinite(halfWidth) || halfWidth < 0) {
    throw new IbkrSurfaceWindowError(
      `halfWidth must be a non-negative number of listed strikes each side (got ${String(halfWidth)})`,
    );
  }

  const atm = atmListedStrike(grid, spot) as number; // grid is non-empty ⇒ never null
  const i = grid.indexOf(atm);
  const half = Math.floor(halfWidth);
  const strikes = grid.slice(Math.max(0, i - half), Math.min(grid.length, i + half + 1));
  return { spot, atm, halfWidth: half, strikes };
}

// ─────────────────────────────────────────────────────────────────────────────
// The honest fit quality — does this surface KNOW THE MONEY?
// ─────────────────────────────────────────────────────────────────────────────

export interface AtmCoverageInput {
  /** The chain slice the surface is actually backed out of (the SAME legs handed to `executionFair`). */
  readonly legs: readonly OptionQuote[];
  /** The observed underlier price. `null` ⇒ there is no money to cover, and nothing may vouch for one. */
  readonly spot: number | null;
  /** Time to expiry in YEARS — injected, never read off a clock (RUNTIME §0). */
  readonly tauYears: number;
  /** The near-the-money band, as a fraction of spot (default {@link DEFAULT_ATM_BAND_FRACTION}). */
  readonly bandFraction?: number | undefined;
}

/**
 * The fit-quality fact `nLiquid` cannot express: **does the surface know where the money is?**
 * `nLiquid=5` is a count, not a location — five deep-ITM strikes are five liquid strikes and zero
 * evidence about an at-the-money price.
 */
export interface AtmCoverage {
  readonly spot: number | null;
  /** The near-the-money band in dollars (`bandFraction × spot`). `null` with no usable spot. */
  readonly bandUsd: number | null;
  readonly bandFraction: number;
  /** How many strikes actually backed the surface — the same count the receipt reports. */
  readonly nLiquid: number;
  /** How many of those lie WITHIN the band around the money. This is the number that mattered. */
  readonly nNearMoney: number;
  /** The liquid strike closest to the money, and its distance. `null` on an empty surface. */
  readonly nearestLiquidStrike: number | null;
  readonly distanceUsd: number | null;
  /** Are there liquid strikes at-or-below AND at-or-above the money? If not, an ATM read is a FLAT
   * EXTRAPOLATION off a wing (`interpIv` flat-extrapolates past both ends), not an interpolation. */
  readonly bracketsSpot: boolean;
  /** The verdict: may a receipt vouch for an AT-THE-MONEY valuation off this surface? */
  readonly supportsAtm: boolean;
  /** Why not — a logged reason, never a silent `false`. `null` exactly when {@link supportsAtm}. */
  readonly reason: string | null;
}

/**
 * Measure a surface's coverage OF THE MONEY (kestrel-7o2.19). The liquidity rule is not re-invented
 * here: {@link buildSurface} is asked which strikes actually produced a vol point, so a leg this
 * module calls "liquid" is exactly a leg the surface was built from — one-sided, dark, crossed and
 * no-arb-violating books contribute nothing, as they contribute nothing there.
 *
 * Which is exactly why the surface must be built at the SAME FORWARD the model prices at — the
 * parity read, not spot (kestrel-ukwz). The forward decides which legs are liquid at all (it sets
 * `realSides`' no-arb bounds) and whether a strike's call and put agree about vol, so measuring
 * coverage at spot while `executionFair` prices at the parity forward would describe a DIFFERENT
 * surface than the one the number came off — a receipt vouching for a surface that was never
 * built. `spot` still defines the MONEY (the band, the bracketing, the distances): where the
 * underlier is, is a fact about spot; what the options are worth, is a fact about the forward.
 */
export function atmCoverage(input: AtmCoverageInput): AtmCoverage {
  const { legs, spot, tauYears } = input;
  const bandFraction = input.bandFraction ?? DEFAULT_ATM_BAND_FRACTION;

  if (spot === null || !Number.isFinite(spot) || spot <= 0) {
    return {
      spot: null,
      bandUsd: null,
      bandFraction,
      nLiquid: 0,
      nNearMoney: 0,
      nearestLiquidStrike: null,
      distanceUsd: null,
      bracketsSpot: false,
      supportsAtm: false,
      reason:
        "there is no usable underlier, so there is no money for the surface to cover — nothing may vouch for an at-the-money valuation",
    };
  }

  // The strikes that ACTUALLY backed the surface — the engine's own liquidity rule, not a second
  // one, AT the engine's own forward, not a second one (see the header).
  const points = buildSurface(legs, impliedForward(legs, spot).forward, tauYears);
  const strikes = points.map((p) => p.strike);
  const bandUsd = Math.abs(bandFraction) * spot;

  const nLiquid = strikes.length;
  const nNearMoney = strikes.filter((s) => Math.abs(s - spot) <= bandUsd).length;
  const below = strikes.some((s) => s <= spot);
  const above = strikes.some((s) => s >= spot);
  const bracketsSpot = below && above;
  const nearestLiquidStrike = atmListedStrike(strikes, spot);
  const distanceUsd = nearestLiquidStrike === null ? null : Math.abs(nearestLiquidStrike - spot);

  const base = {
    spot,
    bandUsd,
    bandFraction,
    nLiquid,
    nNearMoney,
    nearestLiquidStrike,
    distanceUsd,
    bracketsSpot,
  };

  if (nLiquid === 0) {
    return {
      ...base,
      supportsAtm: false,
      reason: "the surface has NO liquid strikes at all — there is nothing to interpolate and nothing to vouch for",
    };
  }
  if (!bracketsSpot) {
    const side = above ? "ABOVE" : "BELOW";
    return {
      ...base,
      supportsAtm: false,
      reason:
        `every one of the ${nLiquid} liquid strike(s) lies ${side} the money (spot=${spot}, liquid=[${strikes.join(", ")}]) — ` +
        `the surface does not BRACKET the money, so an at-the-money read off it is a flat EXTRAPOLATION of the wing's IV, not an interpolation`,
    };
  }
  if (nNearMoney === 0) {
    return {
      ...base,
      supportsAtm: false,
      reason:
        `the nearest liquid strike (${nearestLiquidStrike ?? "—"}) is ${(distanceUsd ?? 0).toFixed(2)} from the money ` +
        `(spot=${spot}), outside the ±${bandUsd.toFixed(2)} near-the-money band — the surface brackets the money only in name`,
    };
  }
  return { ...base, supportsAtm: true, reason: null };
}

/** The taint a valuation carries when its surface does not know the money — the sentence a receipt
 * reporting `nLiquid` alone would never say about itself. */
export function atmTaint(coverage: AtmCoverage): string {
  return (
    `THE SURFACE DOES NOT KNOW THE MONEY (atm-uncovered): ${coverage.reason ?? "no at-the-money coverage"} — ` +
    `nLiquid=${coverage.nLiquid} counts liquid strikes, it does not locate them, so this valuation is NOT vouched for (kestrel-7o2.19)`
  );
}
