/**
 * # fair/black76 — the option-pricing math core (r = 0)
 *
 * ExecutionFair is built on Black-76: options priced off the **forward**, discounted at the
 * risk-free rate. Kestrel runs the whole surface at **r = 0** — short-dated index and
 * equity options carry no meaningful carry over the horizons that matter, and a zero rate
 * makes the forward equal spot, keeps put-call parity exactly `C - P = F - K`, and removes a
 * calibration knob nobody would trust anyway. (ARCHITECTURE §4: "underlying-anchored
 * intrinsic + a vol read backed out of the *liquid* quotes, floored at intrinsic.")
 *
 * Everything here is a **pure function** — no I/O, no clock, no RNG (RUNTIME §0). Time is a
 * parameter (`tauYears`), never read from a wall clock.
 *
 * ## Contract
 * - `black76` prices a call/put; `tauYears <= 0` **or** `sigma <= 0` collapse to the forward
 *   intrinsic (no time value to add). The price is always `>= intrinsic`.
 * - `impliedVol` back-outs the volatility that reprices a given premium by bisection, and
 *   returns `null` when there is **no positive-vol solution** (premium at/below intrinsic) or
 *   the premium is **unreachably rich** (>= the asymptote: `forward` for a call, `strike` for
 *   a put — the sigma→∞ limits at r = 0).
 * - `intrinsic` and `tauYearsToClose` are the small shared helpers callers reuse.
 */

import type { Right } from "../bus/types.ts";
import { dexp, dlog } from "./detmath.ts";

/** Calendar-year basis for `tauYearsToClose` (365d × 24h × 60m × 60s × 1000ms). */
export const MS_PER_YEAR = 365 * 24 * 60 * 60 * 1000;

/**
 * Error function, implemented **odd by construction** (`erf(-x) = -erf(x)`) so that
 * `normCdf(x) + normCdf(-x) === 1` exactly — which is what makes put-call parity hold to
 * floating-point rounding. Abramowitz & Stegun 7.1.26 on the magnitude (|error| ≲ 1.5e-7),
 * mirrored through the sign. The exponential is the vendored {@link dexp}, not `Math.exp`, so
 * this stays bit-identical across V8/JSC on the certified-grade path (see {@link ./detmath.ts}).
 */
export function erf(x: number): number {
  const sign = x < 0 ? -1 : 1;
  const ax = Math.abs(x);
  const t = 1 / (1 + 0.3275911 * ax);
  const y =
    1 -
    ((((1.061405429 * t - 1.453152027) * t + 1.421413741) * t - 0.284496736) * t +
      0.254829592) *
      t *
      dexp(-ax * ax);
  return sign * y;
}

/** Standard-normal CDF. `normCdf(x) + normCdf(-x) === 1` exactly (see {@link erf}). */
export function normCdf(x: number): number {
  return 0.5 * (1 + erf(x / Math.SQRT2));
}

/** `1 / √(2π)` — the standard-normal PDF's normalizing constant, as a literal (no `Math.PI`
 * dependency on the certified path; the value is exact to double precision). */
const INV_SQRT_2PI = 0.3989422804014327;

/** Standard-normal PDF `φ(x) = e^(−x²/2) / √(2π)`. Uses the vendored {@link dexp} so gamma/vega
 * stay bit-identical across V8/JSC on the certified-grade path (see {@link ./detmath.ts}). */
export function normPdf(x: number): number {
  return INV_SQRT_2PI * dexp(-0.5 * x * x);
}

/**
 * The two spot-greeks the options-analytics MODEL panes need (delta + gamma), plus vega, at r = 0
 * (forward = spot). All are **model outputs** — computed, never observed — so every consumer that
 * renders one must carry the Black-Scholes receipt (see {@link ../frame/options-analytics.ts}).
 *  - `delta` — call `N(d1)`, put `N(d1) − 1` (∈ [−1, 1]).
 *  - `gamma` — `φ(d1) / (F σ √τ)` (per $1 of underlier; ≥ 0, peaks at-the-money, blows up as τ→0).
 *  - `vega`  — `F φ(d1) √τ` (per 1.00 = 100 vol-points; ≥ 0).
 *  - `theta` — `−F φ(d1) σ / (2√τ)` (per YEAR of calendar time; the time-decay bleed). At r = 0 the
 *    rate/carry terms vanish, so theta is IDENTICAL for a call and a put and is `≤ 0` for a long
 *    option (holding it loses value as τ shrinks) — the θ the theta-bleed percept pane renders.
 */
export interface Greeks {
  readonly delta: number;
  readonly gamma: number;
  readonly vega: number;
  readonly theta: number;
}

/**
 * The Black-76 greeks at r = 0. A degenerate leg (`τ ≤ 0`, `σ ≤ 0`, or a non-positive forward/
 * strike) has NO time value: gamma/vega/theta collapse to 0 and delta is the moneyness step (a call
 * is 0/1, a put 0/−1 across the strike). Pure — a total function of its parameters.
 */
