/**
 * kestrel.markets/protocol — the incremental SESSION contract (protocol v0.2).
 *
 * This module is the L0 CONTRACT of the transport-neutral Session interaction
 * (djm.2 AC3/AC4; the schema the djm.4 controller and every face encode over).
 * It is the hash-chained transcript vocabulary of ONE agent-day: a genesis seals
 * the pure SessionSpec (SessionId = genesis hash); turn entries carry the binding
 * pentad `(sessionId, ordinal, parentHash, frameRoot)` plus the exact authored
 * bytes; a finalize seal closes the chain into a receipt root a CertifiedGrade
 * can pin. Idempotency IS content-addressing; a changed-bytes-same-slot turn
 * FAILS CLOSED. No face may reimplement Session progression — the wire verbs
 * deliberately omit an `advance`.
 *
 * HARD CONSTRAINTS (identical to src/protocol/index.ts — see its header):
 *  - ZERO RUNTIME dependencies. The ONLY import is intra-protocol
 *    (`./index.ts`, itself dependency-free); this module pulls in NO engine /
 *    session / blotter / fill / lang / grade / frame / render / ledger / cli
 *    code and must typecheck with `chdb` uninstalled. The import-graph boundary
 *    is an automated CI invariant (tests/protocol.boundary.test.ts).
 *  - DETERMINISM. Pure types + own constants; no wall clock, no RNG.
 *  - FAIL CLOSED. Unknown / changed-bytes-same-identity ⇒ a typed diagnostic,
 *    never a silent accept.
 *  - GENERIC ONLY. No founding-app tickers or strategy names in types, comments,
 *    or examples. Product identities ride INSIDE these fields at runtime.
 */

import type { EventCursor } from "./index.ts";

/* ───────────────────────── Operation vs Session identity ───────────────────── */

declare const OperationIdBrand: unique symbol;
declare const SessionIdBrand: unique symbol;

/**
 * CONTROL-PLANE identity (ADR-0004). One durable, resumable Operation carries a
 * canonical agent intent across the free boundary, machine settlement, human
 * completion, callbacks, and retries WITHOUT changing. It is minted by the
 * platform and is NOT derived from content — distinct in kind from a
 * {@link SessionId}. Branded so the two identities are never interchangeable.
 */
export type OperationId = string & { readonly [OperationIdBrand]: "OperationId" };

/**
 * DETERMINISTIC identity: the genesis-entry hash of a Session's transcript. Same
 * SessionSpec ⇒ same genesis ⇒ same `SessionId`, on any host (a self-hosted
 * Kestrel and the managed backend agree). Branded distinctly from
 * {@link OperationId}: a deterministic Session identity is NOT a control-plane
 * handle, and the compiler refuses to substitute one for the other.
 */
export type SessionId = string & { readonly [SessionIdBrand]: "SessionId" };

/* ─────────────────────────── the reserved OPEN ordinal ─────────────────────── */

/**
 * Turn ordinal of the date-blind OPEN Frame — the author's first answer, before
 * any Wake. Reserved as `-1` so real Wake ordinals stay `0,1,2,…` and the OPEN
 * turn is never confused with a Wake turn.
 */
export const OPEN_ORDINAL = -1 as const;

/* ───────────────────────────── turn bodies ─────────────────────────────────── */

/**
 * Why a turn was recorded as a host failure (djm.4 fail semantics 3). A recorded
 * failure is FOREVER distinguishable from a judged STAND_DOWN — a closed
 * vocabulary so a downstream reader can dispatch exhaustively.
 */
export type FailureClass = "provider" | "timeout" | "malformed";

/** Runtime tuple of every {@link FailureClass}. Same both-ways guard as the index rdu vocabularies. */
export const FAILURE_CLASSES = ["provider", "timeout", "malformed"] as const satisfies readonly FailureClass[];
type _FailureClassesExhaustive<_Missing extends never = Exclude<FailureClass, (typeof FAILURE_CLASSES)[number]>> = true;

/**
 * What an author actually produced for a turn — the three OUTCOMES kept forever
 * distinguishable (djm.4 fail semantics; the bead's distinguishability
 * criterion becomes a recorded fact):
 *  - `authored`   a Kestrel document was authored; `bytes` are the exact source.
 *  - `stand-down` the author EXPLICITLY stood down (a judgment, gradable) with a
 *                 stated reason — NOT a failure.
 *  - `failure`    the host recorded a provider/timeout/malformed failure — the
 *                 author never got to answer; never confused with a STAND_DOWN.
 */
