/**
 * # ledger/sim-runs — the regenerable sim_runs + agent_configs projection (kestrel-5zl.9)
 *
 * A QUERY-ONLY, REGENERABLE index over the finalized {@link ../blotter Blotter}s (the graded-run records),
 * surfaced additively off the ledger barrel ({@link ./index.ts}) alongside the existing run/plan/lineage
 * ledger — which it never touches. Unlike that ledger (a durable `bun:sqlite` store of {@link EpisodeReport}s),
 * this is a PURE PROJECTION: a deterministic function of the source Blotters, NOT a stateful mutable store.
 * "Regenerable" means projecting the same source twice yields a BYTE-IDENTICAL index — no wall clock, no RNG,
 * no accumulation across calls, no hidden persisted state (RUNTIME §0). It reads the source and answers
 * queries; it never mutates the source, the runs, or persists anything.
 *
 * Two views + the fail-closed exclusions:
 *
 *   • sim_runs      — one row per {@link import("../session/run-identity").SimRunId SimRunId} (the graded-bus
 *                     content hash the projector already computes as `blotter.session.bus.sha256` — a pure
 *                     read, not a second stamp), carrying its cell identity
 *                     ({@link import("../session/run-identity").cellOf CellKey}), its `ConfigId`
 *                     (`session.config_id`, m9i.2), and the a57.14 experimental **envelope**
 *                     (`session.envelope`). Queryable by run / cell / config.
 *   • agent_configs — the AgentConfig registry: one row per DISTINCT ConfigId (so runs group by config),
 *                     carrying that config's envelope. Replicate runs (same `config_id`, byte-different graded
 *                     buses, distinct SimRunIds) collapse to ONE registry row; two distinct configs are two.
 *   • excluded      — the FAIL-CLOSED refusals: a Blotter missing its a57.14 envelope OR its m9i.2 ConfigId is
 *                     EXCLUDED-WITH-REASON, never silently indexed with a fabricated/blank identity. The row is
 *                     still keyed by the always-present graded-bus receipt (`session.bus.sha256`), so the
 *                     excluded record is NAMED — a refusal is an audit entry, never a blank.
 *
 * The keys are read from the codebase's ONE definition of each (5zl.7 `cellOf`, m9i.2 `session.config_id`,
 * a57.14 `session.envelope`), never re-derived here — the projection carries identity, it does not mint it.
 */

import type { Blotter, ExperimentalEnvelope } from "../blotter/index.ts";
import { cellOf } from "../session/run-identity.ts";

// ─────────────────────────────────────────────────────────────────────────────
// Row shapes — the two views + the fail-closed exclusion, all pure values
// ─────────────────────────────────────────────────────────────────────────────

/** A sim_runs row — keyed by SimRunId, carrying its cell identity, ConfigId, and the a57.14 envelope. */
export interface SimRunRow {
  /** The run identity — `blotter.session.bus.sha256` (the SimRunId, a pure read of the graded bus receipt). */
  readonly sim_run_id: string;
  /** The comparison cell this run belongs to — `cellOf(blotter)` (tape × fill_model × config, 5zl.7). */
  readonly cell_key: string;
  /** The run's FULL ConfigId — `session.config_id` (m9i.2); the identity agent_configs groups by. */
  readonly config_id: string;
  /** The a57.14 experimental envelope, carried VERBATIM from `session.envelope`. */
  readonly envelope: ExperimentalEnvelope;
}

/** An agent_configs row — one per DISTINCT ConfigId, carrying that config's envelope. */
export interface AgentConfigRow {
  readonly config_id: string;
  readonly envelope: ExperimentalEnvelope;
}

/** A fail-closed exclusion — a Blotter whose a57.14/m9i.2 provenance is absent, NAMED (never blank-keyed).
 * `sim_run_id` still identifies WHICH record was refused (the graded-bus receipt is always present). */
export interface ExcludedRow {
  readonly sim_run_id: string;
  readonly reason: string;
}

/** The regenerable index — a PURE VALUE (no handle, no store): the two views + the fail-closed exclusions.
 * Every array is in a DETERMINISTIC sorted order (sim_runs / excluded by `sim_run_id`, agent_configs by
 * `config_id`), so the index is byte-stable and order-insensitive over the source set. */
export interface SimRunsIndex {
  readonly sim_runs: readonly SimRunRow[];
  readonly agent_configs: readonly AgentConfigRow[];
  readonly excluded: readonly ExcludedRow[];
}

// ─────────────────────────────────────────────────────────────────────────────
// The projection — a deterministic fold of the source Blotters (pure, fail-closed)
// ─────────────────────────────────────────────────────────────────────────────

