/**
 * # fair — ExecutionFair: the honest `@fair` anchor (ARCHITECTURE §4, RUNTIME §4)
 *
 * ExecutionFair is the price at which a market maker will actually **fill** you — not the
 * midpoint of a possibly-fictional book. It is **underlying-anchored intrinsic + a vol read
 * backed out of the *liquid* quotes, floored at intrinsic, and receipt-gated**: the value
 * carries whether it can be trusted (model version, how many liquid strikes backed it, the
 * IV it was priced at), not just a number. The observed mid is a health signal (thin/dark
 * book fingerprint), never a value.
 *
 * This module is **pure** — no I/O, no clock, no RNG (RUNTIME §0). It is the one place that
 * knows how `fair` is computed; every surface that resolves `@fair` reads through here.
 *
 * ## The contract (RUNTIME §4)
 * `executionFair(...)` returns `{ value, receipt }` or **`null`**. It returns `null` in
 * exactly two cases — **no usable underlying**, or **zero liquid strikes** to back a vol out
 * of. `null` is *not* an error: it is the signal for the price resolver to fall back to an
 * **annotated** book value (`fair=fallback(mid)` for a BUY, `fair=fallback(max(mid,
 * intrinsic))` for a SELL) — a silent mid is forbidden. This module never invents that
 * fallback; it reports honestly that fair is unbuildable and lets the caller annotate.
 *
 * When it *does* return a value, that value is **always floored at intrinsic** — the price a
 * maker will never sell you below (RUNTIME §4 SELL floor, and the honest lower bound on any
 * option's worth).
 *
 * ## Freshness is NOT this module's job
 * A pure valuation cannot know whether the quotes it priced are stale — staleness is a fact
 * about the clock and the book's last update, and this module reads neither. So it does not
 * gate on freshness; it **carries** the observation time instead. The caller may inject an
 * `asof` timestamp (epoch ms) on {@link ExecutionFairInput}; it is stamped verbatim onto the
 * {@link FairReceipt} as {@link FairReceipt.asof}. **Applying** a staleness gate (comparing
 * `asof` against `now`, de-arming or annotating a too-old fair) is the **engine's** job,
 * downstream — the receipt merely makes the input observable so that gate can exist. Absent
 * with a reason, now documented.
 */

import type { OptionQuote, Right } from "../bus/types.ts";
import { black76, intrinsic } from "./black76.ts";
import { buildSurface, impliedForward, interpIv, type ForwardSource } from "./surface.ts";

export {
  black76,
  impliedVol,
  intrinsic,
  tauYearsToClose,
  erf,
  normCdf,
  MS_PER_YEAR,
  type Black76Params,
  type ImpliedVolParams,
} from "./black76.ts";
export {
  buildSurface,
  impliedForward,
  interpIv,
  FORWARD_NO_ARB_BAND,
  type ForwardSource,
  PARITY_TOTAL_VOL_TOLERANCE,
  type ImpliedForward,
  type IvPoint,
} from "./surface.ts";

/** `@fair` for a spot instrument (equity/crypto, ADR-0017): its own two-sided quote mid with a
 * receipt (`exec-fair-quote-v1`), or `null` when one-sided/dark — never Black-76. */
export {
  executionFairSpot,
  EXEC_FAIR_QUOTE_MODEL,
  type SpotFairInput,
  type SpotFairResult,
  type SpotFairReceipt,
} from "./spot.ts";

/** The versioned model tag stamped on every receipt. EVs across model versions are not
 * comparable and are distinguished by this string (ARCHITECTURE §4). */
export const EXEC_FAIR_MODEL = "exec-fair-b76-v1" as const;

/** The trust envelope carried alongside a fair value — it says how the number was earned. */
export interface FairReceipt {
  /** Versioned model identity. */
  readonly model: typeof EXEC_FAIR_MODEL;
  /** How many liquid strikes backed the vol surface (0 ⇒ no fair; the whole result is null). */
  readonly nLiquid: number;
  /** The implied vol the value was priced at (interpolated to the requested strike). */
  readonly ivAtStrike: number;
  /** The FORWARD the value was priced at — derived by put-call parity, NOT assumed equal to
   * spot (ADR-0037 §2, kestrel-ukwz). Carried so an outsider can recompute the number: a fair
   * whose forward is invisible cannot be audited, and the forward is the single largest term in
   * the price. */
  readonly forward: number;
  /** `"parity"` (derived from the option market) or `"spot"` (the known-degraded fallback). */
  readonly forwardSource: ForwardSource;
  /** The strike parity was read at. Present iff `forwardSource === "parity"`. */
  readonly forwardStrike?: number;
  /** `forward − underlyingSpot` — the carry/dividend/clock-desync wedge the option market itself
   * implies. Present iff `forwardSource === "parity"`. */
  readonly forwardResidual?: number;
  /** The machine-readable reason the parity derivation failed, present iff
   * `forwardSource === "spot"`. This is what makes the degraded forward LOUD: the pricing seam
   * names it in the price annotation and the live-broker wall gates on it (ADR-0037 §6/§7) — a
   * spot forward is never silently substituted. */
  readonly forwardDegraded?: string;
  /** The WORST call-vs-put IV disagreement across the surface points backing this value — the
   * fit-quality signal `nLiquid` never was (kestrel-ku99). (`nLiquid` counts strikes, and counts
   * them *higher* the more legs a corrupt forward splits apart, so it reads healthiest exactly
   * when the surface is least trustworthy — ADR-0037 §6.) Absent when no strike had both sides
   * liquid — parity was then unobservable, and claiming `0` would assert an agreement never
   * checked. */
  readonly parityResidual?: number;
  /** The observation timestamp (epoch ms) the caller injected on {@link ExecutionFairInput.asof},
   * carried through verbatim. Absent when the caller supplied none. This module never gates on
   * it — freshness gating is the engine's job downstream; the receipt only makes `asof`
   * observable (see the module header). */
  readonly asof?: number;
}