export type TurnBody =
  | { readonly kind: "authored"; readonly bytes: string }
  | { readonly kind: "stand-down"; readonly reason: string }
  | { readonly kind: "failure"; readonly failureClass: FailureClass };

/* ──────────────────────────── transcript entry ─────────────────────────────── */

/**
 * One turn's transcript entry — the content-addressed record of an answer bound
 * to its SLOT. The BINDING PENTAD `(sessionId, ordinal, parentHash, frameRoot)`
 * is the turn's SEMANTIC-EFFECT IDENTITY: the position in the chain the author
 * answered. `authoredSha256` addresses the authored bytes; `entryHash` addresses
 * the whole entry (its content address — the next turn's `parentHash`).
 */
export interface TurnEntry {
  readonly kind: "turn";
  /** The Session this turn belongs to (= genesis hash). */
  readonly sessionId: SessionId;
  /** Turn ordinal; {@link OPEN_ORDINAL} for the OPEN turn, `0,1,2,…` for Wakes. */
  readonly ordinal: number;
  /** Content address of the PRIOR entry (the chain link / prior cursor). */
  readonly parentHash: string;
  /** sha256 of the canonical delivered Frame this turn answers. */
  readonly frameRoot: string;
  /** sha256 of the exact authored bytes (idempotency address). */
  readonly authoredSha256: string;
  /** What the author produced. */
  readonly body: TurnBody;
  /** Content address of THIS whole entry (the next turn's `parentHash`). */
  readonly entryHash: string;
}

/* ─────────────────────── closed fail-closed diagnostics ────────────────────── */

/**
 * The closed diagnostic vocabulary of the Session layer (djm.4 L0). Every
 * rejection is one of these typed reasons — never a crash, never a silent false:
 *  - `wrong-session`  the entry names a different Session.
 *  - `broken-chain`   the `parentHash`/ordinal does not continue the chain.
 *  - `turn-conflict`  changed bytes under the SAME semantic-effect slot (AC4).
 *  - `stale-frame`    the turn answers a superseded / non-current Frame.
 *  - `not-pending`    no turn is pending at this slot (phase gate; djm.4 L1).
 *  - `sealed`         the chain is finalized; no further turns admit (djm.4 L1).
 *  - `malformed-entry` the entry is structurally unreadable.
 */
export type SessionDiagnostic =
  | "wrong-session"
  | "broken-chain"
  | "turn-conflict"
  | "stale-frame"
  | "not-pending"
  | "sealed"
  | "malformed-entry";

/**
 * Runtime tuple of every {@link SessionDiagnostic}, in the L0 order. Same
 * both-ways closed-vocabulary guard as the index rdu vocabularies: a diagnostic
 * added to (or removed from) the union without editing this tuple breaks the
 * build, so the runtime vocabulary can never silently drift from the type.
 */
export const SESSION_DIAGNOSTICS = [
  "wrong-session",
  "broken-chain",
  "turn-conflict",
  "stale-frame",
  "not-pending",
  "sealed",
  "malformed-entry",
] as const satisfies readonly SessionDiagnostic[];
type _SessionDiagnosticsExhaustive<_Missing extends never = Exclude<SessionDiagnostic, (typeof SESSION_DIAGNOSTICS)[number]>> = true;

/* ──────────────────────── incremental message union ────────────────────────── */

/**
 * `session/open` — START: mint a Session against a sealed genesis. `sessionId`
 * (base) is the genesis hash; `genesisRoot` is the content root of the pure
 * SessionSpec the genesis seals. Optionally binds the control-plane
 * {@link OperationId} carrying this Session.
 */
export interface OpenMessage {
  readonly kind: "session/open";
  readonly sessionId: SessionId;
  readonly operationId?: OperationId;
  /** Content root of the sealed SessionSpec (pure data) the genesis pins. */
  readonly genesisRoot: string;
}

/**
 * `session/frame` — OPEN DELIVERY and WAKE-DELTA: deliver the Frame the author
 * must answer at `ordinal` ({@link OPEN_ORDINAL} for OPEN, `0,1,…` for a Wake
 * delta), carrying its content-addressed `frameRoot` and the `parentHash` (prior
 * cursor) the answer must bind to.
 */
