/**
 * # session/harness/bounded-numerics — the BOUNDED-NUMERICS emission guard (fail-closed, never scored)
 *
 * A strict schema / constrained-decoding grammar can force a syntactically-VALID but DEGENERATE number: a
 * `json_schema` `qty:integer` probe forced digits and the model looped to an **8,173-digit integer** —
 * valid JSON, garbage. Such an emission is a GENERATION FAILURE, not a decision. It classifies INVALID
 * (fail-closed) and is NEVER scored — the same "reject unknown input, never default it" rule the
 * empty→INVALID guard already enforces in {@link file://./live-agent.ts} (empty/zero-token → `invalid`).
 *
 * This is the LOCAL port of the HOSTED adapter's `invalid-degenerate` class
 * (`scripts/bench/hosted/hosted-adapter.ts` `checkBoundedNumerics` / `classifyEmission`), lifted here so the
 * local live harness and the hosted eval adapter share ONE convention: a degenerate numeric is rejected
 * identically on both lanes. Pure: no clock, no RNG, no I/O.
 *
 * Two cheap, load-bearing checks:
 *   1. **pre-parse raw-string digit cap** — any run of >{@link MAX_NUMERIC_DIGITS} consecutive digits in the
 *      RAW emission is a decoding loop (the 8,173-digit integer), caught BEFORE `JSON.parse` silently
 *      coerces it to `Infinity` (which `Number.isFinite` would then reject anyway) OR — the sneakier case —
 *      to a FINITE-but-absurd float (e.g. a 15-digit run → ~1e15) that a bare finiteness check would SCORE
 *      as a real order. The raw cap catches BOTH before the coercion hides them.
 *   2. **per-field range sanity** — every action's numeric field (`order.qty` / `order.strike` /
 *      `scheduleWake.at.minutes`) must be finite and inside {@link NUMERIC_FIELD_BOUNDS}. A `NaN`/`Infinity`/
 *      absurd value is degenerate. Bounds are generous — they catch garbage, not judgment.
 */

/** Max run of consecutive digits tolerated in a RAW emission. Any longer run is a constrained-decoding
 * digit loop, not a number (the 8,173-digit integer). Matches the hosted adapter's cap. */
export const MAX_NUMERIC_DIGITS = 12;

/** Per-field sanity ranges for the action-schema numeric fields (finite + in-range; a NaN/Inf/absurd value
 * is a degenerate emission, never scored). Generous — they catch garbage, not judgment. Mirrors the hosted
 * adapter's `NUMERIC_FIELD_BOUNDS` and maps onto the Kestrel action schema (`src/session/agent.ts`):
 * `placeOrder.order.{qty,strike}` and `scheduleWake.at.minutes` (the `inMinutes` variant). */
const NUMERIC_FIELD_BOUNDS = {
  /** contracts (option) or shares (equity) — a positive, finite, non-absurd count. */
  qty: { min: 0, max: 1e7, exclusiveMin: true },
  /** an option strike — positive, finite, bounded well above any real underlying. */
  strike: { min: 0, max: 1e7, exclusiveMin: true },
  /** scheduleWake `inMinutes` — non-negative minutes within a sane horizon. */
  minutes: { min: 0, max: 1e7, exclusiveMin: false },
} as const;

function inBounds(v: unknown, b: { min: number; max: number; exclusiveMin: boolean }): boolean {
  if (typeof v !== "number" || !Number.isFinite(v)) return false;
  if (b.exclusiveMin ? v <= b.min : v < b.min) return false;
  return v <= b.max;
}

/** Extract + JSON.parse the single top-level object from a reply. Mirrors `prompt.ts:extractJsonObject`
 * (not exported) and the hosted adapter's `extractObject`: tolerate a ```json fence and surrounding prose,
 * take the first `{` to the last `}`. Returns `null` when there is no parseable object. */
function extractObject(reply: string): Record<string, unknown> | null {
  const fenced = /```(?:json)?\s*([\s\S]*?)```/i.exec(reply);
  const body = (fenced !== null ? fenced[1]! : reply).trim();
  const start = body.indexOf("{");
  const end = body.lastIndexOf("}");
  if (start === -1 || end === -1 || end < start) return null;
  try {
    const v = JSON.parse(body.slice(start, end + 1)) as unknown;
    return typeof v === "object" && v !== null && !Array.isArray(v) ? (v as Record<string, unknown>) : null;
  } catch {
    return null;
  }
}

/**
 * The bounded-numerics guard. Returns `{ ok: true }` when the emission carries no degenerate number;
 * otherwise `{ ok: false, reason }` naming the defect. A parse failure here is NOT this guard's concern
 * (the turn parser owns unparseable input) — an unparseable body simply passes the field check and is
 * rejected by the parser downstream. See the module header for the two checks.
 */
export function checkBoundedNumerics(text: string): { readonly ok: boolean; readonly reason?: string } {
  // 1. Pre-parse raw digit-run cap — catches the digit loop BEFORE JSON.parse coerces it away.
  const longRun = /\d{13,}/.exec(text);
  if (longRun !== null) {
    return {
      ok: false,
      reason: `degenerate numeric: a ${longRun[0].length}-digit run exceeds the ${MAX_NUMERIC_DIGITS}-digit cap (constrained-decoding digit loop, not a number)`,
    };
  }
  // 2. Per-field range sanity on the parsed turn (finite + inside NUMERIC_FIELD_BOUNDS).
  const obj = extractObject(text);
  const actions = obj !== null && Array.isArray(obj.actions) ? obj.actions : null;
  if (actions === null) return { ok: true }; // no turn shape to range-check; the parser owns that.
  for (const a of actions) {
    if (typeof a !== "object" || a === null) continue;
    const act = a as Record<string, unknown>;
    const order = act.order as Record<string, unknown> | undefined;
    if (order !== undefined && order !== null && typeof order === "object") {
      if ("qty" in order && !inBounds(order.qty, NUMERIC_FIELD_BOUNDS.qty)) {
        return { ok: false, reason: `degenerate numeric: order.qty=${JSON.stringify(order.qty)} out of range (0,${NUMERIC_FIELD_BOUNDS.qty.max}]` };
      }
      if ("strike" in order && order.strike !== undefined && !inBounds(order.strike, NUMERIC_FIELD_BOUNDS.strike)) {
        return { ok: false, reason: `degenerate numeric: order.strike=${JSON.stringify(order.strike)} out of range (0,${NUMERIC_FIELD_BOUNDS.strike.max}]` };
      }
    }
    const at = act.at as Record<string, unknown> | undefined;
    if (at !== undefined && at !== null && typeof at === "object" && at.kind === "inMinutes" && "minutes" in at) {
      if (!inBounds(at.minutes, NUMERIC_FIELD_BOUNDS.minutes)) {
        return { ok: false, reason: `degenerate numeric: scheduleWake.at.minutes=${JSON.stringify(at.minutes)} out of range [0,${NUMERIC_FIELD_BOUNDS.minutes.max}]` };
      }
    }
  }
  return { ok: true };
}
