/**
 * # session/instance — the Instance sequence runner (the multi-session horizon design, kestrel-5zl.16.1)
 *
 * A multi-session **Instance** is an ordered sequence of single-session runs (CONTEXT: *Instance /
 * Session*): a fresh {@link SessionCore} per session over its own Bus, finalised to its own Blotter, the
 * whole sequence chained by a {@link SessionCarry} (the one typed hand-off, `./carry.ts`). This runner is
 * the **one orchestration layer ABOVE** the per-session drivers — it does not touch the per-event pass.
 * Each session is driven by the existing {@link runSimulateSession}; the runner only threads the carry:
 *
 * ```
 * runInstance(sequence, cfg):
 *   carry = emptyCarry()                         // flat open, empty book
 *   for (k, session) of sequence:
 *     final = (k === last)
 *     res  = runSimulateSession({ …session, openingCarry: carry, carryClose: !final, sessionOrdinal: k })
 *     blotters.push(res.blotter); carry = res.carry
 *   return { blotters, carries, instanceRunId, … }
 * ```
 *
 * **The single-session driver is the degenerate length-1 call.** `runInstance` over a one-element sequence
 * opens with `emptyCarry()` (no seeding, no re-arm) and settles the final session at intrinsic
 * (`carryClose:false`) — so its per-session graded bus, `SimRunId`, and Blotter are **byte-identical** to a
 * standalone `runSimulateSession` of that session. That length-1 invariance is the regression guard that
 * this layer is purely additive (proven in `tests/session.instance.test.ts`).
 *
 * **Determinism / fail-closed.** The carry into session `k+1` is a pure function of session `k`'s graded
 * bus (no wall clock, no RNG crosses a boundary — RUNTIME §0), so the whole sequence replays
 * byte-identically. A structurally-incomplete carry is refused loudly by {@link assertCarry} at the seam.
 */

import type { BusEvent } from "../bus/index.ts";
import type { InstrumentSpec } from "../engine/index.ts";
import type { EpisodeSigmoidParams } from "../fill/index.ts";
import type { Blotter } from "../blotter/index.ts";
import type { Agent, CapturedTurns } from "./agent.ts";
import { emptyCarry, assertCarry, type SessionCarry, type TimescaleBand } from "./carry.ts";
import type { FillModelName } from "./sim.ts";
import { runSimulateSession } from "./simulate.ts";
import { InstanceRunId, SequenceCellKey, cellOf } from "./run-identity.ts";
import { reanchorFrontier, type TimeOfDay } from "./day.ts";

/** One session in an Instance's ordered tape (the multi-session horizon design). Exactly one of `busPath`/`events` supplies
 * the session's Bus; the per-session knobs mirror {@link import("./simulate.ts").RunSimulateOptions}. The
 * Instance-level pieces (the agent, fill model, R) live on {@link InstanceConfig}, not here. */
export interface SessionInput {
  readonly busPath?: string;
  readonly events?: readonly BusEvent[];
  /** Authored wake vantages (ET times-of-day), strictly inside this session's window. */
  readonly wakes?: readonly TimeOfDay[];
  /** The author acting cutoff (an ET time-of-day); defaults to the canonical T-5 (09:25 ET). */
  readonly authorCutoff?: TimeOfDay;
  /** Add the structural close cadence (T-2h…T-5m) for this session. */
  readonly structural?: boolean;
  readonly instruments?: readonly InstrumentSpec[];
  readonly fairTauYears?: (now: number) => number | null;
  readonly makerFairParams?: EpisodeSigmoidParams;
  /** This session's wake-cadence band (the multi-session horizon design) — overrides the Instance default. The wake-floor
   * wiring is kestrel-5zl.16.7 (P1); carried on the input now so it is a fail-closed-present field. */
  readonly band?: TimescaleBand;
}

/** The Instance-level configuration shared across every session (the multi-session horizon design). `agentFor` is a factory
 * invoked once per session, in order, with the session ordinal — so each session gets its own agent
 * (a fresh {@link import("./agent.ts").recordedAgent} for replay, or a stateful live agent that reasons
 * across days). A stateless agent is `() => sharedAgent`. */
export interface InstanceConfig {
  readonly agentFor: (ordinal: number) => Agent;
  readonly fillModel: FillModelName;
  readonly rUsd: number;
  /** The Instance's default wake-cadence band (the multi-session horizon design), seeded onto the opening carry. Default `day`. */
  readonly band?: TimescaleBand;
  /** Called ONCE per carried wake the runner cannot re-anchor onto a session (kestrel-5zl.16.9 mustFix
   * B/D) — an unresolvable `atClockET` clock, or an `inMinutes` offset that has no target vantage at this
   * seam. The runner ALSO records every drop on {@link InstanceResult.droppedWakes} (the structured report);
   * this hook is the LOUD side-channel so a drop is never merely a return value nothing reads. Omitted ⇒ the
   * real `process.stderr` (a drop is announced, never silent — AGENTS fail-closed). Off the graded path (no
   * clock/RNG read), so determinism is unaffected; a test pins it to capture the drops quietly. */
  readonly onDrop?: (drop: DroppedWake) => void;
}