export interface FrameMessage {
  readonly kind: "session/frame";
  readonly sessionId: SessionId;
  readonly ordinal: number;
  readonly frameRoot: string;
  readonly parentHash: string;
}

/**
 * `session/turn` — AUTHORED-OR-STAND-DOWN and REVISION: the author's answer to a
 * delivered slot. Carries the full binding pentad header plus the {@link TurnBody}.
 * A revision is another turn at the SAME slot; reconciliation ({@link reconcileTurn})
 * decides idempotent-duplicate vs. fail-closed conflict.
 */
export interface TurnMessage {
  readonly kind: "session/turn";
  readonly sessionId: SessionId;
  readonly ordinal: number;
  readonly parentHash: string;
  readonly frameRoot: string;
  readonly authoredSha256: string;
  readonly body: TurnBody;
}

/**
 * `session/events` — RESUME: replay graded events after a cursor. The SSE event
 * id IS the {@link EventCursor} token, so `Last-Event-ID` resume and this request
 * are one mechanism (ADR-0004 resumable cursors). Absent `after` ⇒ from genesis.
 */
export interface EventsMessage {
  readonly kind: "session/events";
  readonly sessionId: SessionId;
  readonly after?: EventCursor;
}

/**
 * `session/describe` — self-describe the Session's pinned catalog entry / spec.
 * `entryRoot` is the content root of the CatalogEntry (see ./catalog.ts) this
 * Session runs, so a client can fetch the full reproducibility surface.
 */
export interface DescribeMessage {
  readonly kind: "session/describe";
  readonly sessionId: SessionId;
  readonly entryRoot: string;
}

/**
 * `session/finalize` — FINALIZATION and ARTIFACTS: seal the chain. `tipHash` is
 * the receipt root a CertifiedGrade can pin; `artifacts` are content-addressed
 * handles to the delivered Blotter/Grade/report artifacts.
 */
export interface FinalizeMessage {
  readonly kind: "session/finalize";
  readonly sessionId: SessionId;
  readonly tipHash: string;
  readonly artifacts: readonly string[];
}

/**
 * The incremental Session wire message union (djm.2 AC3). Discriminated on
 * `kind`. Every message binds the {@link SessionId}; turn/frame messages bind the
 * prior cursor and delivered Frame root. Deliberately NO `advance` verb: a face
 * is a pure encoder over the controller and structurally cannot reimplement
 * Session progression (djm.4 FACES).
 */
export type SessionMessage =
  | OpenMessage
  | FrameMessage
  | TurnMessage
  | EventsMessage
  | DescribeMessage
  | FinalizeMessage;

/** The `kind` discriminant of {@link SessionMessage} — the frozen wire verb space. */
export type SessionMessageKind = SessionMessage["kind"];

/**
 * Every Session wire verb as a runtime tuple — the closed `kind` vocabulary of
 * {@link SessionMessage}. Driven by the union itself (`satisfies readonly
 * SessionMessage["kind"][]`) so adding a message forces this tuple to grow in
 * lockstep. Same both-ways closed-vocabulary guard as `RECEIPT_KINDS`: a `kind`
 * added to (or removed from) the union without editing this tuple breaks the
 * build. Fail-closed, no drift — and no `advance`.
 */
export const SESSION_MESSAGE_KINDS = [
  "session/open",
  "session/turn",
  "session/frame",
  "session/events",
  "session/describe",
  "session/finalize",
] as const satisfies readonly SessionMessage["kind"][];
type _SessionMessageKindsExhaustive<_Missing extends never = Exclude<SessionMessage["kind"], (typeof SESSION_MESSAGE_KINDS)[number]>> = true;

/* ─────────────────────── required incremental interactions ─────────────────── */

/**
 * The incremental interactions djm.2 AC3 enumerates, in lifecycle order. Each is
 * SERVED BY a wire verb via {@link INTERACTION_MESSAGE}; they are the lifecycle
 * STEPS, distinct from the {@link SessionMessageKind} VERBS that carry them.
 */
export type SessionInteraction =
  | "start"
  | "open-delivery"
  | "authored-or-stand-down"
  | "wake-delta"
  | "revision"
  | "resume"
  | "finalization"
  | "artifacts";