/** A resolved fair value with its receipt. */
export interface FairResult {
  readonly value: number;
  readonly receipt: FairReceipt;
}

/** Why no fair was served. Two KINDS, because they are two different facts:
 *  - `unbuildable` — no fair could be computed at all (no underlying / no liquid strikes). The
 *    caller's annotated book fallback is the ordinary, expected path (RUNTIME §4).
 *  - `tainted` — a fair WAS computed and then REFUSED as untrustworthy. Same fallback, but this
 *    is an active corruption signal and the reason belongs in the audit trail. */
export type FairRefusalKind = "unbuildable" | "tainted";

/** The machine-readable refusal reasons — a closed set, so a downstream gate can switch on them
 * rather than parse prose. */
export type FairRefusalReason =
  /** `underlyingSpot` is not a finite positive number. */
  | "no-underlying"
  /** Zero liquid strikes produced a vol point. */
  | "no-liquid-strikes"
  /** The surface could not be read at the requested strike (defensive; unreachable when
   * `nLiquid > 0`). */
  | "no-surface-at-strike"
  /** The valued leg's own book is real+fresh and the value landed ABOVE its ask (ADR-0037 §3). */
  | "fair-above-fresh-ask"
  /** The valued leg's own book is real+fresh and the value landed BELOW its bid (ADR-0037 §3). */
  | "fair-below-fresh-bid";

/** A refusal to serve a fair value, always naming a machine-readable reason. */
export interface FairRefusal {
  readonly refused: FairRefusalKind;
  readonly reason: FairRefusalReason;
}

/** Either a fair value with its receipt, or a reasoned refusal. */
export type FairOutcome = FairResult | FairRefusal;

/** Narrow a {@link FairOutcome} to the refusal arm. */
export function isFairRefusal(o: FairOutcome): o is FairRefusal {
  return "refused" in o;
}

/** The inputs to a single ExecutionFair valuation. `liquidQuotes` is the chain slice the vol
 * surface is built from — two-sided legs are kept, one-sided/dark legs ignored (their mid is
 * a health signal, never a price). */
export interface ExecutionFairInput {
  /** The underlier SPOT. It is **not** the forward — the forward is derived from the option
   * market by put-call parity (ADR-0037 §2); spot is the intrinsic-floor anchor, the ATM-ness
   * yardstick, and the degraded fallback. Non-finite/≤0 ⇒ `null` result. */
  readonly underlyingSpot: number;
  /** The strike being valued. */
  readonly strike: number;
  /** Call or put. */
  readonly right: Right;
  /** Time to expiry in years (injected; `<= 0` prices to intrinsic). */
  readonly tauYears: number;
  /** The chain slice to back the vol surface out of. */
  readonly liquidQuotes: readonly OptionQuote[];
  /** Optional injected observation timestamp (epoch ms) of the quotes being valued. The pure
   * valuation does **not** gate on it — it is carried onto {@link FairReceipt.asof} so a
   * downstream freshness gate (the engine's job) can compare it against `now`. Time is always
   * injected, never read from a clock (RUNTIME §0). */
  readonly asof?: number;
  /** The VALUED leg's own bid, for the fair-vs-book bound (kestrel-ku99, ADR-0037 §3).
   * `null`/absent ⇒ dark ⇒ no bound. Distinct from {@link ExecutionFairInput.liquidQuotes}, which
   * is the chain the surface is built from; this is the one leg being priced. */
  readonly legBid?: number | null;
  /** The VALUED leg's own ask. `null`/absent ⇒ dark ⇒ no bound. */
  readonly legAsk?: number | null;
  /** The caller's verdict on whether the valued leg's book is FRESH — injected, because this
   * module reads no clock (ADR-0037 §4: freshness is the engine's job). `true` ⇒ the book is
   * real-time enough to be authoritative, so a value outside it is refused as tainted.
   * Absent/`false` ⇒ the book carries no such authority and the bound does not apply AT ALL
   * (ADR-0037 §3: a stale/wide/dark/index-lagging book is a diagnostic the underlying-anchored
   * surface legitimately overrides — never a clamp). */
  readonly bookFresh?: boolean;
}

