/**
 * # protocol/grade — the one OSS-exported deterministic Judge (kestrel-h314 D4, CONTEXT "Judge")
 *
 * `grade(blotters) → GradeResult` is the deterministic grader any self-hosted Kestrel — or any outsider —
 * runs to score a SET of protocol {@link ./index.ts Blotter}s. It is the recompute substrate certification
 * rests on: it consumes ONLY the public protocol Blotter (no engine, no bus, no runtime), so the managed
 * service's signed output must be byte-identical to what this function produces from the published record.
 *
 * TWO METRIC FAMILIES, ONE GRID. Every number lands on the SAME 1e-8 rounding grid this module OWNS (NOT a
 * platform `round6`), exported as {@link REPORT_GRID} / {@link roundToGrid} so downstream report surfaces
 * round through this one primitive instead of re-declaring `1e8` (kestrel-z473.6), so a re-derivation is
 * byte-stable:
 *   • the ACTIVITY family  — `realized_pnl`, `fill_rate`, `order_count`, `fill_count`, `position_count`,
 *     `gross_traded_qty`, `filled_notional` — a pure fold of the generic order/fill/position/settlement
 *     arrays every Blotter carries; and
 *   • the RISK-HONEST family — `bankable_ev`, `realized_floor_usd`, `expected_usd` — recomputed from the
 *     CERTIFIED EVIDENCE (`totals` + `fill_claim`, kestrel-h314 D2). `bankable_ev` applies the ADR-0014
 *     banking doctrine ({@link ./support.ts bankableEvOf}) over the fill-claim partition: the calibrated
 *     slice banks in full, the extrapolated slice banks ONLY its losses.
 *
 * AGGREGATION = Σ over the set + a sorted-join on `subjectSessionId`. Grading N Blotters yields ONE
 * GradeResult whose subject identity is the members' `sessionId`s sorted and joined — deterministic and
 * order-independent (same set ⇒ same subject ⇒ same numbers).
 *
 * FAIL-CLOSED (kestrel-h314 D6). A pre-widening Blotter that lacks the certified evidence (`totals` and/or
 * `fill_claim` absent) CANNOT be certified for the risk-honest metrics: the whole family is REFUSED —
 * OMITTED from `metrics` (never a silent zero) — and a `labels.risk_honest = "unknown"` verdict names the
 * offending session(s). The activity family, which needs no evidence, still computes. UNKNOWN in, UNKNOWN
 * out; a certified number never renders without its evidence.
 */

import type { Blotter, FillClaim, GradeResult } from "./index.ts";
import { bankableEvOf, type SupportPartition } from "./support.ts";

/** The Judge's OWN rounding grid (kestrel-h314 D4): a 1e-8 quantization, NOT a platform `round6`. This is
 * the SHARED report/grader $-grid — every downstream surface that rounds report dollars ({@link
 * ../blotter/project.ts}, {@link ../session/sim.ts buildReport}) imports it from here rather than
 * re-declaring `1e8` locally, so an outsider's re-derivation is byte-identical (kestrel-z473.6 re-homing).
 * A `const`, per ADR-0046 (protocol stays dependency-free — types and consts/pure fns only). */
export const REPORT_GRID = 1e8;

/** Quantize a report dollar onto the shared {@link REPORT_GRID}. The ONE rounding primitive the grader and
 * every downstream report surface share (kestrel-z473.6): same math everywhere ⇒ byte-identical bytes. */
export function roundToGrid(x: number): number {
  return Math.round(x * REPORT_GRID) / REPORT_GRID;
}

/** Closeness tolerance for validating a NORMALIZED probability distribution sums to 1 (used by the receipt
 * validator's `day_type_probabilities` check, {@link ../grade/receipt.ts}). A DIFFERENT quantity from
 * {@link REPORT_GRID}, kept distinct on purpose (kestrel-z473.6 — re-home, never silently unify): the grid
 * is a $-quantization (1e-8), this is a dimensionless closeness bound on a unit-sum distribution, held one
 * decade tighter at 1e-9. Both live here so the divergence is named in one place, not scattered. */
