/**
 * # ledger/core — the storage-agnostic heart of the run / plan / lineage ledger (ADR-0006, ADR-0007)
 *
 * The ledger has TWO backends that must agree byte-for-byte on WHAT a run's identity is and HOW a
 * {@link EpisodeReport} reduces to rows:
 * - the durable {@link ./index.ts `bun:sqlite`} store the CLI auto-records into (`data/kestrel.db`), and
 * - the {@link ./memory.ts in-memory} store that runs inside a Cloudflare Worker isolate, where
 *   `bun:sqlite` does not exist (ADR-0006, EPIC kestrel-markets-alw; kestrel-alw.17).
 *
 * This module is that shared heart: the row/query TYPES both backends return, the deterministic
 * {@link deriveRunId run-identity hash}, and the pure {@link aggregatePlans report → per-plan lineage}
 * reduction. It imports NOTHING Bun-only (its only digest is the portable {@link ../crypto/sha256.ts
 * sha256}), so it — and any backend built on it — loads and runs in a Worker isolate. No wall clock,
 * no RNG (RUNTIME §0).
 */

import { sha256 } from "../crypto/sha256.ts";
import type { PlanTrace, EpisodeReport } from "../session/sim.ts";
import type { OrderRole } from "../engine/index.ts";

// ─────────────────────────────────────────────────────────────────────────────
// Row + query types (identical across the sqlite and in-memory backends)
// ─────────────────────────────────────────────────────────────────────────────

/** A run row as stored (the `runs` table, typed). */
export interface RunRow {
  readonly run_id: string;
  readonly recorded_at: number;
  readonly session_date: string;
  readonly mode: string;
  readonly instruments: string;
  readonly bus_sha256: string;
  readonly plans_sha256: string;
  readonly fill_model: string;
  readonly calibrated: number;
  readonly determinism_hash: string;
  readonly r_usd: number;
  readonly realized_floor_usd: number;
  readonly expected_usd: number;
  readonly premium_spent: number;
  readonly report_path: string | null;
  readonly bus_events: number;
}

/** A plan-instance row as stored (the `plan_instances` table, typed). */
export interface PlanInstanceRow {
  readonly run_id: string;
  readonly name: string;
  readonly canonical_sha256: string;
  readonly final_state: string;
  readonly outcome: string | null;
  readonly fired: number;
  readonly orders: number;
  readonly fills: number;
  readonly realized_usd: number;
  readonly expected_usd: number;
}

/** A per-plan × role order-summary row (the `orders_summary` table, typed). */
export interface OrderSummaryRow {
  readonly run_id: string;
  readonly name: string;
  readonly role: string;
  readonly count: number;
  readonly esc_stages_max: number;
  readonly reprice_count: number;
}

/** A run joined with its plan instances (the `getRun` shape). */
export interface RunWithPlans {
  readonly run: RunRow;
  readonly plans: readonly PlanInstanceRow[];
}

/** One plan instance in a lineage, carrying the run context needed to place it in time. */
export interface LineageInstance extends PlanInstanceRow {
  readonly recorded_at: number;
  readonly session_date: string;
  readonly mode: string;
  readonly fill_model: string;
}

/** A per-lineage aggregate row (the `leaderboard` shape). */
export interface LeaderboardRow {
  readonly name: string;
  readonly runs: number;
  readonly fired: number;
  readonly fills: number;
  readonly realized_sum: number;
  readonly expected_sum: number;
  readonly realized_avg: number;
  readonly expected_avg: number;
}

/** Filters for `listRuns`. All optional; `lineage` matches runs that contain a plan instance of that name. */
export interface ListRunsFilter {
  readonly sessionDate?: string;
  readonly mode?: string;
  readonly fillModel?: string;
  readonly lineage?: string;
}

/** Filters for `leaderboard`: optional `mode` and an inclusive `since` lower bound on `recorded_at`. */
export interface LeaderboardFilter {
  readonly mode?: string;
  readonly since?: number;
}

/** Options for `record`. `plansText` is the **canonical** plans text (the caller canonicalizes; the
 * ledger only hashes it). `rUsd` is a run input needed for the run-identity hash + the `r_usd` column.
 * `now` is the caller-injected `recorded_at` (never wall-clock). `calibrated` marks whether the run's
 * fill model was calibrated (default `false`). */
export interface RecordOptions {
  readonly plansText: string;
  readonly reportPath?: string;
  readonly now: number;
  readonly rUsd: number;
  readonly calibrated?: boolean;
}

/** The identity + derived hashes computed while recording a run. */
export interface RecordResult {
  readonly runId: string;
  readonly plansSha256: string;
}

// ─────────────────────────────────────────────────────────────────────────────
// Run identity (ADR-0007) — a pure function of the four inputs, shared by both backends
// ─────────────────────────────────────────────────────────────────────────────

/**
 * The deterministic run identity (ADR-0007): a pure function of the four inputs that fix what was
 * graded and how. Fields are newline-delimited so distinct field boundaries can never collide, and
 * `r_usd` is stringified stably. Same inputs → same `run_id` → idempotent upsert. Uses the portable
 * {@link ../crypto/sha256.ts sha256} so the id is byte-identical under Bun and inside a Worker isolate.
 */
