/**
 * # cli/commands/sim — `sim` (LIGHT, node-runnable): the one-command funnel (kestrel-585 / kestrel-vcn).
 *
 * The site's hero command: `npx kestrel.markets sim wsb-gme-meme-short-squeeze`. LIGHT by
 * construction — it imports only `../../lang` (the real parser, for local --plans
 * validation) and `../../canonical` (its `canonicalPlansText`, itself lang-only), the
 * fetch-only {@link ../backend/hosted.ts HostedFunnel}, and the pure `../render/sim.ts`
 * renderer. No `bun:*`/chdb, so it runs under plain node for every human and agent who
 * copies the snippet.
 *
 * Shape:
 *   - bare `sim`               → the MENU (GET /catalog, derived scenario slugs + aliases).
 *   - `sim <lowercase-slug>`   → resolve the slug → mint trial → POST /simulate → render the
 *                                completed Operation's story + the shareable proof URL.
 *   - `sim <TICKER…>` / mixed-case / a `<when>` selector → this PR ships only the curated-slug
 *                                funnel, so these RESERVED forms refuse LOUDLY (exit 2 USAGE),
 *                                naming what they will become — the case rule is locked in now
 *                                (kestrel-vcn selector grammar; ticker/date selectors are kestrel-do4).
 *
 * Backend routing inverts `run`'s local-default for this ONE verb (design §4 rule 1): a
 * sim positional NAMES platform data, so `sim` always routes to the hosted funnel — the
 * default base, `--api`/`KESTREL_API` overriding — even with no `--api`.
 */

import { readFileSync } from "node:fs";

import { appendHostedRun, defaultHostedStorePath, type HostedRunRow } from "../../ledger/hosted.ts";
import { parse, parseMarkdown, KestrelParseError, type KestrelNode } from "../../lang/index.ts";
import { canonicalPlansText } from "../../canonical/plans.ts";
import { sha256 } from "../../crypto/sha256.ts";
import type { OutputCtx } from "../context.ts";
import type { GlobalFlags } from "../context.ts";
import { parseArgs } from "../args.ts";
import { CliError, EXIT, asConnectionError } from "../errors.ts";
import { renderOffer, renderSettlementRequired } from "../render/offer.ts";
import {
  HostedFunnel,
  DEFAULT_API,
  resolveScenarioSlug,
  nearMissSlugs,
  summarizeReceipts,
  gradedMetrics,
  neverFiredReasonOf,
  gradeArtifactOf,
  gradeReceiptOf,
  proofWebUrl,
  slugifyTitle,
  SCENARIO_ALIASES,
  type HostedCatalogEntry,
  type HostedOperation,
  type HostedProof,
  type Spend,
} from "../backend/hosted.ts";
import { renderSimStory, renderSimMenu, renderDemoLesson, renderStandDownLesson, renderPaidBoundaryNudge, neverFiredLine, demoLessonObject, standDownLessonObject, paidBoundaryNudgeObject, type SimStory, type SimMenu, type DemoLesson, type StandDownLesson, type PaidBoundaryNudge } from "../render/sim.ts";
import { renderResidencySnippet, renderResidencyOneLine, residencyObject } from "../render/residency.ts";
import type { ResolvedScenario } from "../backend/hosted.ts";

type FetchLike = (url: string, init?: RequestInit) => Promise<Response>;

/**
 * The default strategy: a clearly-labeled stand-down demo document (author-no-strategy
 * holds — the DEFAULT is the demo baseline, not a hidden strategy). Its own comment is
 * its label. Supply `--plans <file>` to author a real strategy.
 */
export const STAND_DOWN_DOC = "# demo: stand-down baseline — supply --plans <file> to author a strategy\n";

/**
 * A minimal, replaceable EXAMPLE plan the plan-less stand-down lesson (v6et) shows the reader —
 * three lines they can save to a file and run with `--plans` to make the NEXT `sim` run actually
 * trade. Deliberately naive (rest a limit at the open, hold to the risk-envelope ttl) and clearly
 * a starting point, never dressed up as alpha. Kept in sync in shape with `prove`'s bundled sampler
 * starter (prove.ts SAMPLER_STARTER_DOC), but owned HERE so `sim` never imports `prove` (which
 * imports `sim` — the dependency runs one way only).
 */
export const STAND_DOWN_EXAMPLE_PLAN = "PLAN starter budget 1R ttl +24h\n  WHEN spot > 0\n  DO buy 1 shares @ spot";

function usageErr(message: string, hint?: string): CliError {
  return new CliError({ code: "USAGE", exit: EXIT.USAGE, message, ...(hint !== undefined ? { hint } : {}) });
}

/* ─────────────────────── selector case-rule (kestrel-vcn) ───────────────── */

/** The case-decided kind of a `what` positional (design §4 grammar). */
export type SelectorKind = "slug" | "tickers" | "mixed";

/**
 * Classify a `what` token by CASE (the design's fail-closed case rule): all-lowercase =
 * a catalog scenario slug; all-UPPERCASE = ticker selector(s); a mix of cases = ambiguous
 * (a loud USAGE error names both interpretations). A token with neither letter (a bare
 * date/number) is treated as a slug shape here and rejected downstream as a `<when>`.
 */
