/**
 * # session/carry — SessionCarry, the one typed cross-boundary hand-off (the multi-session horizon design, kestrel-5zl.16.2)
 *
 * A multi-session Instance is an **ordered sequence of single-session runs**, each a fresh
 * {@link SessionCore} over its own Bus, finalised to its own Blotter. The ONE channel between session
 * *k*'s finalize and session *k+1*'s open is a {@link SessionCarry} — a **pure `carryFrom(...)`
 * derivation** of the finalised session (a READ of the graded record, never a second writer, mirroring
 * the {@link import("./run-identity.ts").SimRunId} pure-read discipline). What rides it:
 *
 * - **`positions`** — held inventory the carry-mark settle marked to close and let survive (the lots
 *   that did NOT expiry-settle), carried at their **carry-mark** basis. Session *k+1* re-opens them as
 *   its starting book (seeded into the fill engine at that basis), so the overnight gap P&L lands in
 *   *k+1*, never in *k*.
 * - **`cash`** — the running Instance realised P&L (`prior.cash + this session's realised`). The `R`
 *   budget re-anchors per session (the session is the independent risk unit) — that is a property of the
 *   next session's construction, not of this value.
 * - **`document`** — the standing authored book (View/Wake/Plan), re-armed fresh at each session open,
 *   minting **new plan-instance ids per session** (the engine mints per-arm, so a fresh arm = fresh ids).
 * - **`frontier`** — pending unfired wakes (re-anchored to the next session). `inMinutes` offsets span
 *   any horizon natively; `atClockET` re-anchoring is the b3l router concern (kestrel-5zl.16.9, P1).
 * - **`journal`** — author memory (JOURNAL), Instance-level context so the agent reasons across days. It
 *   is author metadata, **never an engine input** (a57.11), so it cannot perturb determinism.
 *
 * **Fail-closed by omission.** Anything NOT on this struct does not survive a boundary. A Plan's `ttl` /
 * `within` window is a property of a per-session Plan instance, so it CANNOT ride the carry — it
 * re-anchors when the document re-arms. {@link assertCarry} refuses a structurally-incomplete carry
 * loudly (never a silent default). {@link emptyCarry} is the flat-open, empty-book origin of a sequence
 * (and the degenerate opening of a length-1 Instance — byte-identical to today's flat open).
 */

import type { Right } from "../bus/index.ts";
import type { Module } from "../lang/index.ts";
import type { SettleReport } from "../fill/index.ts";
import type { SessionCore } from "./sim.ts";
import type { WakeAt } from "./agent.ts";

/** The per-cell wake-cadence band (the multi-session horizon design / benchmark-matrix Axis A) — carried so a session
 * inherits the Instance's cadence. The wake-floor wiring is kestrel-5zl.16.7 (P1); the band rides the
 * carry now so it is a fail-closed-present field, defaulting to `day` (today's intraday cadence). */
export type TimescaleBand = "scalp" | "day" | "swing" | "position";

/** One held position carried across a session boundary at its **carry-mark** basis (the multi-session horizon design).
 * `basis` is the mark-at-close the prior session re-baselined to, so the next session measures P&L —
 * and lands the overnight gap — from it. `multiplier` is the instrument's contract size (1 for
 * equity/spot, 100 for a listed option). Re-opened into the next session by
 * {@link import("./sim.ts").SessionCore} via `seedFilled`. */
export interface CarriedPosition {
  readonly ref: string;
  readonly plan?: string;
  readonly plan_instance?: string;
  readonly instrument: string;
  readonly side: "buy" | "sell";
  readonly qty: number;
  readonly strike: number;
  readonly right: Right;
  /** The carry-mark (mark-at-close) — the re-baselined basis for the next session. */
  readonly basis: number;
  readonly multiplier: number;
}

/** One unfired wake carried to the next session (the multi-session horizon design), preserving its ORIGINAL
 * {@link WakeAt} intent so it can RE-ANCHOR per target session (kestrel-5zl.16.9, ADR-0015 D3/D1). The
 * runner re-injects the frontier into the next session's seed wakes via
 * {@link import("./day.ts").reanchorFrontier}: an `atClockET` clock re-resolves against the TARGET
 * session's `session_date` (never the origin's — "16:00" fires at 16:00 on the NEXT date). An `inMinutes`
 * offset is NOT re-anchorable at that seam (it is not a clock and has no target vantage to measure from), so
 * it is DROPPED there fail-closed WITH a reason (surfaced out of the runner, never silently swallowed —
 * kestrel-5zl.16.9 mustFix D); native `inMinutes` carry is the b3l producer's concern. The frontier RIDES
 * the carry as a fail-closed-present field; POPULATING it from a session's beyond-window scheduled wakes is
 * the b3l Wake-router's producer half (coordinated with kestrel-b3l.1). */
export interface PendingWake {
  readonly reason: string;
  readonly at: WakeAt;
}

/** One JOURNAL note — author memory carried as Instance-level context (never an engine input, a57.11). */
export interface JournalNote {
  readonly body: string;
}

/** The typed hand-off between session *k*'s finalize and session *k+1*'s open (the multi-session horizon design). The ONLY
 * channel across a boundary — anything not here does not survive (fail-closed by omission). */
