/**
 * # fair/surface — the liquid-strike implied-vol surface
 *
 * The vol read behind `@fair` is backed out of the **liquid** quotes only: a quote counts
 * iff it is **two-sided** (both `bid` and `ask` present) **and both sides are real**. A
 * one-sided or dark leg is the MM-pull fingerprint of a real move (ARCHITECTURE §4) — its
 * "mid" is a health signal, never a price, so it contributes no vol point. From the liquid
 * legs we build a sparse (strike → iv) curve and read it by **linear interpolation with flat
 * extrapolation** (a single point ⇒ a flat surface).
 *
 * ## The input-side quote-realness gate (ven.3)
 * A quote *side* can be **FABRICATED** — a venue/exchange placeholder, not a real willingness
 * to trade. `src/fair` enforces honesty on the **output** side (its `@fair` is intrinsic-
 * floored, receipt-gated, and a dark leg's mid is "a health signal, never a price"). This is
 * the mirror on the **input** side: {@link realSides} **DEMOTES a fabricated side to DARK**
 * (`null` — the model's existing unusable-side marker, bus/types.ts) **BEFORE** its price can
 * reach the mid the vol read is backed out of. A demoted side then behaves like any dark side
 * already does — one-sided ⇒ no two-sided mid ⇒ no vol point; **both** sides fabricated ⇒ the
 * whole leg is dark ⇒ no point (fail-closed to UNKNOWN, never a fabricated anchor). No second
 * dark concept is forked: demotion *is* `null`, and the exclusion is the "one-sided/dark legs
 * contribute no point" rule this surface already ships.
 *
 * ## Per strike: parity AGREEMENT, not a blind average (kestrel-ku99)
 * Back out an IV from each liquid leg's mid (call and/or put). At the PARITY-IMPLIED forward,
 * put-call parity forces a liquid call and put at the same strike to the same IV — so a call-IV
 * and put-IV that DISAGREE are evidence the inputs are inconsistent, not noise to be smoothed.
 * This surface therefore retains the call and put IVs separately and requires them to agree
 * within {@link PARITY_TOTAL_VOL_TOLERANCE} — a band in TOTAL-vol (`sigma*sqrt(tau)`), the only
 * quantity a price identifies at r = 0, so the guard's verdict is invariant to the tenor rather
 * than detonating as tau -> 0. On violation the strike contributes **NO point** (fail-closed)
 * rather than laundering the inconsistency into an authoritative-looking mean. Each point carries
 * its {@link IvPoint.dispersion} so a receipt can no longer read healthy while lying. With a
 * correct (parity-read) forward the two IVs agree to bisection precision, so this guard is the
 * fail-closed NET, not the magnitude fix — that is the forward's job.
 *
 * ## The forward is DERIVED, never assumed (ADR-0037 §2, kestrel-ukwz)
 * The vol read and the Black-76 price are both taken at a **forward**, and the forward is NOT the
 * underlying spot: dividends, cost-of-carry, borrow, and any clock-desync between the spot feed
 * and the option quotes all drive a wedge between them. Assuming `forward = spot` was the
 * DOMINANT error behind the live SPY-752 incident (`@fair` 28% above the ask): an ATM option has
 * delta ~0.5, so a 0.37 spot/forward gap is ~0.18 of price error on a 0.72 option.
 *
 * {@link impliedForward} solves the forward out of the option market itself — `F* = K* + (Cmid −
 * Pmid)` at the ATM-most strike where BOTH a call and a put are liquid (put-call parity, taken at
 * the strike whose parity read is least leveraged by vol error). This captures carry and dividends
 * for free (they are already priced into the quotes) and is immune to the spot/quote clock-desync
 * (the forward comes from the SAME quotes being priced, on their OWN clock).
 *
 * Pure functions only — no I/O, no clock (RUNTIME §0).
 */

import type { OptionQuote } from "../bus/types.ts";
import { impliedVol, intrinsic } from "./black76.ts";