/** Rounding slack for the fair-vs-book comparison. The bound exists to catch a value that is
 * WRONG (the SPY-752 incident sat 28% above the ask), never one a float-ulp outside the quote. */
const BOOK_BOUND_EPS = 1e-9;

/**
 * Resolve `@fair` for one option, reporting a machine-readable reason when it refuses.
 *
 * The forward is READ from the options by parity (kestrel-ukwz, ADR-0037 §2) and used for BOTH
 * the IV inversion and the final pricing; the intrinsic floor stays on spot. A parity strike
 * whose call/put IVs disagree contributes no vol point (kestrel-ku99). A value outside the valued
 * leg's own real+fresh book is REFUSED, never clamped (ADR-0037 §3/§5).
 *
 * {@link executionFair} is the reason-free wrapper over this; prefer THIS when you can put the
 * reason in an audit trail.
 */
export function executionFairOutcome(input: ExecutionFairInput): FairOutcome {
  const { underlyingSpot, strike, right, tauYears, liquidQuotes, asof } = input;

  if (!Number.isFinite(underlyingSpot) || underlyingSpot <= 0) {
    return { refused: "unbuildable", reason: "no-underlying" };
  }

  // THE FORWARD IS DERIVED, NEVER ASSUMED (ADR-0037 §2, kestrel-ukwz). It is threaded into BOTH
  // the IV inversion (buildSurface) and the Black-76 price — the two must agree on the forward or
  // the vol read is inverted against one number and re-priced against another.
  const fwd = impliedForward(liquidQuotes, underlyingSpot);

  const surface = buildSurface(liquidQuotes, fwd.forward, tauYears);
  const nLiquid = surface.length;
  if (nLiquid === 0) return { refused: "unbuildable", reason: "no-liquid-strikes" };

  const ivAtStrike = interpIv(surface, strike);
  if (ivAtStrike === null) return { refused: "unbuildable", reason: "no-surface-at-strike" };

  // The worst parity disagreement any backing strike was accepted under — the honest fit-quality
  // signal (kestrel-ku99). Max (not mean): one inconsistent strike is not excused by many clean ones.
  let parityResidual: number | undefined;
  for (const p of surface) {
    if (p.dispersion === undefined) continue;
    if (parityResidual === undefined || p.dispersion > parityResidual) parityResidual = p.dispersion;
  }

  const modeled = black76({ forward: fwd.forward, strike, tauYears, sigma: ivAtStrike, right });
  // The floor stays SPOT-anchored (ADR-0037 §1): intrinsic is settlement truth — a fact about
  // spot, not about the forward.
  const value = Math.max(modeled, intrinsic(underlyingSpot, strike, right)); // floored, always

  // The fair-vs-book bound (kestrel-ku99, ADR-0037 §3) — applied ONLY on the caller's freshness
  // word, and only when the leg's own book is a REAL two-sided market. REFUSE, never clamp: a
  // clamp would manufacture a plausible number and hide the broken vol read (ADR-0037 §5).
  if (input.bookFresh === true) {
    const bid = input.legBid ?? null;
    const ask = input.legAsk ?? null;
    if (bid !== null && ask !== null && bid < ask) {
      if (value > ask + BOOK_BOUND_EPS) return { refused: "tainted", reason: "fair-above-fresh-ask" };
      if (value < bid - BOOK_BOUND_EPS) return { refused: "tainted", reason: "fair-below-fresh-bid" };
    }
  }

  return {
    value,
    // `asof` is carried verbatim (absent when the caller injected none); never gated here.
    receipt: {
      model: EXEC_FAIR_MODEL,
      nLiquid,
      ivAtStrike,
      forward: fwd.forward,
      forwardSource: fwd.source,
      ...(fwd.strike !== undefined ? { forwardStrike: fwd.strike } : {}),
      ...(fwd.residual !== undefined ? { forwardResidual: fwd.residual } : {}),
      ...(fwd.degraded !== undefined ? { forwardDegraded: fwd.degraded } : {}),
      ...(parityResidual !== undefined ? { parityResidual } : {}),
      ...(asof !== undefined ? { asof } : {}),
    },
  };
}

/**
 * Resolve `@fair` for one option, or `null` when none can be served.
 *
 * The reason-free wrapper over {@link executionFairOutcome} — `null` collapses every refusal
 * (unbuildable AND tainted) into the one signal the caller already handles: fall back to an
 * annotated book value (RUNTIME §4). Callers that can carry the reason into an audit trail
 * should call {@link executionFairOutcome} instead; `src/engine/pricing.ts` does.
 */
export function executionFair(input: ExecutionFairInput): FairResult | null {
  const outcome = executionFairOutcome(input);
  return isFairRefusal(outcome) ? null : outcome;
}
