/**
 * # protocol/support — the four-cell SIGNED partition + named READ VIEWS (kestrel-dpy, ADR-0014)
 *
 * PROMOTED ONTO THE PROTOCOL SURFACE (kestrel-h314, D5). This module used to live at `src/support/index.ts`
 * (a runtime leaf); it is PURE (imports nothing) and it owns the fill-support vocabulary the certified
 * `fill_claim` evidence on the protocol {@link ./index.ts Blotter} is partitioned by, so it now sits on the
 * zero-dependency protocol surface and is re-exported from `kestrel.markets/protocol`. `src/support/index.ts`
 * remains as a thin re-export of THIS module (one source, ADR-0014 — the fill seam, the Blotter projector,
 * the OSS grader, and any self-hosted outsider all read the SAME `bankableEv` / `bankableEvOf` policy). The
 * platform then deletes its `apps/api/src/grade/support.ts` redeclaration and imports these from the package
 * (closing "seam candidate 6" duplication).
 *
 * The single owner of the run's expected-$ **honesty question**. ADR-0014: *sign policy is a property
 * of the QUESTION asked of a run, never of the run itself.* So the support partition is **written once**
 * — as four SIGNED cells, `{calibrated, extrapolated} × {gain, loss}` — and every policy (`raw`,
 * `bankable`, and any future one) is a pure READ VIEW over it, never a write-time collapse. One module
 * owns the vocabulary ({@link FillSupport}), the partition ({@link partition}), and the views; the fill
 * engine, the Blotter projector, and the grader all consume this ONE module so a second, forkable policy
 * can never drift out of sync.
 *
 * The forcing case (ADR-0014): grading one tape under a floor model (`strict-cross`) and an offline
 * ceiling (`maker-fair`, fills `@fair`) is a deliberate bracket, and the ceiling's cells are
 * `extrapolated` by construction. Two legitimate consumers ask different questions:
 *
 *   - **raw** — *"what does this run claim under its own fill model?"* The experiment / comparison view
 *     (fill-model brackets + config grids): the full, support-LABELED total — extrapolated gains KEPT,
 *     because censoring the ceiling's upside makes the floor↔ceiling bracket unreadable. Label, never
 *     censor.
 *   - **bankable** — *"what may be banked, ranked, sold?"* Calibrated cells in FULL (both signs), plus
 *     extrapolated **losses**; extrapolated **gains are REFUSED** (contribute 0). The asymmetry is
 *     deliberate and conservative: a strategy can neither hill-climb into an uncalibrated corner nor
 *     park its losses there. Leaderboards, seasons, and any attested receipt read this view only.
 *
 * The bug this closes: the Blotter's `fill_claim` used to partition expected-$ **unsigned**, so a
 * consumer that "subtracts the extrapolated slice" would erase extrapolated LOSSES together with the
 * gains — flattering exactly the loss-parking strategy the doctrine exists to punish, and disagreeing
 * with `bankableEv`. A SIGNED partition on the record lets a subtracting consumer remove only the
 * refused `gain`, so extrapolated losses can never be zeroed. Pure and deterministic (RUNTIME §0).
 */

/** The support flag's two states (the single source; the fill seam and Blotter re-export from here).
 * `"calibrated"` = the cell is covered by realized live data (or is a structural price-priority fact);
 * `"extrapolated"` = priced off the offline ceiling with no live anchor (a grader refuses to bank its
 * gains). A missing/unstated flag is treated as `"extrapolated"` downstream (the conservative,
 * fail-closed default). */
export type FillSupport = "calibrated" | "extrapolated";

/** The grader/RunRecord's banking predicate: a cell's expected-$ **gain** is bankable **only** when its
 * support is `"calibrated"`. An `"extrapolated"` cell — or an unstated one — is refused (conservative,
 * fail-closed default), so a strategy cannot hill-climb into an uncalibrated corner of the fill model
 * (e.g. the far-OTM-offer fantasy or a dark internalization lift). */
export function isCalibratedSupport(support: FillSupport | undefined): boolean {
  return support === "calibrated";
}

/**
 * The four-cell SIGNED partition of a run's expected-$ (ADR-0014, written ONCE). Each cell is a signed
 * sum on the same currency grid as {@link rawEv}/{@link bankableEvOf}: `gain ≥ 0` (Σ of the non-negative
 * expected-$ in that support class) and `loss ≤ 0` (Σ of the negative). The named views below are pure
 * folds over these four numbers; the partition itself never bakes a sign policy in.
 */
export interface SupportPartition {
  readonly calibrated: { readonly gain: number; readonly loss: number };
  readonly extrapolated: { readonly gain: number; readonly loss: number };
}

/**
 * Fold per-cell expected-$ into the four SIGNED cells (PURE). Fail-closed: an unstated support flag is
 * bucketed as `extrapolated` (never silently banked); each cell's value is split by sign so a gain and a
 * loss in the same support class land in different cells and can never cancel at write time. The empty
 * fold is the all-zero partition.
 */
export function partition(
  cells: readonly { readonly support: FillSupport | undefined; readonly ev: number }[],
): SupportPartition {
  let calGain = 0;
  let calLoss = 0;
  let extGain = 0;
  let extLoss = 0;
  for (const cell of cells) {
    if (isCalibratedSupport(cell.support)) {
      if (cell.ev >= 0) calGain += cell.ev;
      else calLoss += cell.ev;
    } else {
      // Unstated support fails closed to extrapolated (ADR-0014) — never banked as calibrated.
      if (cell.ev >= 0) extGain += cell.ev;
      else extLoss += cell.ev;
    }
  }
  return {
    calibrated: { gain: calGain, loss: calLoss },
    extrapolated: { gain: extGain, loss: extLoss },
  };
}

/**
 * The **raw** view (ADR-0014): the full, support-LABELED total — all four cells summed, so extrapolated
 * gains are KEPT. This is the experiment / comparison number the fill-model floor↔ceiling brackets and
 * the config grids read; the Grade's obligation here is to label the support, never to censor it.
 */
export function rawEv(p: SupportPartition): number {
  return p.calibrated.gain + p.calibrated.loss + p.extrapolated.gain + p.extrapolated.loss;
}

/**
 * The **bankable** view (ADR-0014): calibrated cells in FULL (both signs) + extrapolated LOSSES, and it
 * REFUSES extrapolated GAINS (they contribute 0). The one number leaderboards, seasons, and attested
 * receipts read. Exactly the landed per-cell {@link bankableEv} policy, aggregated:
 * `bankableEvOf(partition(cells)) === Σ bankableEv(cell.ev, cell.support)` (one policy, never forked).
 */
export function bankableEvOf(p: SupportPartition): number {
  return p.calibrated.gain + p.calibrated.loss + p.extrapolated.loss;
}

/**
 * The per-cell banking policy, RE-EXPRESSED here as the single source (grade re-exports this; it does not
 * fork a second copy). A `"calibrated"` cell banks its expected-$ as-is (both signs). An `"extrapolated"`
 * / unstated cell REFUSES a paper GAIN (→ 0) but does NOT forgive a paper LOSS — zeroing a negative EV
 * would flatter a strategy that routes losing fills into uncalibrated corners (the fail-OPEN inverse of
 * the hill-climb this guard prevents). So an extrapolated cell banks only its non-positive part.
 */
export function bankableEv(ev: number, support: FillSupport | undefined): number {
  if (isCalibratedSupport(support)) return ev;
  return Math.min(ev, 0);
}
