/**
 * # session/harness/cascade — the two-tier strategist+watcher cascade driver (ADR-0032 tracer, kestrel-aus)
 *
 * The thinnest real driver that runs ADR-0032's **two-tier cascade** end-to-end through the EXISTING
 * Simulate runner ({@link import("../simulate.ts").runSimulateSession}) with **zero determinism-core
 * change**. It composes TWO ADR-0012/0013 {@link Agent}s over ONE Session at two cadences:
 *
 *   - **STRATEGIST** — a frontier model (Fable/opus). Invoked at the OPEN keyframe (authors the day's
 *     Plan + Brief + Mandate + View) and on a watcher **escalation** (re-brief: supersede a new
 *     Plan/Brief). The few expensive judgments that set the frame.
 *   - **WATCHER** — a small/fast model (haiku). Invoked at every in-loop Wake. It **manages** the
 *     strategist's armed Plan within the Mandate (reload/exit/adjust/size-within-budget) — MANAGE-ONLY
 *     (owner v1 default): it may NOT arm new authority. When it reaches the edge of its authority it
 *     **escalates via the driver** to the strategist.
 *
 * ## The load-bearing trick: the cascade IS one {@link Agent} (no new determinism-core plumbing)
 * `runSimulateSession` already keys {@link import("../agent.ts").CapturedTurns} on each wake's stable
 * identity ({@link import("../agent.ts").wakeKeyOf}, kestrel-5zl.19) and captures the ONE terminal turn
 * each wake returns. So the cascade is expressed as a single
 * {@link Agent} whose `open` dispatches to the strategist and whose `decide` dispatches to the watcher
 * — and, on an escalation, re-invokes the strategist and returns ITS turn instead. Exactly one terminal
 * turn crosses to the Bus per wake, so:
 *   - the driver captures the whole cascade into `CapturedTurns` for free, and
 *   - {@link import("../agent.ts").recordedAgent} replays the cascade **byte-identically** (it replays
 *     the captured terminal turn per wake — it never learns which tier authored it).
 * This is the ADR §6 claim ("both tiers are above-the-line Agents; recordedAgent replays the whole
 * cascade byte-identically") realized without touching the determinism line.
 *
 * ## The safety keystone rides for free (ADR-0032 §3)
 * Every terminal turn — watcher OR strategist — crosses into the SAME fail-closed admission Gate
 * (`core.gate`) inside `runSimulateSession`: SELL floored at intrinsic, budget clamp, never-naked, the
 * bounded-risk envelope. The watcher buys judgment; the runtime keeps authority. A watcher action that
 * would exceed the Mandate/budget is refused fail-closed by that Gate exactly as any over-reaching
 * frontier action is; a watcher attempt to **arm new authority** (a `supersede`) is refused at the tier
 * boundary here ({@link classifyEscalation}) and **escalated** to the strategist rather than admitted.
 * Neither model tier is trusted; both are bounded.
 *
 * ## Attribution across tiers (ADR-0032 §6, the three-level ladder)
 * {@link attributeCascade} computes the frozen-Plan counterfactual over the SAME recorded tape:
 * `strategist_alpha = EV(frozen strategist Plan, pure-algo) − EV(benchmark)` and
 * `watcher_alpha = EV(cascade) − EV(frozen strategist Plan)`, so we can read which tier earned the P&L.
 */

import {
  OPEN_ORDINAL,
  OPEN_WAKE_KEY,
  wakeKeyOf,
  type ActingFrame,
  type Agent,
  type AgentConfig,
  type AgentTurn,
  type WakeKey,
} from "../agent.ts";
import type { BriefingInput } from "../../frame/types.ts";
import { disarmsPlan } from "../../engine/disarm.ts";

// ─────────────────────────────────────────────────────────────────────────────
// Escalation — the watcher wakes the strategist (ADR-0032 §4, owner v1 "minimal")
// ─────────────────────────────────────────────────────────────────────────────