export const PROBABILITY_SUM_TOLERANCE = 1e-9;

const round = roundToGrid;

/** Reconstruct the four-cell SIGNED {@link SupportPartition} from a Blotter's wire `fill_claim` slices. A
 * missing slice folds to the all-zero cell (conservative) — but a WHOLLY absent `fill_claim` is handled by
 * the fail-closed evidence gate in {@link grade}, never here. */
function partitionOf(claims: readonly FillClaim[]): SupportPartition {
  const cal = claims.find((c) => c.support === "calibrated");
  const ext = claims.find((c) => c.support === "extrapolated");
  return {
    calibrated: { gain: cal?.gain ?? 0, loss: cal?.loss ?? 0 },
    extrapolated: { gain: ext?.gain ?? 0, loss: ext?.loss ?? 0 },
  };
}

/** Does this Blotter carry the CERTIFIED EVIDENCE (both `totals` and `fill_claim`) the risk-honest family
 * recomputes from? Absence is UNKNOWN, never a silent zero (kestrel-h314 D6). */
function hasCertifiedEvidence(b: Blotter): boolean {
  return b.totals !== undefined && b.fill_claim !== undefined;
}

/**
 * The deterministic OSS Judge (kestrel-h314 D4): grade a SET of protocol Blotters into one {@link GradeResult}.
 * Pure — no wall clock, no RNG, no runtime. Same set ⇒ byte-identical result. See the module header for the
 * two metric families, the shared 1e-8 grid, the sorted-join subject identity, and the fail-closed evidence
 * gate on the risk-honest family.
 */
export function grade(blotters: readonly Blotter[]): GradeResult {
  const subjectSessionId = blotters
    .map((b) => b.sessionId)
    .sort()
    .join("+");

  // ── activity family — a pure fold of the generic arrays every Blotter carries (needs no evidence) ──
  let orderCount = 0;
  let fillCount = 0;
  let positionCount = 0;
  let realizedPnl = 0;
  let grossTradedQty = 0;
  let filledNotional = 0;
  for (const b of blotters) {
    orderCount += b.orders.length;
    fillCount += b.fills.length;
    positionCount += b.positions.length;
    realizedPnl += b.settlement.realized;
    for (const f of b.fills) {
      grossTradedQty += Math.abs(f.qty);
      filledNotional += Math.abs(f.qty * f.price);
    }
  }

  const metrics: Record<string, number> = {
    realized_pnl: round(realizedPnl),
    fill_rate: orderCount > 0 ? round(fillCount / orderCount) : 0,
    order_count: orderCount,
    fill_count: fillCount,
    position_count: positionCount,
    gross_traded_qty: round(grossTradedQty),
    filled_notional: round(filledNotional),
  };

  // ── risk-honest family — recomputed from the certified evidence, or REFUSED (fail-closed, D6) ──
  const missing = blotters.filter((b) => !hasCertifiedEvidence(b)).map((b) => b.sessionId);
  if (missing.length === 0) {
    let floor = 0;
    let expected = 0;
    let bankable = 0;
    for (const b of blotters) {
      floor += b.totals!.floor;
      expected += b.totals!.expected;
      bankable += bankableEvOf(partitionOf(b.fill_claim!));
    }
    metrics.realized_floor_usd = round(floor);
    metrics.expected_usd = round(expected);
    metrics.bankable_ev = round(bankable);
    return { subjectSessionId, metrics };
  }

  // Fail closed: the risk-honest family is UNKNOWN — OMITTED, never zeroed — and the verdict names the
  // session(s) whose certified evidence is absent (deterministic order).
  const named = missing.length === 0 ? "the set" : [...missing].sort().join(", ");
  return {
    subjectSessionId,
    metrics,
    labels: {
      risk_honest: "unknown",
      risk_honest_reason: `certified evidence (totals + fill_claim) absent on: ${named} — risk-honest metrics refused, never a silent zero (kestrel-h314 D6)`,
    },
  };
}