export function classifySelector(token: string): SelectorKind {
  const hasUpper = /[A-Z]/.test(token);
  const hasLower = /[a-z]/.test(token);
  if (hasUpper && hasLower) return "mixed";
  if (hasUpper) return "tickers";
  return "slug";
}

const DATE_RE = /^\d{4}-\d{2}(-\d{2})?$/;
const RANGE_RE = /^\d{4}-\d{2}(-\d{2})?\.\.\d{4}-\d{2}(-\d{2})?$/;
const REL_WHEN = new Set(["yesterday", "last-week", "last-month"]);

/** True when a token is a `<when>` (date/range/relative), not a scenario slug. */
function looksLikeWhen(token: string): boolean {
  return DATE_RE.test(token) || RANGE_RE.test(token) || REL_WHEN.has(token);
}

/**
 * Parse the `sim` positionals into a scenario slug, or refuse LOUDLY (exit 2 USAGE) for a
 * reserved form — naming what each will become so the case rule is locked in now:
 *   - a second positional (a `<when>`)     → date/window selectors are reserved (kestrel-do4).
 *   - an UPPERCASE `what`                  → ticker selectors are reserved (kestrel-do4).
 *   - a mixed-case `what`                  → ambiguous slug/ticker; refuse naming both.
 *   - a bare `<when>` in the `what` slot   → needs a scenario or ticker; reserved.
 */
export function parseSimSelector(positionals: readonly string[]): { scenarioSlug: string } {
  const first = positionals[0]!;

  if (positionals.length > 1) {
    const when = positionals[1]!;
    throw usageErr(
      `date-window selectors are reserved — \`kestrel sim ${first} ${when}\` is not available yet`,
      "this PR ships curated scenario slugs only; ticker × <when> selectors are coming (kestrel-do4). Run `kestrel sim` for the menu.",
    );
  }

  const kind = classifySelector(first);
  if (kind === "mixed") {
    throw usageErr(
      `"${first}" is ambiguous: a lowercase scenario slug or an UPPERCASE ticker selector? — mixed case is reserved`,
      "use an all-lowercase scenario slug (e.g. meme-stock-short-squeeze) or, once shipped, all-UPPERCASE tickers (e.g. SPY). Run `kestrel sim` for the menu.",
    );
  }
  if (kind === "tickers") {
    throw usageErr(
      `ticker selectors are reserved — \`kestrel sim ${first} <when> --plans <file>\` is not available yet`,
      "this PR ships curated lowercase scenario slugs only; UPPERCASE ticker × <when> selectors are coming (kestrel-do4). Run `kestrel sim` for the menu.",
    );
  }
  if (looksLikeWhen(first)) {
    throw usageErr(
      `"${first}" is a <when> with no scenario or ticker — reserved`,
      "name a scenario slug (kestrel sim <slug>); a standalone market window is coming (kestrel-do4). Run `kestrel sim` for the menu.",
    );
  }
  return { scenarioSlug: first };
}

/* ─────────────────────────── strategy loading ──────────────────────────── */

export interface Strategy {
  readonly source: string;
  readonly label: string;
  /**
   * The deterministic CONTENT identity of the strategy — `sha256` of the canonical plan text
   * ({@link source}), 64-char lowercase hex (kestrel-etyu). This is what distinguishes two runs of
   * the SAME `--plans` path edited IN PLACE (the returning-user workflow): the {@link label} (the
   * path) is unchanged across an edit, but the digest moves with the content. No wall clock, no RNG.
   */
  readonly digest: string;
}

/** Wrap a parse throw as a loud, stack-free USAGE(PARSE) error (mirrors commands/lang.ts). */
function asParseError(e: unknown): CliError {
  if (e instanceof KestrelParseError) return new CliError({ code: "PARSE", exit: EXIT.USAGE, message: e.message });
  return usageErr(e instanceof Error ? e.message : String(e));
}

/**
 * A BUNDLED example document a verb may default to when the caller supplies no `--plans`.
 * It is exactly parallel to what `--plans` uploads — a labeled, replaceable example document,
 * never a hidden strategy (author-provenance stays customer/explicitly-supplied server-side).
 * `sim` keeps the {@link STAND_DOWN_DOC} default (the raw funnel); `prove` — the value-
 * demonstration front door — passes a firing sampler starter so its bare run actually trades.
 */
export interface BundledDefaultDoc {
  /** Raw Kestrel source, parsed + canonicalized locally (must parse — validated like the default). */
  readonly source: string;
  /** The strategy label printed in the story — self-describing as a replaceable bundled example. */
  readonly label: string;
}

/**
 * The strategy for the hosted run, validated LOCALLY with the REAL parser first (a bad
 * document fails at exit 2 before any capability is minted or dataset is touched):
 *   - no `--plans`  → the `bundledDefault` (a labeled example), else the stand-down baseline.
 *   - `--plans <p>` → the author's document — `.md` (fenced ```kestrel) / `.kestrel` / `-` (stdin).
 *
 * `bundledDefault` is consulted ONLY when no `--plans` is given; an author-supplied document
 * always wins, so the caller's own strategy path is unchanged.
 */