/** One point of the surface: an implied vol attributed to a strike. */
export interface IvPoint {
  readonly strike: number;
  readonly iv: number;
  /** `|ivCall − ivPut|` at this strike — the parity disagreement the point was accepted under, in
   * IV units (0 when the two agree exactly). The GUARD is applied to this scaled into total-vol
   * space (`dispersion * sqrt(tau) <= ` {@link PARITY_TOTAL_VOL_TOLERANCE}); the receipt reports
   * the IV-space number because that is the human-legible one, and the tenor needed to convert it
   * is already on the receipt. **Absent** when only one side was liquid: with a single leg there
   * is no disagreement to observe, and reporting `0` would claim an agreement that was never
   * checked. */
  readonly dispersion?: number;
}

/**
 * The parity-agreement band, in **TOTAL-VOL** units: `v = sigma * sqrt(tau)` — the standard
 * deviation of log-forward to expiry (kestrel-ku99 acceptance decision, settled on the integrated
 * T1+T2 base in Phase-2; see `docs/plans/2026-07-16-fair-redliff-artifact.md` for the calibration
 * evidence).
 *
 * ## Why total-vol and not an absolute IV band
 * Black-76 at r = 0 identifies **only** `v = sigma*sqrt(tau)` from a price — `sigma` is merely
 * `v / sqrt(tau)` (this is the same identity that makes tau cancel out of `@fair`'s VALUE:
 * `buildSurface` inverts sigma from the same book at the same tau it then prices with). Two
 * consequences follow, and together they rule out an IV-space band:
 *
 * 1. **An IV band is a tenor-dependent guard on a tenor-INVARIANT quantity.** Since
 *    `|sigmaC - sigmaP| = |vC - vP| / sqrt(tau)`, the SAME price inconsistency reads as an
 *    arbitrarily large IV disagreement as tau -> 0. Measured on the real corpus: one identical
 *    book at 15:59 shows IV-dispersion p50 = 2.7e-2 at a 4-day tau and p50 = 2.0e+0 at a
 *    session-close tau — a 76x swing from the tenor alone. An IV band of 0.02 therefore admits a
 *    normal chain at 4DTE and rejects EVERY strike of the same chain near a 0DTE close, collapsing
 *    the surface to empty exactly when the benchmark's 0DTE tapes need it most.
 * 2. **The harm the guard exists to bound is measured in v, not sigma.** Averaging a dispersed
 *    pair injects `~dSigma/2` of surface error, which reaches `@fair` multiplied by vega
 *    (`= F*sqrt(tau)*phi(d1)`), i.e. the resulting price error is proportional to
 *    `dSigma*sqrt(tau) = dV`. A v band bounds the actual @fair damage uniformly across tenor; an
 *    IV band does not.
 *
 * The v-space read is EXACTLY tau-invariant, not approximately: measured across the corpus at tau
 * ratios up to 5761x, `max |vDisp(tauTrue) - vDisp(tauBroken)| = 5.6e-14` (float epsilon). This is
 * why the band survives the wcnd tau fix without recalibration — an IV band would not.
 *
 * ## The value: 1e-3
 * Two INDEPENDENT lines of evidence land on the same place.
 *
 * (1) SEPARATION QUALITY, measured on the real corpus. A well-shaped band should admit the pairs
 * whose model-free price-space parity residual (`|Cmid - Pmid - (F - K)|`) is small and reject the
 * large ones — i.e. `worstResidualAdmitted < bestResidualRejected`. An INVERSION of that ordering
 * means the guard is anti-correlated with the inconsistency it exists to detect. Measured on the
 * 4DTE SPY tape, `1e-3` separates monotonically at both sampled instants (12:00: admits <= $0.180,
 * rejects >= $0.245; 15:59: admits <= $0.160, rejects >= $0.215) — while `2e-3` INVERTS at 12:00
 * (admits $0.340, rejects $0.310), and an IV band of `0.02` inverts badly on the real 0DTE tape at
 * 10:59: it REJECTS a pair whose price residual is $0.005 (pristine — its only sin is a vega of
 * 0.18) while ADMITTING one at $0.085.
 *
 * The cost is disclosed: `1e-3` admits FEWER strikes than a `0.02` IV band on the 4DTE tape (5/23
 * vs 9/23 at 12:00). Those 18 rejected strikes are not "legitimate points spuriously dropped" —
 * their residuals run $0.245..$0.90 and are the American early-exercise structure described below.
 * The surviving 4-5 are the near-ATM window with the cleanest parity, which is where armed
 * documents price and from which the surface interpolates. `@fair` still builds on every corpus
 * tape.
 *
 * An absolute band (not a book-width-scaled one) keeps the guard independent of quote width — a
 * wide book is a freshness signal, not a licence to disagree about parity.
 *
 * NOTE (open, for the fleet): a legitimate structured parity residual DOES exist on this corpus —
 * SPY/QQQ options are AMERICAN, and an ITM call ahead of an ex-dividend carries an early-exercise
 * premium that no European put-call parity can absorb. It is visible as a smooth, monotone
 * residual reaching $0.90 at 10 strikes ITM on the 2024-06-17 tape (whose 2024-06-21 expiry IS a
 * SPY ex-div date), where `F(K) = K + C - P` converges on SPOT exactly — the signature of a call
 * priced for immediate exercise. This band drops those strikes. That is arguably CORRECT (a
 * European inversion of an American early-exercise premium is not a vol point), but it is a
 * disclosed property, not an accident.
 */
