/**
 * # session/authoring-loop — the bounded OPEN authoring loop (ADR-0029 §2) + its evidence log
 *
 * The driver-owned iteration the Simulate driver ({@link import("./simulate.ts")}) runs when the agent is
 * viewshop-enabled (it exposes {@link Agent.authoringOpen}): {@link runOpenAuthoringLoop} drives a
 * view-request / repair loop under ONE dual budget (`viewRequestCap` + `authoringTokenBudget`) and returns
 * the TERMINAL {@link AgentTurn} — only that turn crosses to the Bus + CapturedTurns; the iterations are
 * logged as {@link EmergenceRecord}s ABOVE the determinism line (private application data, never a replay
 * or graded input). Terminal = a valid `supersede` / `standDown` / empty pass, or a fail-closed `standDown`
 * on budget exhaustion (NEVER a forced Plan).
 *
 * The loop is driver-owned because only the driver holds the frozen snapshot + the pane catalog. It is
 * extracted here so that surface — the loop, its dual-budget defaults, the emergence-record shape, and the
 * View-document → {@link ViewSelection} mapper it fails closed through — lives beside its own interface. A
 * baseline / recorded / fixed run never exposes `authoringOpen`, so this module is inert for it and the run
 * is byte-identical (single-shot open).
 */

import { parse } from "../lang/index.ts";
import { resolveView, type ViewSelection } from "../frame/pane-catalog.ts";
import { OPEN_ORDINAL, type Agent, type AgentTurn, type LlmUsage } from "./agent.ts";
import type { BriefingInput } from "../frame/types.ts";
import { standDownTurn } from "../engine/disarm.ts";

/** The conservative default cap on non-terminal authoring iterations (owner-confirmed 2026-07-13). */
export const DEFAULT_VIEW_REQUEST_CAP = 3;
/** The conservative default cumulative model-token ceiling for the authoring loop (a config axis). */
export const DEFAULT_AUTHORING_TOKEN_BUDGET = 8000;

/**
 * One evidence record per NON-terminal authoring iteration (ADR-0029 §5), keyed by
 * `(ordinal=OPEN_ORDINAL, iteration)`. It captures BOTH view-requests (the emergence-study signal:
 * panes + budget + reason + usage) AND parse-error/repair events (the grammar-evolution signal,
 * ADR-0030: the guiding error surfaced + the model + the spend). OFF the graded path — never a
 * replay input, never a graded input (private application data).
 */
export interface EmergenceRecord {
  /** {@link OPEN_ORDINAL} — the loop is OPEN-only in v1. */
  readonly ordinal: number;
  /** 0-based index of this non-terminal iteration (shared by view-requests + repairs). */
  readonly iteration: number;
  readonly kind: "view-request" | "repair";
  readonly model: string;
  readonly usage: LlmUsage;
  readonly latencyMs?: number;
  /** `true` when this iteration hit the dual budget → the driver failed closed to standDown. */
  readonly budgetExhausted: boolean;
  // ── view-request fields ──
  /** The catalog pane ids the requested View selected (empty when the View was a defect). */
  readonly panes?: readonly string[];
  /** The raw Kestrel VIEW document the agent authored. */
  readonly view?: string;
  /** The View's own declared token budget (the cost of the screen), distinct from the loop budget. */
  readonly viewBudget?: number;
  /** The agent's stated reason for this lens (the Brief-acting-through-perception signal). */
  readonly reason?: string;
  // ── repair fields ──
  /** The repair-guiding error surfaced to the model (the malformed-input → error signal, ADR-0030). */
  readonly error?: string;
}

/** Local message extractor (the driver's own `msgOf`) — kept private so this module carries no
 * back-edge onto the driver it was extracted from. */
function msgOf(e: unknown): string {
  return e instanceof Error ? e.message : String(e);
}

/** Total model spend for the loop's `authoringTokenBudget` (input already includes the cache splits). */
function authoringSpend(u: LlmUsage): number {
  return u.inputTokens + u.outputTokens + u.thinkingTokens;
}

/** Map a validated Kestrel VIEW document to the renderer's {@link ViewSelection} — pane names +
 * budget + the authored pane ARGS (kestrel-wa0j.3: args thread through `resolveView` →
 * `materializePanes`; a pane that does not understand an arg fails closed there, never a silent
 * drop). Throws if it is not a single `View` (defensive — the harness parser already validated
 * this shape in `parseAuthoringReply`). Exported for the pane-args threading test. */