export function loadStrategy(plansPath: string | undefined, bundledDefault?: BundledDefaultDoc): Strategy {
  if (plansPath === undefined) {
    const doc = bundledDefault ?? {
      source: STAND_DOWN_DOC,
      label: "stand-down baseline (demo) — supply --plans <file> to author a strategy",
    };
    const documents = [parse(doc.source)]; // validate the default too — it must parse
    const source = canonicalPlansText(documents);
    return { source, label: doc.label, digest: sha256(source) };
  }
  let text: string;
  try {
    text = plansPath === "-" ? readFileSync(0, "utf8") : readFileSync(plansPath, "utf8");
  } catch (e) {
    throw new CliError({
      code: "NOT_FOUND",
      exit: EXIT.NOT_FOUND,
      message: `cannot read --plans ${JSON.stringify(plansPath)}: ${e instanceof Error ? e.message : String(e)}`,
    });
  }
  let documents: readonly KestrelNode[];
  try {
    documents = plansPath.endsWith(".md") ? parseMarkdown(text) : [parse(text)];
  } catch (e) {
    throw asParseError(e);
  }
  const source = canonicalPlansText(documents);
  // The digest is the CONTENT identity (kestrel-etyu): two runs of this SAME `plansPath` edited in
  // place carry the same label (the path) but a DIFFERENT digest, so they are distinguishable.
  return { source, label: plansPath, digest: sha256(source) };
}

/* ──────────────────────────── base URL routing ─────────────────────────── */

interface Env {
  readonly [k: string]: string | undefined;
}

/** The hosted funnel base: `--api <url>` > `KESTREL_API` > the default; empty/`default` → default. */
export function resolveSimBase(globals: GlobalFlags, env: Env): string {
  const raw = globals.api ?? env.KESTREL_API;
  if (raw === undefined || raw === "" || raw === "default") return DEFAULT_API;
  return raw;
}

/* ──────────────────────────────── the verb ─────────────────────────────── */

export interface SimDeps {
  /** Injected fetch (tests pass an in-process funnel face); defaults to global fetch. */
  readonly fetch?: FetchLike;
  /** Injected env (tests); defaults to process.env. */
  readonly env?: Env;
  /** Where the hosted-run receipt is appended; defaults to the identity-bound home store
   * `~/.kestrel/hosted-runs.jsonl` ({@link defaultHostedStorePath}, kestrel-pvlc / kestrel-fq78). */
  readonly hostedStorePath?: string;
}

/**
 * Read the opaque copy-token: `--copy-token <tok>` > `KESTREL_COPY_TOKEN`. The CLI never
 * interprets it — it is forwarded verbatim as one header at mint time (see {@link runHostedProof}).
 * An empty/absent value returns `undefined` (the header is omitted; behaviour unchanged).
 */
export function resolveCopyToken(flags: Map<string, string>, env: Env): string | undefined {
  const raw = flags.get("copy-token") ?? env.KESTREL_COPY_TOKEN;
  return raw === undefined || raw === "" ? undefined : raw;
}

/**
 * Parse `--budget <amount>` into the {@link Spend} cap forwarded on `POST /simulate` as
 * `spend.budget` (bead kestrel-4dz5) — the CLI's ONLY affordance to reach the paid boundary
 * (exit 5 PAYMENT_REQUIRED). The declared budget is a HONORED CAP the caller opts into (the CLI
 * never auto-pays): a tiny budget on a metered dataset pauses at the Spend boundary and returns
 * the settleable Offer as DATA. Absent → `undefined` (the free default path, unchanged). A
 * malformed value — non-numeric, negative, or non-finite (NaN/±Infinity) — is a loud USAGE error
 * (exit 2), never a silently-dropped or widened cap.
 */
export function resolveSpend(flags: Map<string, string>): Spend | undefined {
  const raw = flags.get("budget");
  if (raw === undefined) return undefined;
  const budget = Number(raw);
  if (raw.trim() === "" || !Number.isFinite(budget) || budget < 0)
    throw usageErr(
      `--budget must be a finite, non-negative number (got ${JSON.stringify(raw)})`,
      "declare a spend ceiling in USD, e.g. `--budget 0.50`; use `--budget 0` to pause at the first metered draw",
    );
  return { budget };
}

/** `sim` — the curated-scenario funnel. Bare = the menu; a slug = mint → simulate → render. */
export async function simCommand(
  argv: readonly string[],
  ctx: OutputCtx,
  globals: GlobalFlags,
  deps: SimDeps = {},
): Promise<number> {
  const { positionals, flags } = parseArgs(argv, new Set(), new Set(["plans", "copy-token", "budget"]));
  const env = deps.env ?? (process.env as Env);
  const base = resolveSimBase(globals, env);
  const client = new HostedFunnel({ baseUrl: base, ...(deps.fetch !== undefined ? { fetch: deps.fetch } : {}) });

  // A raw `fetch` transport failure (network down / DNS miss / refused socket) throws an untyped
  // `TypeError`, not a `CliError` — left alone it falls through the router as an unclear GENERIC
  // exit 1 on the launch's paper/sim entry path. Type it fail-closed as NETWORK_UNAVAILABLE (exit
  // 4) carrying the base + a network/API hint. This wraps the whole hosted drive — catalog, mint,
  // simulate, proof (via `runHostedProof`) — so any leg's connection fault is typed identically.
  // Already-typed throws (USAGE from the selector, NOT_FOUND, an HTTP status) pass through
  // untouched: `asConnectionError` returns null for anything that is not a transport fault
  // (PR #68 review 6a, kestrel-a3v9).
  try {
    return await runSimFunnel(client, base, positionals, flags, ctx, deps, env);
  } catch (e) {
    throw asConnectionError(e, base) ?? e;
  }
}