export function deriveRunId(busSha256: string, plansSha256: string, fillModel: string, rUsd: number): string {
  return sha256(`${busSha256}\n${plansSha256}\n${fillModel}\n${String(rUsd)}`);
}

/** sha256 of the canonical plans text — the `plans_sha256` column + a run-identity input. */
export function plansSha256Of(plansText: string): string {
  return sha256(plansText);
}

/**
 * The ONE ranking tiebreak comparator shared by both ledger backends (kestrel-uary). Compares strings
 * by code unit (`a < b`), matching SQLite's default **BINARY** collation on a `TEXT` column
 * (`ORDER BY name ASC`) — for the ASCII / BMP plan names the ledger orders, code-unit order equals
 * UTF-8 byte order equals code-point order, so the in-isolate in-memory backend lands the SAME row
 * order as a `bun:sqlite` recomputation on any host.
 *
 * `String.prototype.localeCompare` must NEVER be used on the ledger/report path: it is host-locale
 * dependent and, for mixed-case / non-ASCII names (e.g. `'a'` vs `'B'`), orders differently from
 * BINARY — which would make hosted `getRun`/`leaderboard`/`listRuns`/`lineage` order rows differently
 * from a local SQLite recomputation, breaking the KestrelBench recomputability claim (ADR-0006/0007).
 */
export function compareCodePoint(a: string, b: string): number {
  return a < b ? -1 : a > b ? 1 : 0;
}

// ─────────────────────────────────────────────────────────────────────────────
// Report → per-plan lineage reduction (pure; shared by both backends)
// ─────────────────────────────────────────────────────────────────────────────

/** One plan's aggregated lineage instance, derived from the report's plan trace + its orders. */
export interface AggregatedPlan {
  readonly name: string;
  readonly finalState: string;
  readonly outcome: string | undefined;
  readonly fired: boolean;
  readonly orders: number;
  readonly fills: number;
  readonly realizedUsd: number;
  readonly expectedUsd: number;
  readonly roles: readonly { readonly role: OrderRole; readonly count: number; readonly escStagesMax: number }[];
}

/**
 * Pure reduction of a {@link EpisodeReport} into per-plan **lineage** rows (ADR-0006: the plan NAME is the
 * aggregation key). The report now carries one trace per exact Plan INSTANCE (kestrel-22j.15), so a run that
 * authored a name across MULTIPLE instances yields multiple same-name traces; the ledger's `plan_instances`
 * table keys on `(run_id, name)`, so those instances ROLL UP into ONE per-run lineage row here — never two
 * rows colliding on the primary key. Orders join by name (spanning the name's instances), and the aggregate
 * `final_state`/`outcome`/`fired` reflect the name's LAST instance in the run (deterministic: report order).
 * A name with a single instance (the common case) rolls up to exactly the prior one-trace behaviour.
 */
export function aggregatePlans(report: EpisodeReport): AggregatedPlan[] {
  const names: string[] = [];
  const tracesByName = new Map<string, PlanTrace[]>();
  for (const t of report.plans) {
    let g = tracesByName.get(t.plan);
    if (g === undefined) {
      g = [];
      tracesByName.set(t.plan, g);
      names.push(t.plan);
    }
    g.push(t);
  }

  return names.map((name) => {
    const traces = tracesByName.get(name)!;
    const orders = report.orders.filter((o) => o.plan === name);
    const fills = orders.filter((o) => o.filled).length;
    const realizedUsd = orders.reduce((s, o) => s + o.floorPnl, 0);
    const expectedUsd = orders.reduce((s, o) => s + o.expectedPnl, 0);

    // The name reached (or passed through) the `fired` state in ANY of its instances, or it produced orders.
    const lifecycleFired = traces.some((t) => t.lifecycle.some((s) => s.state === "fired" || s.state === "managing"));
    // The terminal outcome/state of the name's LAST instance in the run (its most recent standing).
    const lastTrace = traces[traces.length - 1]!;
    const lastStep = lastTrace.lifecycle[lastTrace.lifecycle.length - 1];
    const outcome = lastStep?.outcome;

    // Lean per-role rollup: count + max esc-stage occupied, by order role (across the name's instances).
    const roleAgg = new Map<OrderRole, { count: number; escStagesMax: number }>();
    for (const o of orders) {
      const cur = roleAgg.get(o.role) ?? { count: 0, escStagesMax: 0 };
      cur.count += 1;
      cur.escStagesMax = Math.max(cur.escStagesMax, o.esc_stages);
      roleAgg.set(o.role, cur);
    }
    const roles = [...roleAgg.entries()]
      .map(([role, v]) => ({ role, count: v.count, escStagesMax: v.escStagesMax }))
      .sort((a, b) => compareCodePoint(a.role, b.role));

    return {
      name,
      finalState: String(lastTrace.final_state),
      outcome,
      fired: lifecycleFired || orders.length > 0,
      orders: orders.length,
      fills,
      realizedUsd,
      expectedUsd,
      roles,
    };
  });
}