export function viewSelectionOfDocument(doc: string): ViewSelection {
  const node = parse(doc);
  const stmt = node.kind === "module" ? node.statements[0] : node;
  if (stmt === undefined || stmt.kind !== "view") {
    throw new Error("requestView.view is not a single VIEW statement");
  }
  return {
    name: stmt.name,
    panes: stmt.panes.map((p) => (p.args.length > 0 ? { name: p.name, args: p.args } : { name: p.name })),
    ...(stmt.budget !== undefined ? { budget: stmt.budget } : {}),
  };
}

export interface AuthoringLoopParams {
  readonly viewRequestCap: number;
  readonly authoringTokenBudget: number;
  readonly emergenceLog?: EmergenceRecord[];
  readonly model: string;
}

/**
 * Drive the bounded OPEN authoring loop (ADR-0029 §2) and return the TERMINAL {@link AgentTurn}. TWO
 * kinds of non-terminal iteration share ONE dual budget (`viewRequestCap` + `authoringTokenBudget`):
 *  - a **requestView** → materialize the requested View (validated against the one catalog, fail-closed
 *    on an unknown pane) and re-ask under the SAME frozen snapshot / cutoff (the T-5m guard, §3 —
 *    structural, since the pane builders are pure functions of the frozen frame);
 *  - an **invalid author** → surface the repair-guiding error and re-ask (repair).
 * Terminal = a valid `supersede` / `standDown` / empty pass, or — on budget exhaustion — a fail-closed
 * `standDown` (NEVER a forced Plan). Only this returned turn crosses to the Bus + CapturedTurns; the
 * iterations are logged as evidence, above the determinism line. The loop is driver-owned because only
 * the driver holds the frozen snapshot and the pane catalog.
 */
export async function runOpenAuthoringLoop(agent: Agent, briefing: BriefingInput, p: AuthoringLoopParams): Promise<AgentTurn> {
  const standDown = (reason: string): AgentTurn => standDownTurn(reason);
  let iteration = 0; // NON-terminal iterations consumed so far (view-requests + repairs, shared)
  let tokensSpent = 0;
  let view: ViewSelection | undefined; // the incumbent default View (undefined ⇒ the frame default)
  let repairError: string | undefined;

  for (;;) {
    const step = await agent.authoringOpen!(briefing, {
      ...(view !== undefined ? { view } : {}),
      ...(repairError !== undefined ? { repairError } : {}),
    });
    tokensSpent += authoringSpend(step.usage);
    const reply = step.reply;
    if (reply.kind === "turn") return reply.turn; // TERMINAL: supersede(Plan) | standDown | empty pass

    // A non-terminal iteration — a view-request OR a repair. Both consume the SAME dual budget; whichever
    // bound binds first terminates the loop with a fail-closed standDown (never a forced Plan).
    const nextIteration = iteration + 1;
    const exhausted = nextIteration > p.viewRequestCap || tokensSpent >= p.authoringTokenBudget;
    const latency = step.latencyMs !== undefined ? { latencyMs: step.latencyMs } : {};

    if (reply.kind === "requestView") {
      let sel: ViewSelection | undefined;
      let viewError: string | undefined;
      try {
        const candidate = viewSelectionOfDocument(reply.request.view);
        resolveView(candidate, "OPEN"); // fail-closed: unknown pane id / reserved kernel id
        sel = candidate;
      } catch (e) {
        viewError = msgOf(e);
      }
      p.emergenceLog?.push({
        ordinal: OPEN_ORDINAL,
        iteration,
        kind: "view-request",
        model: p.model,
        usage: step.usage,
        ...latency,
        budgetExhausted: exhausted,
        panes: sel?.panes.map((x) => x.name) ?? [],
        view: reply.request.view,
        ...(sel?.budget !== undefined ? { viewBudget: sel.budget } : {}),
        reason: reply.request.reason,
      });
      if (exhausted) return standDown("view-request budget exhausted (bounded authoring loop, ADR-0029)");
      if (sel === undefined) {
        // A defective View (unknown pane / not a single View) → a repair re-ask carrying the guidance.
        repairError = viewError ?? "requestView.view could not be resolved to panes";
        view = undefined;
      } else {
        view = sel;
        repairError = undefined;
      }
      iteration = nextIteration;
      continue;
    }

    // reply.kind === "invalid" → a repair iteration: surface the guiding error and re-ask.
    p.emergenceLog?.push({
      ordinal: OPEN_ORDINAL,
      iteration,
      kind: "repair",
      model: p.model,
      usage: step.usage,
      ...latency,
      budgetExhausted: exhausted,
      error: reply.reason,
    });
    if (exhausted) return standDown("authoring budget exhausted during repair (bounded authoring loop, ADR-0029)");
    repairError = reply.reason; // a repair re-ask does not re-render — the last materialized lens stays
    iteration = nextIteration;
  }
}