/** The hosted-funnel drive for {@link simCommand}, wrapped by its connection-error mapping. */
async function runSimFunnel(
  client: HostedFunnel,
  base: string,
  positionals: readonly string[],
  flags: Map<string, string>,
  ctx: OutputCtx,
  deps: SimDeps,
  env: Env,
): Promise<number> {
  // ── bare `sim` → the MENU ──
  if (positionals.length === 0) {
    const { entries } = await client.catalog();
    return renderMenu(entries, ctx);
  }

  // ── the selector case rule (reserved forms refuse here, exit 2) ──
  const { scenarioSlug } = parseSimSelector(positionals);

  // ── resolve the slug against the catalog ──
  const { entries } = await client.catalog();
  const resolved = resolveScenarioSlug(scenarioSlug, entries);
  if (resolved === null) {
    const near = nearMissSlugs(scenarioSlug, entries);
    throw new CliError({
      code: "NOT_FOUND",
      exit: EXIT.NOT_FOUND,
      message: `unknown scenario slug ${JSON.stringify(scenarioSlug)}`,
      hint:
        near.length > 0
          ? `did you mean: ${near.join(", ")}? — run \`kestrel sim\` for the full menu`
          : "run `kestrel sim` for the menu",
    });
  }

  // ── strategy: validated LOCALLY first (bad doc → exit 2 before any mint) ──
  const plansPath = flags.get("plans");
  const strategy = loadStrategy(plansPath);
  const copyToken = resolveCopyToken(flags, env);
  // The self-declared Spend cap (`--budget`) — validated before any mint (a bad value → exit 2).
  const spend = resolveSpend(flags);

  return runHostedProof({
    client,
    resolved,
    strategy,
    ctx,
    hostedStorePath: deps.hostedStorePath ?? defaultHostedStorePath(env),
    // sim prints the one-line residency guidance (kestrel-qupe): the proof URL is the shareable handle,
    // and — since kestrel-fq78 — the receipt is recorded to the identity-bound home ledger so `runs
    // list` finds it from any directory (unlike the fuller `prove` snippet).
    residencyOneLine: true,
    // Whether the strategy was authored (`--plans`) vs the stand-down default — governs the
    // `--copy-token` continuation notice (kestrel-gu9h).
    authoredPlan: plansPath !== undefined,
    // The plan-less stand-down lesson (v6et): rides ONLY the no-`--plans` path — the class of every
    // bare `sim <slug>` run, not one slug — so a menu-browser (and the crypto door) is told the empty
    // grade is a by-design stand-down and shown the two next steps. An author-supplied `--plans` run
    // arms real orders and needs no such nudge, so it carries none.
    ...(plansPath === undefined
      ? {
          standDownLesson: {
            scenarioSlug: resolved.canonicalSlug,
            examplePlanSource: STAND_DOWN_EXAMPLE_PLAN,
            swapInCli: `npx kestrel.markets sim ${resolved.canonicalSlug} --plans <your-plan.kestrel>`,
            proveCli: "npx kestrel.markets prove",
          },
        }
      : {}),
    // The free-path paid-boundary nudge (kestrel-yk9n, mirror of kestrel-markets-f6ly): rides ONLY the
    // free path — a run that did NOT opt into the paid boundary via `--budget` (spend === undefined) — so
    // a persona following the advertised free CLI path is told, legibly, that this run cost nothing AND
    // how to cross into the paid boundary (a signed, settleable Offer). A `--budget` run has already
    // engaged the boundary (it either paused with the Offer, exit 5, or completed within its cap), so it
    // needs no nudge and carries none. Set here by the real command so deleting the render call site —
    // not editing render/sim.ts — is what a wiring guard catches.
    ...(spend === undefined
      ? { paidBoundaryNudge: { scenarioSlug: resolved.canonicalSlug } as PaidBoundaryNudge }
      : {}),
    ...(copyToken !== undefined ? { copyToken } : {}),
    ...(spend !== undefined ? { spend } : {}),
  });
}