export const PARITY_TOTAL_VOL_TOLERANCE = 1e-3;

/**
 * The input-side quote-realness gate (ven.3, see the module header). Returns the quote's
 * sides with any **fabricated** side demoted to DARK (`null`) — so the caller's mid is only
 * ever taken over sides that are real willingness to trade. The no-arb bounds are the
 * canonical, magic-free anchors, identical to the ones {@link impliedVol} self-rejects on:
 * the intrinsic **floor** and the sigma→∞ **ceiling/asymptote** (`forward` for a call,
 * `strike` for a put). Pure — no clock, no RNG.
 */
function realSides(q: OptionQuote, forward: number): { bid: number | null; ask: number | null } {
  let bid = q.bid;
  let ask = q.ask;
  // BID < INTRINSIC (no-arb floor): a bid below intrinsic could be lifted and exercised for a
  // riskless profit — it cannot be a real bid. Demote it to dark.
  if (bid !== null && bid < intrinsic(forward, q.strike, q.right)) bid = null;
  // SENTINEL-ASK / NEAR-MONEY PLACEHOLDER-CAP (no-arb ceiling): an ask at/above the sigma→∞
  // asymptote is unreachably rich — a "no real offer" placeholder, not a price. Demote it.
  const ceiling = q.right === "C" ? forward : q.strike;
  if (ask !== null && ask >= ceiling) ask = null;
  // CROSSED / LOCKED: if both real sides survive but `bid >= ask`, this is not a real two-sided
  // market (a fabricated crossed/locked book) — neither side is a real price → the leg is dark.
  if (bid !== null && ask !== null && bid >= ask) {
    bid = null;
    ask = null;
  }
  return { bid, ask };
}

/** Is this quote liquid — two-sided with a usable positive mid, **after** the realness gate
 * has demoted any fabricated side to dark ({@link realSides})? A demoted side is `null`, so a
 * fabricated leg goes one-sided (or fully dark) and yields no mid — its price never reaches
 * the vol read. */
function liquidMid(q: OptionQuote, forward: number): number | null {
  const { bid, ask } = realSides(q, forward);
  if (bid === null || ask === null) return null; // one-sided / dark (incl. demoted) → not liquid
  const mid = 0.5 * (bid + ask);
  return mid > 0 ? mid : null;
}

// ─────────────────────────────────────────────────────────────────────────────
// The derived forward (ADR-0037 §2, kestrel-ukwz)
// ─────────────────────────────────────────────────────────────────────────────

/** Where the forward `@fair` priced at came from. `"parity"` is the derived, trusted read;
 * `"spot"` is the KNOWN-DEGRADED fallback ADR-0037 §2 mandates when no strike qualifies —
 * never null (that would kill `@fair` on ~92% of real strikes), never a novel guess, and
 * never silent: it always rides with a machine-readable {@link ImpliedForward.degraded}. */
export type ForwardSource = "parity" | "spot";

/** The widest band a parity-implied forward may sit from spot before it is rejected as
 * not-a-forward (ADR-0037 §5: a violent disagreement is a SIGNAL, not a price — refuse, never
 * project). Deliberately loose: real carry/dividend/desync wedges are basis points, so anything
 * past ±10% is a broken quote, not a forward. */
export const FORWARD_NO_ARB_BAND = 0.1;