/** Why the watcher escalated to the strategist (the two minimal owner-v1 triggers):
 *  - `arm-new-authority` — the watcher authored a `supersede` (arm/re-author standing authority),
 *    which is BEYOND its MANAGE-ONLY tactical authority (§5); the driver refuses it at the tier
 *    boundary and re-invokes the strategist (the right actor for a mandate/thesis change).
 *  - `defer-and-flag` — the watcher explicitly asked for a re-brief (a `standDown` reason or a journal
 *    opening with {@link ESCALATION_SENTINEL}), the "I am out of my depth; call the PM" move. */
export type EscalationReason = "arm-new-authority" | "defer-and-flag";

/** The token a prompted watcher opens its `standDown` reason / journal with to explicitly request a
 * re-brief (the defer-and-flag escalation). A legible, greppable signal — matched case-insensitively at
 * the start of the trimmed text so a fast model's `RE-BRIEF: regime broke` reads as an escalation. */
export const ESCALATION_SENTINEL = "RE-BRIEF";

/**
 * Pure classification of a watcher turn: does it escalate to the strategist, and why? Returns the
 * {@link EscalationReason}, or `null` when the turn is WITHIN the watcher's manage-only authority (it
 * crosses to the Bus and the admission Gate as-is). Above the determinism line — a total function of
 * the turn, so the driver's routing is deterministic and unit-testable.
 */
export function classifyEscalation(turn: AgentTurn): EscalationReason | null {
  // MANAGE-ONLY: a `supersede` is an attempt to arm/re-author standing authority — the watcher may not.
  // Refuse it at the tier boundary and escalate (the strategist is the right actor).
  if (turn.actions.some((a) => a.kind === "supersede")) return "arm-new-authority";
  // Explicit defer-and-flag: a standDown reason or the journal opening with the sentinel.
  const flags: string[] = [];
  if (turn.journal !== undefined) flags.push(turn.journal);
  for (const a of turn.actions) if (a.kind === "standDown") flags.push(a.reason);
  if (flags.some((t) => t.trimStart().toUpperCase().startsWith(ESCALATION_SENTINEL))) return "defer-and-flag";
  return null;
}

// ─────────────────────────────────────────────────────────────────────────────
// The cascade Agent
// ─────────────────────────────────────────────────────────────────────────────

/** One routing record the cascade appends per invoked ordinal (audit evidence, ABOVE the determinism
 * line — never a graded input, never a replay key). The `escalation-rebrief` records BOTH the watcher's
 * escalation (why) and that the strategist's re-brief turn is what crossed to the Bus. */
export interface CascadeStep {
  /** {@link OPEN_ORDINAL} for the open turn, else the wake ordinal — a DISPLAY/log coordinate only (it
   * shifts when a source inserts a vantage). Join a step to its captured turn on {@link key}. */
  readonly ordinal: number;
  /** The step's stable wake identity (kestrel-5zl.19) — {@link OPEN_WAKE_KEY} for the open turn, else
   * {@link wakeKeyOf} of the delivered frame: the SAME key the driver captured the turn under. */
  readonly key: WakeKey;
  /** The tier whose turn CROSSED to the Bus at this ordinal (on an escalation, the strategist). On an
   * `escalation-hold-armed` step NEITHER tier's turn crosses — the driver holds the frozen plan armed
   * (an empty PASS) — so the tier records the strategist (the tier the escalation was routed to). */
  readonly tier: "strategist" | "watcher";
  /** `escalation-hold-armed` (frozen-fan only): the watcher escalated but the frozen/recorded strategist
   * carried NO re-brief; instead of letting its fail-closed stand-down DISARM the armed plan, the driver
   * REFUSED the watcher's over-reach and HELD the frozen plan armed (fail-closed to an empty PASS). */
  readonly kind: "open" | "wake" | "escalation-rebrief" | "escalation-hold-armed";
  readonly escalated: boolean;
  readonly escalationReason?: EscalationReason;
}

/** Options for {@link cascadeAgent}. `strategist`/`watcher` are two ordinary {@link Agent}s (live,
 * scripted, or recorded); `log`, when supplied, receives one {@link CascadeStep} per invoked ordinal. */