export interface SessionCarry {
  readonly positions: readonly CarriedPosition[];
  readonly cash: number;
  readonly document: Module | null;
  readonly frontier: readonly PendingWake[];
  readonly journal: readonly JournalNote[];
  readonly band: TimescaleBand;
  /** The ordinal `k` of the session this carry came OUT of — the prior session index (`-1` at the
   * sequence origin, before any session has run). The next session is `priorOrdinal + 1`. */
  readonly priorOrdinal: number;
}

/** The flat-open, empty-book origin of a sequence (the multi-session horizon design) — no inventory, zero running cash, no
 * standing document, an empty frontier/journal, the default `day` band, and `priorOrdinal = -1`. The
 * degenerate opening of a length-1 Instance: constructing a {@link import("./sim.ts").SessionCore} with
 * this carry is byte-identical to today's flat open (no seeding, no re-arm). */
export function emptyCarry(): SessionCarry {
  return { positions: [], cash: 0, document: null, frontier: [], journal: [], band: "day", priorOrdinal: -1 };
}

/** Fail-closed structural check (the multi-session horizon design fail-closed-by-omission): every required field must be
 * present and well-typed, or the carry is refused loudly — never a silent default at the boundary. */
export function assertCarry(c: SessionCarry): SessionCarry {
  const bad = (why: string): never => {
    throw new Error(`carry: refused — ${why} (fail-closed by omission, the multi-session horizon design)`);
  };
  if (!Array.isArray(c.positions)) bad("positions missing");
  if (typeof c.cash !== "number" || !Number.isFinite(c.cash)) bad("cash missing or non-finite");
  if (c.document !== null && typeof c.document !== "object") bad("document must be a Module or null");
  if (!Array.isArray(c.frontier)) bad("frontier missing");
  if (!Array.isArray(c.journal)) bad("journal missing");
  if (c.band !== "scalp" && c.band !== "day" && c.band !== "swing" && c.band !== "position") bad("band invalid");
  if (typeof c.priorOrdinal !== "number") bad("priorOrdinal missing");
  for (const p of c.positions) {
    if (typeof p.ref !== "string" || p.ref.length === 0) bad("position ref missing");
    if (typeof p.qty !== "number" || !Number.isFinite(p.qty)) bad(`position ${p.ref} qty invalid`);
    if (typeof p.basis !== "number" || !Number.isFinite(p.basis)) bad(`position ${p.ref} basis invalid`);
    if (typeof p.multiplier !== "number" || !Number.isFinite(p.multiplier)) bad(`position ${p.ref} multiplier invalid`);
  }
  return c;
}

/** Inputs to {@link carryFrom} — the finalised session and the carry it opened with. */
export interface CarryFromArgs {
  /** The finalised {@link SessionCore} (after `settle`) — read for its carried inventory + standing doc. */
  readonly core: SessionCore;
  /** This session's settle report — its `floorTotal` is the session's realised P&L. */
  readonly settle: SettleReport;
  /** The carry this session OPENED with — the source of running cash + the carried band. */
  readonly prior: SessionCarry;
  /** This session's ordinal `k`. The produced carry's `priorOrdinal` is `k`. */
  readonly ordinal: number;
}

/**
 * Derive the {@link SessionCarry} handed to session `ordinal+1` — a PURE read of the finalised session
 * (the multi-session horizon design, kestrel-5zl.16.2). Never writes a byte back onto the graded bus or Blotter.
 *
 * - `positions` = the carry-mark lots the settle marked-and-kept ({@link SessionCore.carriedInventory}).
 * - `cash` = `prior.cash + settle.floorTotal` (the session's realised, chained into the Instance total).
 * - `document` = the session's standing authored module (the last armed), else the prior document — so a
 *   session that authors nothing still re-arms the standing book next session.
 * - `journal` = the prior journal plus this session's JOURNAL bodies (accumulated author memory).
 * - `frontier` = the prior frontier (the runner re-injects it into the next session's seed wakes via
 *   {@link import("./day.ts").reanchorFrontier}, 5zl.16.9; POPULATING it from beyond-window scheduled wakes
 *   is the b3l router's producer half).
 */
export function carryFrom(args: CarryFromArgs): SessionCarry {
  const { core, settle, prior, ordinal } = args;
  const mult = core.execSpec.multiplier;
  const positions: CarriedPosition[] = core.carriedInventory().map((lot) => ({
    ref: lot.order.ref,
    ...(lot.order.plan !== undefined ? { plan: lot.order.plan } : {}),
    ...(lot.order.plan_instance !== undefined ? { plan_instance: lot.order.plan_instance } : {}),
    instrument: lot.order.instrument,
    side: lot.order.side,
    qty: lot.order.qty,
    strike: lot.order.strike,
    right: lot.order.right,
    basis: lot.basis,
    multiplier: mult,
  }));
  const journalNotes: JournalNote[] = core.emitted.events
    .filter((e): e is Extract<typeof e, { stream: "JOURNAL" }> => e.stream === "JOURNAL")
    .map((e) => ({ body: e.body }));
  const standing = core.standingModule;
  return assertCarry({
    positions,
    cash: prior.cash + settle.floorTotal,
    document: standing ?? prior.document,
    frontier: prior.frontier,
    journal: [...prior.journal, ...journalNotes],
    band: prior.band,
    priorOrdinal: ordinal,
  });
}