/** The forward `@fair` prices at, with the receipt material proving where it came from. */
export interface ImpliedForward {
  /** The forward used for BOTH the IV inversion and the Black-76 price. */
  readonly forward: number;
  /** `"parity"` (derived) or `"spot"` (known-degraded fallback). */
  readonly source: ForwardSource;
  /** The strike parity was read at. Present iff `source === "parity"`. */
  readonly strike?: number;
  /** `forward − spot`: how far the OPTION market's own forward sits from the spot feed — the
   * carry + dividend + clock-desync wedge, made observable. Present iff `source === "parity"`. */
  readonly residual?: number;
  /** The machine-readable reason the derivation failed. Present iff `source === "spot"` — this
   * is what makes the degraded fallback loud instead of silent. */
  readonly degraded?: string;
}

/**
 * Derive the forward from the option market by put-call parity: `F* = K* + (Cmid − Pmid)` at the
 * ATM-most strike where a call AND a put are both liquid (ADR-0037 §2). `spot` is used only as
 * the provisional anchor for the input-side realness gate ({@link realSides}, whose no-arb bounds
 * need *an* underlier context) and as the ATM-ness yardstick — never as the answer.
 *
 * **Fail-closed, per ADR-0037 §2/§5:** when no strike carries both sides liquid, or the solved
 * forward is non-finite/non-positive or outside the wide no-arb band {@link FORWARD_NO_ARB_BAND},
 * the result is the KNOWN-DEGRADED `{ forward: spot, source: "spot", degraded: <reason> }` — the
 * caller must carry that reason onto the receipt and annotate any price built on it. It is never
 * a silent spot substitution, and never a guess.
 *
 * Deterministic and leg-order-invariant: the ATM-most strike is chosen by `|K − spot|` with ties
 * broken to the LOWER strike, so permuting `quotes` cannot change the answer.
 */
export function impliedForward(quotes: readonly OptionQuote[], spot: number): ImpliedForward {
  const degraded = (reason: string): ImpliedForward => ({ forward: spot, source: "spot", degraded: reason });
  if (!Number.isFinite(spot) || spot <= 0) return degraded("underlying spot unusable — no ATM anchor to read parity at");

  // Per strike, the liquid call mid and put mid (dark/one-sided/fabricated sides never arrive here).
  const calls = new Map<number, number>();
  const puts = new Map<number, number>();
  for (const q of quotes) {
    const mid = liquidMid(q, spot);
    if (mid === null) continue;
    (q.right === "C" ? calls : puts).set(q.strike, mid);
  }

  let bestStrike: number | null = null;
  let bestDist = Infinity;
  for (const [strike, _cmid] of calls) {
    if (!puts.has(strike)) continue; // parity needs BOTH sides of the pair at the SAME strike
    const dist = Math.abs(strike - spot);
    // Strictly-better, else lower-strike tie-break ⇒ independent of `quotes` order (determinism).
    if (dist < bestDist || (dist === bestDist && bestStrike !== null && strike < bestStrike)) {
      bestDist = dist;
      bestStrike = strike;
    }
  }
  if (bestStrike === null) {
    return degraded("no strike carries a liquid two-sided CALL and PUT — put-call parity underivable");
  }

  const cmid = calls.get(bestStrike);
  const pmid = puts.get(bestStrike);
  if (cmid === undefined || pmid === undefined) return degraded("parity pair vanished — refusing to guess"); // unreachable

  const forward = bestStrike + (cmid - pmid);
  if (!Number.isFinite(forward) || forward <= 0) {
    return degraded(`parity-implied forward is not a price (${forward}) — refusing to price on it`);
  }
  if (Math.abs(forward / spot - 1) > FORWARD_NO_ARB_BAND) {
    return degraded(
      `parity-implied forward ${forward} is >${FORWARD_NO_ARB_BAND * 100}% from spot ${spot} — a broken quote, not a forward`,
    );
  }
  return { forward, source: "parity", strike: bestStrike, residual: forward - spot };
}

