/**
 * # src/fill/mark-to-model — the PER-WAKE mark-to-model of held option legs (kestrel theta-cell seam b)
 *
 * ## The gap this closes
 * The sim marks an option position ONCE: {@link import("./engine.ts").SimFillEngine} cash-settles filled
 * inventory at **intrinsic at the close**. So a held straddle's loss lands only at the terminal settle — there is NO
 * intra-session mark between wakes, and therefore no dense signal that punishes STANDING DOWN on a
 * position that is bleeding theta. On the FOMC-OPTIONS WHIPSAW day that terminal floor is already −$186,
 * but a watcher graded per-wake sees a FLAT position at every interim wake and pays no running cost for
 * doing nothing.
 *
 * This module adds that missing intra-session mark: at each wake it re-marks the held legs against the
 * **tape's own option book** (the bid-side / sellable value folded as-of that wake ts) and reports the
 * running mark-to-market versus the premium basis. A held long straddle whose premium is decaying shows
 * a progressively NEGATIVE mark between wakes — so standing down becomes natively costly, which is the
 * whole point of the theta cell (it structurally dissolves the "doing-nothing-wins" degenerate reward).
 *
 * ## Gated, additive, byte-identical when OFF
 * The entire pass is behind {@link MarkToModelInput.markToModel}. When it is `false` (the default for
 * every existing cell — their pinned floors do not move) the function returns an EMPTY mark series and a
 * `standDownFloorUsd` of exactly `0`: nothing is marked, nothing downstream changes. Only a cell that
 * opts in (`markToModel: true` on its frame) gets the per-wake bleed.
 *
 * ## What it is NOT (the clean boundary)
 * This is a MARKING seam, not a settlement rewrite. It does not touch the τ provider
 * ({@link import("../session/clock.ts").expiryTauYears}), does not touch `SimFillEngine.settle`, and does
 * not compute position greeks (delta/gamma/theta as first-class quantities — that is the watcher-pareto
 * peer's percept pane). It marks against the observed book, the one honest closeable value; a Black-76
 * `@fair` mark is a deliberate alternative NOT taken here.
 *
 * ## Why kestrel-wcnd (τ-from-expiry) does not touch this file's MATH
 * The τ bug mispriced every model-derived value. This seam derives NO model value: `sellablePerContract`
 * reads the tape's own posted bid/ask, which needs no τ, no forward, and no vol surface. So the marking
 * is unaffected by the τ fix BY CONSTRUCTION — it is exactly the property that made an observed-book mark
 * the honest choice here. What DOES change is the leg-tenor assertion below: it now guards a `book.expiry`
 * that genuinely drives pricing elsewhere, so a multi-tenor position (the ONE case where a single
 * `book.expiry` cannot serve every leg) must keep surfacing loudly rather than mark a leg against another
 * contract's book.
 */

import { foldBook, type BookEvent, type BookState, type BusEvent } from "../bus/types.ts";
import type { ExpirySelector } from "../lang/ast.ts";

/** The standard listed-option contract multiplier (premiums + P&L are per-contract-×-100 dollars),
 * matching `src/session/sim.ts` execSpec for an option underlier. */
export const OPTION_CONTRACT_MULTIPLIER = 100;

/** One held option leg to mark — a reconstructed (not authored) position, so it carries its OWN tenor
 * via {@link ExpirySelector} (the {@link import("../lang/ast.ts").OptionLeg} `expiry` seam). `basisPremium`
 * is the per-contract premium the leg was opened at (the arm-time ask for a long leg). */
export interface HeldOptionLeg {
  readonly side: "buy" | "sell";
  /** Contracts held (positive). */
  readonly qty: number;
  readonly strike: number;
  readonly right: "C" | "P";
  /** Per-contract premium basis (arm-time ask for a long leg; the credit for a short leg). */
  readonly basisPremium: number;
  /** The leg's preserved tenor (seam a). Optional; when present it is asserted to match the book's
   * single expiry, so a multi-tenor position would surface the (unbuilt) re-keying boundary loudly. */
  readonly expiry?: ExpirySelector;
}

/** One per-wake mark of the whole held position. */
export interface WakeMark {
  readonly ts: number;
  /** The book state's `asof_ts` the mark was folded from (≤ `ts`); `null` when no book exists yet. */
  readonly bookAsOfTs: number | null;
  /** The position's per-contract sellable value at this wake (Σ leg bid-side, a dark bid ⇒ 0). */
  readonly sellablePerContract: number;
  /** The running mark-to-market in USD vs the premium basis, summed over legs and SIGNED BY SIDE: a LONG
   * leg contributes `(bid − basis) × qty × multiplier` (it paid the basis as a debit), a SHORT leg
   * contributes `(−ask + basis) × qty × multiplier = (credit − ask) × qty × multiplier` (it received the
   * basis as a credit). NEGATIVE = the held position has bled below its basis. */
  readonly mtmUsd: number;
}

export interface MarkToModelInput {
  /** The tape (BOOK events are folded into the running option book; everything else is ignored). */
  readonly events: readonly BusEvent[];
  /** The instrument whose book to fold (the option underlier symbol, e.g. `"SPY"`). */
  readonly instrument: string;
  /** The held legs to mark (the armed straddle, reconstructed from the arm). */
  readonly legs: readonly HeldOptionLeg[];
  /** The wake instants (UTC ms) to mark AT — typically the post-statement wakes where the position is held. */
  readonly wakeTsList: readonly number[];
  /** THE GATE. `false`/absent ⇒ no marking at all (empty series, `standDownFloorUsd = 0`) — byte-identical. */
  readonly markToModel?: boolean;
  /** Contract multiplier (default {@link OPTION_CONTRACT_MULTIPLIER}). */
  readonly multiplier?: number;
}