/** The shared inputs to {@link runHostedProof}: a resolved scenario + a validated strategy. */
export interface HostedProofRun {
  readonly client: HostedFunnel;
  readonly resolved: ResolvedScenario;
  readonly strategy: Strategy;
  readonly ctx: OutputCtx;
  readonly hostedStorePath: string;
  /** Opaque copy-token forwarded verbatim to the trial mint (see {@link HostedFunnel.mintTrial}). */
  readonly copyToken?: string;
  /**
   * The caller's self-declared Spend cap (`--budget`, bead kestrel-4dz5), forwarded on
   * `POST /simulate` as `spend.budget` — the ONE trigger that lets the CLI reach the paid boundary
   * (a settleable Offer → exit 5). Absent → the free default drive, request byte-identical to before.
   */
  readonly spend?: Spend;
  /** When true (the `prove` front door), append the full three-part residency snippet after the proof. */
  readonly emitResidency?: boolean;
  /**
   * When true (the `sim` verb, kestrel-qupe), append the ONE-LINE residency guidance after the proof:
   * the proof URL is the durable handle because `runs list` is CWD-local. `sim` sets this; `prove`
   * uses the fuller {@link emitResidency} snippet instead. Rendered HERE by the real command (delete
   * this call — not the pure renderer — to catch the wiring).
   */
  readonly residencyOneLine?: boolean;
  /**
   * Whether the caller authored the strategy via `--plans` (true) or the run fell back to a bundled/
   * stand-down default (false/undefined). Governs the `--copy-token` continuation notice (kestrel-gu9h):
   * a copy-token claims to "reproduce or extend this proof", but the CLI can NEVER recover the origin
   * plan from the opaque token — so on a NON-authored copy-token run it emits a LOUD stderr notice that
   * the origin plan is not being carried (supply `--plans`), instead of silently running the baseline.
   */
  readonly authoredPlan?: boolean;
  /**
   * The one-frame lesson `prove` prints when its bare run used the BUNDLED example plan (no
   * `--plans`): what the example is + how to swap in your own. Set only by the `prove` front
   * door on its bundled-default path; absent whenever the caller supplied `--plans`. Rendered
   * HERE by the real command (delete this call — not the pure renderer — to catch the wiring).
   */
  readonly demoLesson?: DemoLesson;
  /**
   * The one-frame lesson `sim <slug>` prints when its run fell back to the STAND-DOWN default (no
   * `--plans`) — the honest counterpart to `prove`'s {@link demoLesson} (v6et). Bug: a plan-less
   * `sim <slug>` certifies an EMPTY, $0 session and said nothing, so a cold menu-browser (and the
   * crypto door, which routes straight to `sim <slug>`) reads the all-zeros grade as "broken". This
   * frame states plainly that the empty grade is a plan-less stand-down BY DESIGN and points at the
   * two next steps (author `--plans`, or `prove` for a bundled example that trades). Set ONLY by
   * `sim` on its no-`--plans` path; absent whenever the caller supplied `--plans` (and never on
   * `prove`, which passes {@link demoLesson} on its bundled-that-trades path instead — the two are
   * mutually exclusive). Rendered HERE by the real command (delete this call — not the pure
   * renderer — to catch the wiring).
   */
  readonly standDownLesson?: StandDownLesson;
  /**
   * The free-path paid-boundary nudge (kestrel-yk9n, mirror of kestrel-markets-f6ly). Set ONLY by
   * `sim` on the FREE path — a run that did not opt into the paid boundary via `--budget`
   * (`spend === undefined`). Bug f6ly: the free happy CLI path completes, prints its grade + proof
   * URL, and only ever offers MORE free stuff — the 402 value boundary (the signed, settleable Offer
   * that certifies a run against real spend) is invisible, so the persona never learns they could
   * "pay to certify MY book" and never converts. This nudge states plainly that the run was free and
   * HOW to cross into the paid boundary (`--budget`), inventing no price (the Offer is signed
   * server-side). Absent whenever `--budget` was declared (the boundary was already engaged — a paused
   * run prints the Offer itself) and on `prove`. Rendered HERE by the real command (delete this call —
   * not the pure renderer — to catch the wiring).
   */
  readonly paidBoundaryNudge?: PaidBoundaryNudge;
  /**
   * Suppress this function's OWN stdout render (still mints, records, and fires {@link onProof}).
   * `replay` uses it to drive a fresh run for its reproducibility check while owning the report.
   */
  readonly quiet?: boolean;
  /**
   * Fired on the SUCCESS path with the fresh proof's signature envelope + URL (before render).
   * `replay` uses it to assert the re-run is structurally reproducible and its signature
   * re-verifies. Never fired on a gated/settlement outcome.
   */
  readonly onProof?: (outcome: HostedProofOutcome) => void;
}

/** The fresh proof's verifiable envelope + graded shape, handed to {@link HostedProofRun.onProof}. */
export interface HostedProofOutcome {
  readonly proofUrl: string;
  readonly proofId: string;
  readonly root: string;
  readonly signature: string;
  readonly kid: string;
  readonly epoch: number;
  readonly metrics: { readonly orderCount: number; readonly fillCount: number; readonly realizedPnl: number };
}

/**
 * The SHARED hosted-proof core reused by `sim` and `prove`: mint an anonymous trial →
 * POST /simulate → read + validate the signed Grade proof → append the local receipt →
 * render. Its ONLY behavioral delta between the two verbs is `emitResidency` (prove appends
 * the persist-me snippet; sim does not). The selector case-rule, slug resolution, and
 * strategy loading stay in each verb's own body — this function starts at the mint.
 */