export function greeks(p: Black76Params): Greeks {
  const { forward, strike, tauYears, sigma, right } = p;
  if (!(tauYears > 0) || !(sigma > 0) || !(forward > 0) || !(strike > 0)) {
    const itm = right === "C" ? forward > strike : forward < strike;
    const delta = right === "C" ? (itm ? 1 : 0) : itm ? -1 : 0;
    return { delta, gamma: 0, vega: 0, theta: 0 };
  }
  const sqrtT = Math.sqrt(tauYears);
  const vol = sigma * sqrtT; // σ√τ
  const d1 = (dlog(forward / strike) + 0.5 * vol * vol) / vol;
  const pdf = normPdf(d1);
  const delta = right === "C" ? normCdf(d1) : normCdf(d1) - 1;
  const gamma = pdf / (forward * vol); // φ(d1) / (F σ √τ)
  const vega = forward * pdf * sqrtT;
  // Calendar-time decay per YEAR: −∂Price/∂τ. At r = 0 both call and put share this single term
  // (the discount/drift terms that split them are zero), so long options bleed and short options earn.
  const theta = -(forward * pdf * sigma) / (2 * sqrtT);
  return { delta, gamma, vega, theta };
}

/**
 * Intrinsic value of an option at `spot` — the payoff if it settled now. Call = `max(spot -
 * strike, 0)`, put = `max(strike - spot, 0)`. At r = 0 the forward equals spot, so this is
 * also the forward intrinsic that {@link black76} floors at. Never negative.
 */
export function intrinsic(spot: number, strike: number, right: Right): number {
  return right === "C" ? Math.max(spot - strike, 0) : Math.max(strike - spot, 0);
}

/** Parameters for a single Black-76 valuation (r = 0, so `forward` is the spot/forward). */
export interface Black76Params {
  readonly forward: number;
  readonly strike: number;
  readonly tauYears: number;
  readonly sigma: number;
  readonly right: Right;
}

/**
 * Black-76 price at r = 0. `tauYears <= 0` or `sigma <= 0` ⇒ intrinsic exactly (no time value
 * to add). Strictly increasing in `sigma` (positive vega), so it is invertible by bisection.
 * The result is always `>= intrinsic(forward, strike, right)`.
 */
export function black76(p: Black76Params): number {
  const { forward, strike, tauYears, sigma, right } = p;
  const intr = intrinsic(forward, strike, right);
  if (!(tauYears > 0) || !(sigma > 0) || !(forward > 0) || !(strike > 0)) {
    return intr;
  }
  const sqrtT = Math.sqrt(tauYears);
  const vol = sigma * sqrtT;
  const d1 = (dlog(forward / strike) + 0.5 * vol * vol) / vol;
  const d2 = d1 - vol;
  const price =
    right === "C"
      ? forward * normCdf(d1) - strike * normCdf(d2)
      : strike * normCdf(-d2) - forward * normCdf(-d1);
  // Numerical floor: the closed form is >= intrinsic analytically; guard rounding.
  return Math.max(price, intr);
}

/** Parameters for an implied-vol back-out. `price` is the premium to invert (typically a
 * two-sided mid). */
export interface ImpliedVolParams {
  readonly forward: number;
  readonly strike: number;
  readonly tauYears: number;
  readonly price: number;
  readonly right: Right;
}

/**
 * Back out the Black-76 volatility that reprices `price`, by bisection.
 *
 * Returns `null` when there is no positive-vol solution:
 * - `price` is not a finite positive number, or `tauYears <= 0` (no time value to carry vol);
 * - `price <= intrinsic` (the sigma→0 limit — nothing to back out);
 * - `price >= asymptote` (`forward` for a call, `strike` for a put — the sigma→∞ limit at
 *   r = 0): an **unreachably-rich** mid.
 *
 * Otherwise a unique sigma exists in the open interval and bisection converges to it (the
 * inverse is exact against {@link black76}, so a round-trip recovers the input sigma).
 */
export function impliedVol(p: ImpliedVolParams): number | null {
  const { forward, strike, tauYears, price, right } = p;
  if (!Number.isFinite(price) || price <= 0) return null;
  if (!(tauYears > 0) || !(forward > 0) || !(strike > 0)) return null;

  const intr = intrinsic(forward, strike, right);
  if (price <= intr) return null; // at/below intrinsic → sigma would be 0, not positive
  const asymptote = right === "C" ? forward : strike;
  if (price >= asymptote) return null; // richer than the sigma→∞ limit → unreachable

  const value = (sigma: number): number =>
    black76({ forward, strike, tauYears, sigma, right });

  // Bracket: b76(lo) ≈ intrinsic < price by construction; grow hi until it dominates price.
  let lo = 1e-8;
  let hi = 5;
  const MAX_HI = 50;
  let vhi = value(hi);
  for (let guard = 0; vhi < price && hi < MAX_HI && guard < 64; guard++) {
    hi = Math.min(hi * 2, MAX_HI);
    vhi = value(hi);
  }
  if (vhi < price) return null; // still short of the premium at MAX_HI → treat as unreachable

  for (let i = 0; i < 128; i++) {
    const midSigma = 0.5 * (lo + hi);
    const v = value(midSigma);
    if (Math.abs(v - price) < 1e-12) return midSigma;
    if (v < price) lo = midSigma;
    else hi = midSigma;
    if (hi - lo < 1e-12) break;
  }
  return 0.5 * (lo + hi);
}

/**
 * Year-fraction from `nowTs` to the session `closeTs`, both epoch-ms (injected — never a wall
 * clock, RUNTIME §0), on a 365-day calendar basis. Past the close the value is `<= 0`, which
 * {@link black76} treats as intrinsic.
 */
export function tauYearsToClose(nowTs: number, closeTs: number): number {
  return (closeTs - nowTs) / MS_PER_YEAR;
}