export interface MarkToModelResult {
  /** Whether the pass actually ran (mirrors the gate) — an inert seam reads `false` here. */
  readonly enabled: boolean;
  /** The per-wake mark series (empty when disabled). */
  readonly marks: readonly WakeMark[];
  /**
   * The realized floor of a STAND-DOWN (hold-and-do-nothing) at the LAST marked wake — the intra-session
   * theta-bled value the watcher is charged for standing pat. `0` when disabled (the position is carried
   * at basis, no interim loss recognized) — which is exactly the OFF/ON contrast the theta seam exists to
   * create: costly when ON, ~0 when OFF.
   */
  readonly standDownFloorUsd: number;
}

const r2 = (x: number): number => Math.round(x * 100) / 100;

/** Fold the running option book of `instrument` in ONE forward pass, snapshotting the book state at
 * each wake ts (each snapshot folds only events at-or-before its ts — no lookahead). Pure. O(events +
 * wakes). Returns the book state to mark against at each ascending wake (`null` before the first BOOK). */
function booksAtWakes(events: readonly BusEvent[], instrument: string, ascWakes: readonly number[]): (BookState | null)[] {
  const out: (BookState | null)[] = new Array(ascWakes.length).fill(null);
  let state: BookState | undefined;
  let w = 0;
  for (const ev of events) {
    // Every wake strictly before this event's ts sees the book as folded so far (no lookahead).
    while (w < ascWakes.length && ascWakes[w]! < ev.ts) {
      out[w] = state ?? null;
      w++;
    }
    if (w >= ascWakes.length) break; // all wakes resolved
    if (ev.stream === "TICK" && ev.type === "BOOK" && ev.instrument === instrument) {
      state = foldBook(ev as BookEvent, state);
    }
  }
  for (; w < ascWakes.length; w++) out[w] = state ?? null; // wakes at/after the last event see the final book
  return out;
}

/** The per-contract sellable (bid-side) value of one leg against a book state. A long leg is closed by
 * SELLING at the bid; a short leg is closed by BUYING at the ask. A dark quote counts 0 (genuinely
 * unclose­able), matching `measureOptionTape`'s bid-side convention. */
function sellablePerContract(leg: HeldOptionLeg, book: BookState | null): number {
  if (book === null) return 0;
  const q = book.legs.find((l) => l.strike === leg.strike && l.right === leg.right);
  if (q === undefined) return 0;
  // Long ⇒ we receive the bid on close; short ⇒ we pay the ask to close (a cost, so negative sellable).
  return leg.side === "buy" ? q.bid ?? 0 : -(q.ask ?? 0);
}

/**
 * Mark the held legs at each wake against the tape's option book. See the module header. Pure; no clock,
 * no RNG, no lookahead (each wake folds the book only up to its own ts).
 */
export function markHeldLegsPerWake(input: MarkToModelInput): MarkToModelResult {
  const mult = input.multiplier ?? OPTION_CONTRACT_MULTIPLIER;
  if (input.markToModel !== true) {
    return { enabled: false, marks: [], standDownFloorUsd: 0 };
  }

  const marks: WakeMark[] = [];
  const ascWakes = [...input.wakeTsList].sort((a, b) => a - b);
  const books = booksAtWakes(input.events, input.instrument, ascWakes);
  for (let i = 0; i < ascWakes.length; i++) {
    const ts = ascWakes[i]!;
    const book = books[i]!;
    // Seam a is load-bearing: if a leg carries its own tenor, it must match the single-expiry book it is
    // marked against — a mismatch is the (unbuilt) multi-expiry re-keying boundary, surfaced loudly.
    for (const leg of input.legs) {
      if (leg.expiry?.kind === "expiry-tag" && book?.expiry !== undefined && leg.expiry.tag !== book.expiry) {
        throw new Error(
          `mark-to-model: held leg ${leg.strike}${leg.right} tenor "${leg.expiry.tag}" != book expiry "${book.expiry}" — ` +
            `the execution book is keyed by symbol with ONE expiry; a multi-tenor position needs the (symbol, expiry) ` +
            `re-keying that is deliberately out of scope for this marking seam.`,
        );
      }
    }
    let sellable = 0;
    let mtm = 0;
    for (const leg of input.legs) {
      const s = sellablePerContract(leg, book);
      sellable += s;
      // Mark-to-market vs the premium BASIS, signed by side. `sellablePerContract` already carries the
      // close side: a LONG's `s` is +bid (received on close), a SHORT's `s` is −ask (paid to close). The
      // basis, however, is stored UNSIGNED per leg (`reconstructHeldOptionLegs` gives a short a POSITIVE
      // credit). A long paid its basis (a debit) ⇒ P&L = s − basis; a short RECEIVED its basis (a credit)
      // ⇒ P&L = s + basis = credit − ask. Using `s − basis` for a short computed −ask − credit (a
      // fabricated loss of 2×credit×qty×mult) instead of credit − ask — the sign bug this expression fixes
      // (kestrel-wt8c; folded into the bankable `totals.floor`).
      mtm += (leg.side === "buy" ? s - leg.basisPremium : s + leg.basisPremium) * leg.qty * mult;
    }
    marks.push({ ts, bookAsOfTs: book?.asof_ts ?? null, sellablePerContract: r2(sellable), mtmUsd: r2(mtm) });
  }

  // The stand-down floor is the mark at the LAST wake (the terminal intra-session value of holding).
  const standDownFloorUsd = marks.length > 0 ? marks[marks.length - 1]!.mtmUsd : 0;
  return { enabled: true, marks, standDownFloorUsd };
}