export async function runHostedProof(run: HostedProofRun): Promise<number> {
  const { client, resolved, strategy, ctx } = run;

  // ── mint an anonymous trial (carrying the opaque copy-token, if any), then drive the sim ──
  const trial = await client.mintTrial(run.copyToken !== undefined ? { copyToken: run.copyToken } : {});
  const g = await client.simulate(
    { source: strategy.source, artifactId: resolved.entry.artifact_id, ...(run.spend !== undefined ? { spend: run.spend } : {}) },
    trial.capability,
  );
  // A Spend-boundary suspension is surfaced as its itemized Offer + resume affordance (exit 5),
  // never the misleading "operation is not completed" 502 (kestrel-e8e7).
  if ("settlementRequired" in g) {
    renderSettlementRequired(g.settlementRequired, ctx);
    return EXIT.PAYMENT_REQUIRED;
  }
  if (g.gated) {
    renderOffer(g.payment, ctx);
    return EXIT.PAYMENT_REQUIRED;
  }
  const op = g.value;

  // ── read the grade proof for the metrics + the shareable URL ──
  const gradeArtifact = gradeArtifactOf(op);
  const gradeReceipt = gradeReceiptOf(op);
  const proofArtifactId = gradeArtifact.artifact_id;
  const proofUrl = proofWebUrl(proofArtifactId);
  // A COMPLETED, metered Operation is the caller's paid-for result. A transient proof 404 is only
  // R2 write lag — the signed grade is still SETTLING — so it must DEGRADE, never discard the run:
  // render the full story with the metrics + proof URL annotated "settling — retry shortly" and
  // exit 0 (kestrel-fzum). A proof that IS readable but fails verification is a real integrity
  // fault (HTTP_502, not a 404) and never reaches this catch — it stays fail-closed.
  let proof: HostedProof | null = null;
  try {
    proof = await client.proof(gradeArtifact, gradeReceipt, op.operation_id, trial.capability);
  } catch (e) {
    if (!isProofSettling(e)) throw e;
    process.stderr.write(
      `warning: the grade proof for ${op.operation_id} is not yet readable (settling — retry shortly); ` +
        `metrics are pending and this run is not yet in \`kestrel runs list --hosted\`: ${e.message}\n`,
    );
  }
  const metrics = proof === null ? null : gradedMetrics(proof.result.metrics);
  // The grader's never-fired reason (kestrel-kglw) — surfaced ONLY on a 0-order run, the highest-value
  // teaching moment that was otherwise silent on the default face. Gated on `orderCount === 0` so a run
  // that traded never carries it, and read straight off the signed proof (surface, never recompute).
  const neverFiredReason = metrics !== null && metrics.orderCount === 0 ? neverFiredReasonOf(proof) : undefined;

  // Hand the fresh proof's verifiable envelope to any observer (replay's reproducibility check).
  // A still-settling proof has no envelope to verify, so the observer is NOT fired — the caller
  // (replay) surfaces that honestly rather than fabricating an unverifiable outcome.
  if (proof !== null && metrics !== null) {
    run.onProof?.({
      proofUrl,
      proofId: proofArtifactId,
      root: proof.root,
      signature: proof.signature,
      kid: proof.verification.kid,
      epoch: proof.verification.epoch,
      metrics,
    });
  }

  // ── append the LOCAL hosted-run receipt so this run is re-listable (`runs list`, kestrel-pvlc) ──
  // Best-effort + zero network: the remote sim already succeeded and printed its proof URL, so a
  // failed local append DEGRADES the ledger but must not fail-report the sim (exit stays 0). It is
  // still LEGIBLE — the failure is logged loudly to stderr, never a silent drop. A still-settling
  // proof carries no honest metrics/timestamp, so the receipt is skipped rather than faked.
  if (proof !== null && metrics !== null) {
    recordHostedRun(run.hostedStorePath, {
      source: "hosted",
      slug: resolved.canonicalSlug,
      requested_slug: resolved.requestedSlug,
      operation_id: op.operation_id,
      grade_artifact_id: proofArtifactId,
      bus_artifact_id: op.evidence.busRef.artifact_id,
      manifest_artifact_id: op.evidence.manifestRef.artifact_id,
      order_count: metrics.orderCount,
      fill_count: metrics.fillCount,
      realized_pnl: metrics.realizedPnl,
      proof_url: proofUrl,
      // The server's signed-proof timestamp (never a local wall clock) — reproducible (RUNTIME §0).
      issued_at: proof.issuedAt ?? "",
      // The plan/strategy that produced this run (kestrel-etyu). `strategy` is the human LABEL (the
      // `--plans <file>` path, or the stand-down/bundled default's label); `strategy_digest` is the
      // deterministic CONTENT identity (sha256 of the canonical plan text) — the field that
      // distinguishes two runs of the SAME path edited IN PLACE (same label, different digest).
      strategy: strategy.label,
      strategy_digest: strategy.digest,
      // Record the opaque copy-token VERBATIM so a `--copy-token` continuation threads back to its
      // origin (kestrel-gu9h) — the CLI never interprets it (repo boundary), it just keeps the link.
      ...(run.copyToken !== undefined ? { copy_token: run.copyToken } : {}),
      // Record the grader's never-fired reason (kestrel-kglw) so a returning user who `runs show`s this
      // SAME 0-order run re-reads WHY nothing fired — the reason `renderSimStory` printed once on stdout
      // is otherwise lost, leaving a bare `order_count=0` one surface downstream. `neverFiredReason` is
      // already gated on `orderCount === 0` above (a traded run carries none), so this stores it only on
      // the run whose highest-value moment it explains — VERBATIM (the store never recomputes a verdict).
      ...(neverFiredReason !== undefined ? { never_fired_reason: neverFiredReason } : {}),
    });
  }

  // The `--copy-token` continuation notice (kestrel-gu9h). A copy-token claims to "reproduce or
  // extend this proof", but the origin plan lives in the origin run, NOT in the opaque token — the
  // CLI cannot recover it (repo boundary). So when a copy-token run did NOT author its own strategy
  // (`--plans`), say LOUDLY (to stderr, the diagnostics channel) that the origin plan is not carried
  // and this run used the bundled/stand-down baseline — never silently degrade. The lineage back to
  // the origin IS recorded (the copy_token on the receipt above). Suppressed for `replay` (quiet: it
  // owns its own report and reproduces from the no-leak baseline by design).
  if (!run.quiet && run.copyToken !== undefined && run.authoredPlan !== true) {
    process.stderr.write(
      `notice: --copy-token continues a proof, but the origin plan is NOT recoverable from the opaque token — ` +
        `this run used the bundled/stand-down baseline (${strategy.label}), not the strategy you are continuing. ` +
        `Supply \`--plans <your-plan>\` to author the continuation; the lineage back to the origin is recorded on the receipt.\n`,
    );
  }

  // `replay` drives the run for its own report — record + onProof happened, render is suppressed.
  if (run.quiet) return 0;

  if (ctx.mode === "json") {
    process.stdout.write(
      JSON.stringify({
        schema: "kestrel.sim/v1",
        scenario: resolved.canonicalSlug,
        requested: resolved.requestedSlug,
        operation: op,
        metrics: proof === null ? null : proof.result.metrics,
        // The grader's never-fired reason under a stable top-level key (kestrel-kglw) — the twin of
        // the human `why` line. Present ONLY on a 0-order run carrying a reason (never on a run that
        // traded), so a machine caller reads the SAME diagnostic the default face now states.
        ...(neverFiredReason !== undefined ? { never_fired_reason: neverFiredReason } : {}),
        proof_url: proofUrl,
        // The signed grade is still settling (transient R2 write lag) — surface it as data.
        ...(proof === null ? { proof_settling: true } : {}),
        // The residency ask rides a structured field in --json (never the printed snippet).
        ...(run.emitResidency ? { residency: residencyObject(proofUrl) } : {}),
        // sim's one-line residency guidance rides a structured field in --json (kestrel-qupe), so a
        // machine caller is warned its folder-bound history is not durable — the proof URL is (never
        // the printed line). Only when NOT already carrying the fuller `residency` snippet (prove).
        ...(run.residencyOneLine && !run.emitResidency ? { residency_guidance: renderResidencyOneLine(proofUrl) } : {}),
        // The one-frame demo lesson rides a structured field in --json (never the printed block).
        ...(run.demoLesson !== undefined ? { demo: demoLessonObject(run.demoLesson) } : {}),
        // The plan-less stand-down lesson rides a structured `stand_down` field in --json (v6et), so a
        // machine caller reads the SAME by-design-not-broken diagnostic the default face now prints —
        // never the printed block. Present ONLY on a `sim` run that used the stand-down default AND
        // whose readable grade is the empty shape (metrics present, orderCount 0) — never against a
        // still-settling grade, mirroring the human gate below.
        ...(run.standDownLesson !== undefined && metrics !== null && metrics.orderCount === 0
          ? { stand_down: standDownLessonObject(run.standDownLesson) }
          : {}),
        // The free-path paid-boundary nudge rides a structured `paid_nudge` field in --json (kestrel-yk9n),
        // so a machine caller reads the SAME "this run was free — reach the paid boundary via --budget"
        // affordance the default face prints — never the printed block. Present ONLY on the free path
        // (`paidBoundaryNudge` set), mirroring the human gate below.
        ...(run.paidBoundaryNudge !== undefined && metrics !== null ? { paid_nudge: paidBoundaryNudgeObject(run.paidBoundaryNudge) } : {}),
      }) + "\n",
    );
    return 0;
  }

  const story: SimStory = {
    canonicalSlug: resolved.canonicalSlug,
    requestedSlug: resolved.requestedSlug,
    viaAlias: resolved.viaAlias,
    title: resolved.entry.title,
    instrument: resolved.entry.instrument,
    period: resolved.entry.period,
    frameCount: resolved.entry.frame_count,
    strategyLabel: strategy.label,
    operation: { id: op.operation_id, status: op.status },
    receipts: summarizeReceipts(op.receipts),
    evidence: op.evidence,
    metrics,
    proofUrl,
  };
  renderSimStory(story, ctx);
  // The never-fired reason line (kestrel-kglw): a 0-order run is the highest-value teaching moment,
  // and it was SILENT on the default face until now. Driven HERE by the real command so removing THIS
  // call — not editing render/sim.ts — is what a wiring guard catches. Emitted only when the run placed
  // no orders AND the signed grade carried a reason (never on a run that traded).
  if (neverFiredReason !== undefined) process.stdout.write(neverFiredLine(neverFiredReason) + "\n");
  // The one-frame lesson for a bare `prove` running its BUNDLED example plan: what the example
  // did + how to swap in your own (the one-frame-lesson doctrine). Driven HERE by the real
  // command so removing THIS call — not editing render/sim.ts — is what a wiring guard catches.
  if (run.demoLesson !== undefined) process.stdout.write(renderDemoLesson(run.demoLesson) + "\n");
  // The plan-less stand-down lesson (v6et): a bare `sim <slug>` used the stand-down default and
  // certified an empty, $0 session — the honest counterpart to `prove`'s demo lesson. Print the
  // by-design-not-broken framing + the two next steps so the menu-browser/crypto-door path does not
  // dead-end reading the all-zeros grade as "broken". Driven HERE by the real command so removing
  // THIS call — not editing render/sim.ts — is what a wiring guard catches. Gated on the no-`--plans`
  // path (an authored run carries no `standDownLesson`) AND a readable, empty grade (metrics present,
  // orderCount 0): the lesson's "$0 / no-orders result" framing would be dishonest against a still-
  // SETTLING grade or a run that armed orders. Mirrors the `neverFiredLine` orderCount gate above.
  if (run.standDownLesson !== undefined && metrics !== null && metrics.orderCount === 0)
    process.stdout.write(renderStandDownLesson(run.standDownLesson) + "\n");
  // The free-path paid-boundary nudge (kestrel-yk9n, mirror of kestrel-markets-f6ly): a persona following
  // the advertised FREE CLI path completes, reads its grade + proof URL, and is only ever offered more
  // free stuff — the 402 value boundary (the signed, settleable Offer) is invisible, so it never converts.
  // Print, on the free path only, that this run cost nothing AND how to cross into the paid boundary
  // (`--budget`). Driven HERE by the real command so removing THIS call — not editing render/sim.ts — is
  // what a wiring guard catches. Gated on `paidBoundaryNudge` (set ONLY when no `--budget` opted into the
  // boundary; a paused `--budget` run prints the Offer itself, exit 5, and never reaches here) AND a
  // readable grade (`metrics !== null`): a still-SETTLING grade is a transient degraded state whose
  // attention is on "retry shortly", not the free happy path this conversion nudge belongs on — so it is
  // suppressed there, mirroring the `standDownLesson` metrics gate above.
  if (run.paidBoundaryNudge !== undefined && metrics !== null)
    process.stdout.write(renderPaidBoundaryNudge(run.paidBoundaryNudge) + "\n");
  // The persist-me snippet is the `prove` front door's success exhaust (n04e.1 / ADR-0038 §4).
  // Driven HERE by the real command (not the pure renderer) so removing this call — not editing
  // render/residency.ts — is what a wiring guard catches.
  if (run.emitResidency) process.stdout.write(renderResidencySnippet(proofUrl) + "\n");
  // sim's one-line residency guidance (kestrel-qupe): the proof URL is the shareable handle, and the
  // receipt is recorded to the identity-bound home ledger so `runs list` finds it from any directory
  // (kestrel-fq78). Driven HERE by the real command so removing THIS call — not editing
  // render/residency.ts — is what a wiring guard catches. Suppressed when the fuller `prove`
  // snippet already carried the same message (never both).
  if (run.residencyOneLine && !run.emitResidency) process.stdout.write(renderResidencyOneLine(proofUrl) + "\n");
  return 0;
}