export interface CascadeOptions {
  readonly strategist: Agent;
  readonly watcher: Agent;
  /** Override the advertised cascade {@link AgentConfig}; defaults to {@link cascadeConfigOf}. */
  readonly config?: AgentConfig;
  /** The above-the-line routing audit (one record per invoked ordinal). */
  readonly log?: CascadeStep[];
  /**
   * FROZEN/RECORDED-strategist FAN MODE ONLY (set by {@link import("./plan-fixture.ts").fanWatchers},
   * whose strategist tier is a {@link import("./plan-fixture.ts").recordedStrategistOf} recording). When
   * `true`, a watcher escalation whose strategist re-invoke fails closed to a stand-down does NOT DISARM
   * the armed Plan: a frozen/recorded strategist carries no captured re-brief, so its `decide` stands the
   * book down — and letting that cross would tear down the armed CONTROL plan and collapse the whole
   * frozen-plan fan to $0 (docs/results/author-and-fan/, PR #8). Instead the watcher's over-reach is
   * REFUSED and the frozen plan is HELD ARMED (fail-closed to an empty PASS — the existing armed authority
   * persists), so the fan can measure watcher management against a LIVE armed plan.
   *
   * SAFETY SCOPING: this is confined to the recorded/frozen fan. A LIVE cascade leaves this UNSET — a live
   * watcher that exceeds its mandate is still refused and its live strategist re-briefed or stood down
   * exactly as before. It is NOT a global flag: only the recorded-fan builder sets it, and it never
   * weakens the admission Gate (every crossing turn, held or not, still passes the same fail-closed Gate).
   */
  readonly frozenStrategist?: boolean;
}

/** The fail-closed HOLD turn the frozen fan returns instead of a strategist stand-down: an empty PASS
 * (the standing armed Plan keeps managing itself) carrying a legible pre-hoc JOURNAL naming the refusal.
 * Byte-stable — no per-turn material, no clock. */
const HELD_ARMED_TURN: AgentTurn = {
  actions: [],
  journal:
    "cascade(frozen-fan): watcher over-reach REFUSED at the tier boundary; frozen plan HELD ARMED " +
    "(no captured strategist re-brief — the armed authority persists rather than being torn down)",
};

/** Derive the cascade's advertised {@link AgentConfig} from the two tiers: the strategist's identity
 * (the frame-setting brain) with a cascade `label` and the cascade `harness` folded in, so a cascade
 * run mints its OWN ConfigId / grid column and never averages with a single-tier column (ADR-0032 §7,
 * the ADR-0013 controlled-division discipline). The watcher's model rides the label. */
export function cascadeConfigOf(strategist: AgentConfig, watcher: AgentConfig): AgentConfig {
  return {
    ...strategist,
    label: `cascade(${strategist.label}»${watcher.label})`,
    harness: "strategist-watcher-cascade",
    harnessVersion: "v1",
  };
}

/**
 * Build the cascade as ONE {@link Agent} over two tiers. `open` → the strategist authors the frame
 * (Plan/Brief/Mandate/View). `decide` → the watcher manages in-loop; if the watcher escalates
 * ({@link classifyEscalation}), the strategist is re-invoked on the SAME frozen frame (which carries
 * the book/mandate/plan state + the engine log since last wake — the acting Frame IS the state) and
 * ITS re-brief turn crosses to the Bus instead. Exactly one terminal turn per ordinal → the driver
 * captures the whole cascade and {@link recordedAgent} replays it byte-identically.
 */