/** One carried wake the runner refused to re-anchor onto a session (kestrel-5zl.16.9), tagged with the
 * ordinal of the session it was refused AT. Surfaced on {@link InstanceResult.droppedWakes} and through
 * {@link InstanceConfig.onDrop} — the observable, reasoned refusal (never a silent swallow). */
export interface DroppedWake {
  /** The ordinal `k` of the session this carried wake was refused entry to (its opening carry held it). */
  readonly sessionOrdinal: number;
  /** The pending wake's original `reason` (author/platform provenance — carried verbatim). */
  readonly reason: string;
  /** WHY it could not be re-anchored (an unresolvable clock, or a non-re-anchorable `inMinutes` offset). */
  readonly why: string;
}

/** The result of running a whole Instance (the multi-session horizon design). Per-session Blotters (each a pure projection of
 * its own graded bus, ADR-0011) and the ordered {@link SessionCarry}s, plus the per-session `SimRunId`s
 * (the graded-bus content hash the sequence identity — {@link import("./run-identity.ts").InstanceRunId},
 * kestrel-5zl.16.5 — chains). The sequence-level {@link InstanceBlotter} aggregation is 5zl.16.8 (P1). */
export interface InstanceResult {
  readonly blotters: readonly Blotter[];
  /** Each session's OUT-carry, in order — the last is the Instance's final state. */
  readonly carries: readonly SessionCarry[];
  /** Each session's captured agent turns, in order — the deterministic replay input, per session. */
  readonly captured: readonly CapturedTurns[];
  /** Each session's `SimRunId` (`= blotter.session.bus.sha256`, a pure read) — the ordered per-session
   * identities the {@link InstanceRunId} hashes into one Instance identity. */
  readonly simRunIds: readonly string[];
  /** The whole Instance's identity — `sha256` of the ORDERED per-session SimRunIds (kestrel-5zl.16.5).
   * Order-sensitive and deterministic: same tapes + same agent turns ⇒ identical id. */
  readonly instanceRunId: string;
  /** The grid CELL key for this Instance as a sequence cell (kestrel-5zl.16.5) — the ordered per-session
   * {@link cellOf} keys. A length-1 Instance's key is the single session's cell key UNCHANGED. */
  readonly sequenceCellKey: string;
  /** Each session's canonical GRADED bus, in order (a pure read of `res.gradedBus` — the judge-stamped
   * record, ADR-0010). Surfaced so a caller can assert bus-level facts a Blotter hash hides — most of all
   * that a re-anchored `atClockET` wake landed a `WAKE` checkpoint at the TARGET session's own 16:00 epoch
   * (kestrel-5zl.16.9). Byte-for-byte the same records `project(gradedBus)` folds into {@link blotters}. */
  readonly gradedBuses: readonly (readonly BusEvent[])[];
  /** Every carried wake the runner REFUSED to re-anchor, in session order (kestrel-5zl.16.9 mustFix B/D) —
   * the observable, reasoned record that an unresolvable `atClockET` / non-re-anchorable `inMinutes` failed
   * closed LOUDLY at the carry seam rather than vanishing. Empty on a clean run (every carry today, until
   * the b3l producer populates a frontier). Also streamed live through {@link InstanceConfig.onDrop}. */
  readonly droppedWakes: readonly DroppedWake[];
}

/**
 * Drive an ordered sequence of Sessions as one Instance, threading the {@link SessionCarry} (the multi-session horizon design,
 * kestrel-5zl.16.1). Each session is a fresh {@link runSimulateSession}; the runner opens session 0 flat
 * ({@link emptyCarry}), carry-marks every non-final session's settle (`carryClose`), and settles the final
 * session at intrinsic — so a length-1 sequence is byte-identical to a standalone run.
 *
 * `async` because the per-session driver is (the live loop is deliberately non-deterministic; recorded /
 * fixed adapters resolve synchronously). Refuses an empty sequence loudly (fail-closed).
 *
 * `seedCarry` (OPTIONAL, kestrel-5zl.16.9) seeds the OPENING carry of session 0 — the minimal seam that lets
 * a NON-EMPTY opening frontier be driven end-to-end BEFORE the b3l producer (kestrel-b3l.1) can populate one
 * (until it lands, `carryFrom` only ever propagates an empty frontier, so the re-anchoring path would be
 * untestable through the real runner without this). It is `assertCarry`-checked (fail-closed by omission).
 * ABSENT ⇒ the sequence opens flat ({@link emptyCarry} with the cfg band) — DEFAULT BEHAVIOUR UNCHANGED, so
 * every existing caller and the length-1 byte-invariance are untouched.
 */