/**
 * A proof-fetch failure that is only transient R2 write lag — the signed grade is still SETTLING:
 * a 404 on `GET /proof`. A completed, metered run degrades past this (the story renders with the
 * proof URL annotated) rather than being discarded (kestrel-fzum). Every OTHER proof failure —
 * verification refused (HTTP_502), a transport 5xx — is a real fault and stays fail-closed.
 */
function isProofSettling(e: unknown): e is CliError {
  return e instanceof CliError && e.code === "HTTP_404";
}

/**
 * Append the hosted-run receipt, best-effort. A local-ledger write must never turn a remotely
 * successful sim into a failure, so any fs error is logged LOUDLY to stderr (legible, not silent)
 * and swallowed — the run's exit code reflects the remote outcome, not the local receipt.
 */
function recordHostedRun(storePath: string, row: HostedRunRow): void {
  try {
    appendHostedRun(storePath, row);
  } catch (e) {
    process.stderr.write(
      `warning: could not append the hosted-run receipt to ${JSON.stringify(storePath)} — ` +
        `\`kestrel runs list --hosted\` will not show this run: ${e instanceof Error ? e.message : String(e)}\n`,
    );
  }
}

/** Build + render the bare-`sim` menu from the catalog entries. */
function renderMenu(entries: readonly HostedCatalogEntry[], ctx: OutputCtx): number {
  const scenarios = entries.map((e) => ({
    slug: slugifyTitle(e.title),
    title: e.title,
    instrument: e.instrument,
    artifactId: e.artifact_id,
    free: e.free,
  }));
  const menu: SimMenu = { scenarios, aliases: SCENARIO_ALIASES };
  if (ctx.mode === "json") {
    process.stdout.write(
      JSON.stringify({
        schema: "kestrel.sim.menu/v1",
        scenarios: scenarios.map((s) => ({
          slug: s.slug,
          title: s.title,
          instrument: s.instrument,
          artifact_id: s.artifactId,
          free: s.free,
        })),
        aliases: SCENARIO_ALIASES,
        usage: "kestrel sim <scenario-slug> [--plans <file>]",
      }) + "\n",
    );
    return 0;
  }
  renderSimMenu(menu, ctx);
  return 0;
}

// re-export for the router + tests
export type { HostedOperation };