/**
 * Build the sparse implied-vol surface from a chain slice. Only two-sided quotes that yield a
 * positive-vol back-out contribute; strikes are returned **sorted ascending**, each carrying
 * the average IV of its usable legs. `forward` is the DERIVED forward ({@link impliedForward}) —
 * not the spot; `tauYears` the shared time-to-expiry of the slice.
 *
 * A strike carrying BOTH a liquid call and put must satisfy put-call parity within
 * {@link PARITY_TOTAL_VOL_TOLERANCE} (compared in total-vol space, so the verdict does not move
 * with `tauYears`) or it contributes NO point (fail-closed, kestrel-ku99). The accepted point's
 * IV is the mean of the two, which — because the two are keyed by `right` and combined in a fixed
 * (call, put) order — is a function of the SET of legs, never of the order they arrived in (the
 * old running sum/count was order-dependent; RUNTIME §0).
 */
export function buildSurface(
  quotes: readonly OptionQuote[],
  forward: number,
  tauYears: number,
): IvPoint[] {
  // strike → the call/put IVs, RETAINED separately: the parity guard needs to compare them, and
  // a blind running mean would have already destroyed the evidence of their disagreement.
  const acc = new Map<number, { call?: number; put?: number }>();
  for (const q of quotes) {
    const mid = liquidMid(q, forward);
    if (mid === null) continue;
    const iv = impliedVol({ forward, strike: q.strike, tauYears, price: mid, right: q.right });
    if (iv === null) continue;
    const cur = acc.get(q.strike) ?? {};
    // First-wins per (strike, right): the folded book is latest-wins per leg, so a duplicate here
    // is a malformed slice — taking the first keeps the read a deterministic function of the
    // input rather than a silent last-writer race.
    if (q.right === "C") {
      if (cur.call === undefined) cur.call = iv;
    } else if (cur.put === undefined) {
      cur.put = iv;
    }
    acc.set(q.strike, cur);
  }
  const points: IvPoint[] = [];
  // The tenor's sqrt, computed ONCE: the guard compares TOTAL vol (v = sigma*sqrt(tau)), the only
  // quantity a price actually identifies at r = 0, so its verdict does not move with the tenor.
  const sqrtTau = Math.sqrt(tauYears);
  for (const [strike, { call, put }] of acc) {
    if (call !== undefined && put !== undefined) {
      const dispersion = Math.abs(call - put);
      // FAIL-CLOSED: parity says these must be equal. They are not ⇒ the inputs are inconsistent
      // and no honest vol point can be built here. Contribute nothing rather than average an
      // inconsistency into an authoritative-looking number.
      //
      // Compared in TOTAL-VOL space (`dispersion * sqrtTau`), NOT as a bare IV band: the same
      // price inconsistency reads as an arbitrarily large IV gap as tau -> 0, so an IV band would
      // empty the surface near a 0DTE close while waving the identical book through at 4DTE.
      // See PARITY_TOTAL_VOL_TOLERANCE for the derivation + the measured calibration.
      if (dispersion * sqrtTau > PARITY_TOTAL_VOL_TOLERANCE) continue;
      points.push({ strike, iv: 0.5 * (call + put), dispersion });
      continue;
    }
    const only = call ?? put;
    // One liquid side: parity is unobservable here, so no `dispersion` is claimed.
    if (only !== undefined) points.push({ strike, iv: only });
  }
  points.sort((a, b) => a.strike - b.strike);
  return points;
}

/**
 * Read the surface at `strike` by linear interpolation, flat-extrapolating past both ends.
 * A single point ⇒ that point's IV everywhere. Assumes `points` is non-empty and sorted
 * ascending (as {@link buildSurface} returns); returns `null` only for an empty surface.
 */
export function interpIv(points: readonly IvPoint[], strike: number): number | null {
  const n = points.length;
  const first = points[0];
  const last = points[n - 1];
  if (first === undefined || last === undefined) return null; // empty
  if (n === 1 || strike <= first.strike) return first.iv; // single point or below-range → flat
  if (strike >= last.strike) return last.iv; // above-range → flat

  for (let i = 0; i < n - 1; i++) {
    const a = points[i];
    const b = points[i + 1];
    if (a === undefined || b === undefined) break;
    if (strike >= a.strike && strike <= b.strike) {
      const span = b.strike - a.strike;
      if (span <= 0) return a.iv; // coincident strikes → left value
      const w = (strike - a.strike) / span;
      return a.iv + w * (b.iv - a.iv);
    }
  }
  return last.iv; // unreachable given sorted, in-range input
}