/** Runtime tuple of every {@link SessionInteraction}, in lifecycle order. Same both-ways guard. */
export const SESSION_INTERACTIONS = [
  "start",
  "open-delivery",
  "authored-or-stand-down",
  "wake-delta",
  "revision",
  "resume",
  "finalization",
  "artifacts",
] as const satisfies readonly SessionInteraction[];
type _SessionInteractionsExhaustive<_Missing extends never = Exclude<SessionInteraction, (typeof SESSION_INTERACTIONS)[number]>> = true;

/**
 * The TOTAL map proving every required interaction is served by a real wire verb
 * — `Record<SessionInteraction, SessionMessageKind>`, so a missing interaction or
 * an unknown verb fails the build. Several interactions share a verb (the wire
 * vocabulary is intentionally smaller than the interaction set): authored /
 * revision both ride `session/turn`; OPEN delivery / Wake delta both ride
 * `session/frame`; finalization / artifacts both ride the `session/finalize`
 * seal. There is no bespoke verb per interaction — and still no `advance`.
 */
export const INTERACTION_MESSAGE: Record<SessionInteraction, SessionMessageKind> = {
  start: "session/open",
  "open-delivery": "session/frame",
  "authored-or-stand-down": "session/turn",
  "wake-delta": "session/frame",
  revision: "session/turn",
  resume: "session/events",
  finalization: "session/finalize",
  artifacts: "session/finalize",
};

/* ──────────────── idempotency + fail-closed reconciliation (AC4) ───────────── */

/**
 * The result of reconciling an incoming turn against the entry already recorded
 * at its slot. Either an idempotent admit (`ok: true`, `duplicate` says whether
 * the incoming was a byte-identical re-send) carrying the settled `entryHash`, or
 * a fail-closed rejection (`ok: false`) with a typed {@link SessionDiagnostic}.
 */
export type AdmitResult =
  | { readonly ok: true; readonly duplicate: boolean; readonly entryHash: string }
  | { readonly ok: false; readonly diagnostic: SessionDiagnostic };

/**
 * Reconcile an `incoming` turn against the `prior` entry recorded at the same
 * intended slot — the idempotency + fail-closed primitive (djm.2 AC4; djm.4
 * "idempotency IS content-addressing"). Pure and synchronous; no crypto, no I/O.
 *
 * Ladder (fail-closed, most-specific first):
 *  1. structurally unreadable incoming        → `malformed-entry`
 *  2. names a different Session                → `wrong-session`
 *  3. does not continue the chain (parent/ord) → `broken-chain`
 *  4. answers a superseded Frame               → `stale-frame`
 *  5. SAME slot, SAME entry address            → idempotent duplicate (`ok`)
 *  6. SAME slot, CHANGED bytes                 → `turn-conflict` (the headline
 *     rule: a different-bytes-same-identity turn is NEVER silently accepted)
 *
 * (`not-pending` / `sealed` are phase diagnostics raised by the djm.4 L1 gate
 * that knows the Session's phase — not by this same-slot reconciler.)
 */
export function reconcileTurn(prior: TurnEntry, incoming: TurnEntry): AdmitResult {
  // 1. Structural readability — fail closed on an unreadable envelope.
  if (
    incoming.entryHash.length === 0 ||
    (incoming.body.kind === "authored" && incoming.authoredSha256.length === 0)
  ) {
    return { ok: false, diagnostic: "malformed-entry" };
  }
  // 2. Same Session.
  if (incoming.sessionId !== prior.sessionId) {
    return { ok: false, diagnostic: "wrong-session" };
  }
  // 3. Continues the chain (same ordinal AND same parent link).
  if (incoming.ordinal !== prior.ordinal || incoming.parentHash !== prior.parentHash) {
    return { ok: false, diagnostic: "broken-chain" };
  }
  // 4. Answers the current Frame (not a superseded one).
  if (incoming.frameRoot !== prior.frameRoot) {
    return { ok: false, diagnostic: "stale-frame" };
  }
  // 5. Same slot, same content address ⇒ idempotent duplicate. Content-addressing
  //    IS idempotency: an identical entry re-sent is a no-op that returns the
  //    settled hash. (Equal entryHash implies equal authored bytes.)
  if (incoming.entryHash === prior.entryHash) {
    return { ok: true, duplicate: true, entryHash: prior.entryHash };
  }
  // 6. Same slot, CHANGED bytes ⇒ fail closed. Never silently accept a
  //    different-bytes-same-semantic-identity turn.
  return { ok: false, diagnostic: "turn-conflict" };
}