const bySimRunId = (a: { sim_run_id: string }, b: { sim_run_id: string }): number =>
  a.sim_run_id < b.sim_run_id ? -1 : a.sim_run_id > b.sim_run_id ? 1 : 0;

const byConfigId = (a: { config_id: string }, b: { config_id: string }): number =>
  a.config_id < b.config_id ? -1 : a.config_id > b.config_id ? 1 : 0;

/**
 * Project the finalized Blotters into the regenerable sim_runs + agent_configs index (kestrel-5zl.9).
 *
 * A deterministic fold — for each Blotter it READS `session.bus.sha256` (the SimRunId), `cellOf(blotter)`,
 * `session.config_id`, and `session.envelope`; nothing is stamped back and nothing is mutated. A Blotter that
 * carries BOTH a complete a57.14 envelope AND a non-empty m9i.2 ConfigId becomes a sim_runs row and registers
 * its agent_configs row; a Blotter missing EITHER is FAIL-CLOSED to an `excluded` row whose `reason` names the
 * absent provenance — no sim_runs row is ever synthesized with an absent envelope or a blank ConfigId.
 *
 * Regenerable: keyed folds (SimRunId → run, ConfigId → registry, SimRunId → exclusion) collapse duplicate
 * source records idempotently (mirroring the ledger's INSERT-OR-REPLACE doctrine), and every output array is
 * SORTED by its stable key — so the same source Blotters (in any order) yield a BYTE-IDENTICAL index. Pure:
 * no wall clock, no RNG, no accumulation across calls. Query-only: the source Blotters are never mutated.
 */
export function projectSimRuns(blotters: readonly Blotter[]): SimRunsIndex {
  const runs = new Map<string, SimRunRow>();
  const configs = new Map<string, AgentConfigRow>();
  const excluded = new Map<string, ExcludedRow>();

  for (const blotter of blotters) {
    const session = blotter.session;
    const simRunId = session.bus.sha256; // the SimRunId — a pure read of the graded-bus receipt (5zl.7)
    const envelope = session.envelope; // a57.14: present ONLY when the graded bus declared a complete one
    const configId = session.config_id; // m9i.2: the driver-stamped FULL ConfigId (absent on a config-less bus)

    // FAIL-CLOSED: an absent experimental envelope OR an absent/blank ConfigId is a typed refusal — the
    // provenance the index keys on is missing, so the run is EXCLUDED-WITH-REASON, never blank-keyed.
    if (envelope === undefined || configId === undefined || configId.length === 0) {
      const missing: string[] = [];
      if (envelope === undefined) missing.push("experimental envelope (a57.14)");
      if (configId === undefined || configId.length === 0) missing.push("config_id (m9i.2)");
      excluded.set(simRunId, {
        sim_run_id: simRunId,
        reason: `excluded — missing ${missing.join(" and ")}`,
      });
      continue;
    }

    // A valid experimental run: index it, and register its config in the distinct-ConfigId registry. The
    // envelope is a SUPERSET-determined function of the ConfigId, so replicates under one config carry the
    // same envelope — first-registered is deterministic under the sorted output.
    runs.set(simRunId, { sim_run_id: simRunId, cell_key: cellOf(blotter), config_id: configId, envelope });
    if (!configs.has(configId)) configs.set(configId, { config_id: configId, envelope });
  }

  return {
    sim_runs: [...runs.values()].sort(bySimRunId),
    agent_configs: [...configs.values()].sort(byConfigId),
    excluded: [...excluded.values()].sort(bySimRunId),
  };
}

// ─────────────────────────────────────────────────────────────────────────────
// Query API — by run / cell / config, over the pure index VALUE (never a source read)
// ─────────────────────────────────────────────────────────────────────────────

/** The sim_runs row for a SimRunId, or `undefined` when the index holds no such run. */
export function simRunById(index: SimRunsIndex, simRunId: string): SimRunRow | undefined {
  return index.sim_runs.find((r) => r.sim_run_id === simRunId);
}

/** The replicate runs in one comparison cell (the runs sharing a CellKey), in the index's sorted order. */
export function runsByCell(index: SimRunsIndex, cellKey: string): readonly SimRunRow[] {
  return index.sim_runs.filter((r) => r.cell_key === cellKey);
}

/** The runs grouped under one distinct ConfigId, in the index's sorted order. */
export function runsByConfig(index: SimRunsIndex, configId: string): readonly SimRunRow[] {
  return index.sim_runs.filter((r) => r.config_id === configId);
}