export function cascadeAgent(opts: CascadeOptions): Agent {
  const { strategist, watcher, log } = opts;
  const config = opts.config ?? cascadeConfigOf(strategist.config, watcher.config);
  return {
    config,
    async open(briefing: BriefingInput): Promise<AgentTurn> {
      const turn = await strategist.open(briefing);
      log?.push({ ordinal: OPEN_ORDINAL, key: OPEN_WAKE_KEY, tier: "strategist", kind: "open", escalated: false });
      return turn;
    },
    async decide(frame: ActingFrame): Promise<AgentTurn> {
      const watcherTurn = await watcher.decide(frame);
      const reason = classifyEscalation(watcherTurn);
      if (reason === null) {
        // Within manage-only authority — the watcher's turn crosses to the Bus + the admission Gate.
        log?.push({ ordinal: frame.wakeOrdinal, key: wakeKeyOf(frame), tier: "watcher", kind: "wake", escalated: false });
        return watcherTurn;
      }
      // Escalate: the watcher wakes the strategist (attention, not risk — ADR-0001's Wake invariant).
      // Re-invoke the strategist on the same frozen frame; its re-brief supersede is the terminal turn.
      const strategistTurn = await strategist.decide(frame);
      // FROZEN/RECORDED-STRATEGIST FAN MODE ONLY (opts.frozenStrategist): a recorded strategist has no
      // captured re-brief, so its `decide` fails closed to a stand-down. Letting that DISARM the armed
      // plan would collapse the whole frozen-plan fan to $0. Instead REFUSE the watcher's over-reach and
      // HOLD the frozen plan armed (fail-closed to an empty PASS — the armed authority persists). A frozen
      // strategist that DID capture a genuine re-brief (a non-disarming turn) is still honored. This is
      // scoped to the recorded fan: a LIVE cascade leaves frozenStrategist unset and disarms exactly as
      // before, and the admission Gate is untouched (the held PASS still crosses the same Gate).
      if (opts.frozenStrategist === true && disarmsPlan(strategistTurn)) {
        log?.push({
          ordinal: frame.wakeOrdinal,
          key: wakeKeyOf(frame),
          tier: "strategist",
          kind: "escalation-hold-armed",
          escalated: true,
          escalationReason: reason,
        });
        return HELD_ARMED_TURN;
      }
      log?.push({
        ordinal: frame.wakeOrdinal,
        key: wakeKeyOf(frame),
        tier: "strategist",
        kind: "escalation-rebrief",
        escalated: true,
        escalationReason: reason,
      });
      return strategistTurn;
    },
  };
}

// ─────────────────────────────────────────────────────────────────────────────
// Three-level attribution ladder (ADR-0032 §6) — the frozen-Plan counterfactual
// ─────────────────────────────────────────────────────────────────────────────

/** The three EVs the attribution ladder reads, each a graded run over the SAME recorded tape + fill
 * model (so the only variable is the tier under test — the controlled-comparison discipline). Use the
 * conservative floor P&L (`blotter.totals.floor`) — a deterministic, definite-fill number under the
 * strict-cross family, comparable across the three arms. */
export interface AttributionInput {
  /** `EV(benchmark)` — buy-and-hold / the null baseline over the tape. */
  readonly benchmark: number;
  /** `EV(frozen strategist Plan, replayed pure-algo, no watcher)` — the armed Plan alone. */
  readonly frozenPlan: number;
  /** `EV(cascade)` — the watcher managing the strategist's armed Plan in the loop. */
  readonly cascade: number;
}

/** The three-level `management_alpha` decomposition (ADR-0032 §6): `strategist_alpha` is the frame
 * (plan/brief) alpha vs the benchmark; `watcher_alpha` is the in-loop management alpha the watcher adds
 * ON TOP of the frozen plan; `cascadeTotal` is their sum (= `EV(cascade) − benchmark`). Pure. */
export interface Attribution {
  readonly strategistAlpha: number;
  readonly watcherAlpha: number;
  readonly cascadeTotal: number;
}

/** Compute the three-level ladder from the three frozen counterfactual EVs (ADR-0032 §6). Pure. */
export function attributeCascade(x: AttributionInput): Attribution {
  const strategistAlpha = x.frozenPlan - x.benchmark;
  const watcherAlpha = x.cascade - x.frozenPlan;
  return { strategistAlpha, watcherAlpha, cascadeTotal: strategistAlpha + watcherAlpha };
}