export async function runInstance(
  sequence: readonly SessionInput[],
  cfg: InstanceConfig,
  seedCarry?: SessionCarry,
): Promise<InstanceResult> {
  if (sequence.length === 0) {
    throw new Error("instance: runInstance needs a non-empty session sequence (fail-closed)");
  }

  const blotters: Blotter[] = [];
  const carries: SessionCarry[] = [];
  const captured: CapturedTurns[] = [];
  const simRunIds: string[] = [];
  const gradedBuses: (readonly BusEvent[])[] = [];
  const droppedWakes: DroppedWake[] = [];

  // The LOUD drop channel (mustFix B/D): announce every non-re-anchorable carried wake, never silently. A
  // caller may capture it; the default writes the reasoned refusal to the real stderr (off the graded path).
  const onDrop =
    cfg.onDrop ??
    ((d: DroppedWake): void => {
      process.stderr.write(
        `instance: dropped a carried wake into session ${d.sessionOrdinal} — ${d.reason}: ${d.why} (fail-closed, kestrel-5zl.16.9)\n`,
      );
    });

  // The opening carry: a caller-supplied seed (fail-closed-checked), else the flat empty open. DEFAULT
  // BEHAVIOUR (no seed) is byte-identical to before — `{ ...emptyCarry(), band }`.
  let carry: SessionCarry =
    seedCarry !== undefined ? assertCarry(seedCarry) : { ...emptyCarry(), band: cfg.band ?? "day" };

  for (let k = 0; k < sequence.length; k++) {
    const session = sequence[k]!;
    const isFinal = k === sequence.length - 1;
    // Re-inject the OPENING carry's frontier as authored vantages for THIS session (kestrel-5zl.16.9): an
    // `atClockET` pending wake re-anchors to THIS session's `session_date` (its bus supplies the date to
    // `computeWakes` inside the driver — never the origin's). An unresolvable clock, and an `inMinutes`
    // offset (not re-anchorable at this seam, mustFix D), are DROPPED fail-closed and surfaced below — never
    // silently swallowed. An empty frontier (every carry today, until the b3l producer lands) re-injects
    // nothing and drops nothing, so a standalone / length-1 run is byte-identical.
    const { wakes: reanchored, dropped } = reanchorFrontier(carry.frontier);
    // Surface EVERY dropped wake with its reason (mustFix B/D): a structurally-unresolvable `atClockET` or a
    // non-re-anchorable `inMinutes` refuses LOUDLY here — recorded on the result AND streamed through
    // `onDrop`. The pre-fix seam discarded `.dropped` on the floor (the review's finding 3), so an
    // unresolvable clock was swallowed with no reason; it now fails closed observably.
    for (const d of dropped) {
      const rec: DroppedWake = { sessionOrdinal: k, reason: d.reason, why: d.why };
      droppedWakes.push(rec);
      onDrop(rec);
    }
    const seededWakes: TimeOfDay[] = [...(session.wakes ?? []), ...reanchored];
    const res = await runSimulateSession({
      ...(session.busPath !== undefined ? { busPath: session.busPath } : {}),
      ...(session.events !== undefined ? { events: session.events } : {}),
      agent: cfg.agentFor(k),
      fillModel: cfg.fillModel,
      rUsd: cfg.rUsd,
      ...(seededWakes.length > 0 ? { wakes: seededWakes } : {}),
      ...(session.authorCutoff !== undefined ? { authorCutoff: session.authorCutoff } : {}),
      ...(session.structural !== undefined ? { structural: session.structural } : {}),
      ...(session.instruments !== undefined ? { instruments: session.instruments } : {}),
      ...(session.fairTauYears !== undefined ? { fairTauYears: session.fairTauYears } : {}),
      ...(session.makerFairParams !== undefined ? { makerFairParams: session.makerFairParams } : {}),
      openingCarry: { ...carry, band: session.band ?? carry.band },
      // The FINAL session settles at intrinsic (today's terminal behaviour — the position dies); every
      // earlier session carry-marks its held inventory to close so it survives to the next open.
      carryClose: !isFinal,
      sessionOrdinal: k,
    });

    blotters.push(res.blotter);
    carries.push(res.carry);
    captured.push(res.captured);
    gradedBuses.push(res.gradedBus); // the judge-stamped record (ADR-0010) — a pure read, surfaced for assertion
    simRunIds.push(res.blotter.session.bus.sha256); // = SimRunId(gradedBus), a pure read (kestrel-5zl.7)
    carry = res.carry;
  }

  // The Instance identity chain (kestrel-5zl.16.5) — pure reads of the finalised sequence.
  const instanceRunId = InstanceRunId(simRunIds);
  const sequenceCellKey = SequenceCellKey(blotters.map((bl) => cellOf(bl)));

  return { blotters, carries, captured, simRunIds, instanceRunId, sequenceCellKey, gradedBuses, droppedWakes };
}
