/**
 * # cli/index — the Kestrel CLI router (Kestrel CLI v1 §3)
 *
 * `main(argv)` resolves the render context ONCE, fast-paths the meta commands **before** any dynamic
 * import, dispatches LIGHT commands from eagerly-imported light modules, and lazy-imports the HEAVY
 * command modules only when a heavy verb is chosen. The whole body is wrapped in one try/catch; any
 * throw is rendered by {@link fail} and its exit code returned. `bin.ts` does `process.exit(await
 * main(...))`.
 *
 * Top-level imports are LIGHT only (args/context/errors/meta/lang/frame → `src/lang`, `src/frame`,
 * node built-ins). NO top-level import of `session/*`, `registry`, `bus`, or `adapters/lake`, so the
 * bundled `dist/cli.js` runs under bun-less node for every LIGHT command; heavy verbs fault loud
 * (exit 4) via `loadHeavy`.
 */

import { extractGlobals } from "./args.ts";
import { loadEnvFallback } from "./credentials.ts";
import { loadHeavy } from "./commands/_heavy.ts";
import { resolveFromProcess, type OutputCtx } from "./context.ts";
import { resolveCallerFromProcess, type Caller } from "./caller.ts";
import { CliError, EXIT, fail } from "./errors.ts";
import { helpCommand, versionCommand } from "./commands/meta.ts";
import { orientCommand } from "./commands/orient.ts";
import { validateCommand, printCommand } from "./commands/lang.ts";
import { frameCommand } from "./commands/frame.ts";
import { resolveBackend } from "./backend/select.ts";
import type { CliIo } from "./commands/agent.ts";

function unknownCommand(cmd: string): CliError {
  return new CliError({
    code: "USAGE",
    exit: EXIT.USAGE,
    message: `unknown command ${JSON.stringify(cmd)}`,
    hint: "run `kestrel help` for the command list",
  });
}

export async function main(argv: readonly string[], io?: CliIo): Promise<number> {
  // Hydrate `process.env` from the shared out-of-tree secrets home (~/.kestrel/.env, OSS-ADR-0054)
  // BEFORE any command resolves its config: a downstream reader (the `paper` verb's IBKR gateway
  // resolver, a bench pull) then sees a stored dev key as if it were exported. Fail-quiet and
  // dependency-light — `process.env` always WINS, an absent/hermetic store is a no-op, no value is
  // ever logged (so it cannot perturb determinism or a fixture's environment).
  loadEnvFallback();
  // Arg pre-parse + render-context resolution BOTH fail closed to a minimal machine-safe context: a throw
  // here (a bad global flag, `--format`/`--color` with no value) must render the SAME clean
  // `error\tcode=…` line and a NONZERO exit as any other failure — never escape `main` as an unhandled
  // rejection (which dumps a raw stack and, on older node, lets the process exit 0). `extractGlobals`
  // previously ran OUTSIDE this guard, so a bad global flag rejected `main` instead of failing closed.
  let parsed: ReturnType<typeof extractGlobals>;
  let ctx: OutputCtx;
  let caller: Caller;
  try {
    parsed = extractGlobals(argv);
    ctx = resolveFromProcess(parsed.globals);
    // The ONE Caller resolution (ADR-0035 §a/§b), env-first / TTY-second. Additive: it governs ONLY
    // the bare-invocation orientation below; every one-shot verb keeps `resolveOutputCtx`'s existing
    // per-command rendering byte-for-byte (the ladder does not re-render verbs — PRD §0.2).
    caller = resolveCallerFromProcess(parsed.globals);
  } catch (err) {
    return fail({ mode: "text", color: false, width: Infinity, interactive: false, stream: false }, err);
  }
  const { globals, rest, wantHelp, wantVersion } = parsed;

  try {
    // Meta fast-path — BEFORE any dynamic import. `help`/`--help` → the full usage (stdout, exit 0);
    // `<cmd> --help` (and `help <cmd>`) route to that command's OWN usage; `rest` still carries the
    // command path (extractGlobals peeled only the render/meta flags), and helpCommand stays LIGHT
    // (no heavy import), so per-command help never pulls bun/chdb (kestrel-1qc).
    if (wantVersion) return versionCommand(ctx);
    if (wantHelp) return helpCommand(ctx, rest);
    // The bare invocation (no verb, no `--help`) renders the ORIENTATION, not the generic help
    // (ADR-0035 §c, bead kestrel-jvr4.3). Same content two Renderings: a resolved agent/CI/pipe gets
    // static `text` and exits 0 without reading stdin; a confident human gets it as the session's
    // opening view. `orientCommand` is LIGHT (node built-ins only) — it never faults exit 4.
    if (rest.length === 0) return orientCommand(ctx, caller, globals);

    // Local is the DEFAULT; `--api`/`KESTREL_API` selects the RemoteBackend. Node-light: this
    // pulls no heavy runtime, so it is safe to resolve before dispatching any LIGHT command.
    const backend = resolveBackend(globals, process.env as Record<string, string | undefined>);

    const [command, ...args] = rest;
    switch (command) {
      // ── LIGHT (node-runnable, eager light imports) ──
      case "version":
        return versionCommand(ctx);
      case "help":
        return helpCommand(ctx);
      case "parse":
      case "validate":
        // `await`, not a bare return: a rejection must be caught by THIS try (the fail() renderer),
        // and the command is async only for its lazy arm-tier edge (node-portable engine import).
        return await validateCommand(args, ctx);
      case "print":
        return printCommand(args, ctx);
      case "card": {
        // The authoring on-ramp (kestrel-nde6): print the shipped agent language card / first-steps
        // walkthroughs from the INSTALLED package, offline. LIGHT (node built-ins + fs only, no
        // network, no bun/chdb) — lazy-imported to keep the meta fast-path graph minimal, like `sim`.
        const card = await import("./commands/card.ts");
        return card.cardCommand(args, ctx);
      }
      case "frame":
      case "percept":
        return await frameCommand(args, ctx, backend);
      case "register": {
        // Autonomous-agent self-registration (kestrel-markets-0t0, PLAT-ADR-0021 tier 1).
        // Lazy-imported: it is node-light (built-ins + fetch), but kept off the eager graph
        // so the meta/light hot path stays minimal. Threads `globals` for the API base.
        const register = await import("./commands/register.ts");
        return await register.registerCommand(args, ctx, globals);
      }
      case "whoami": {
        // Self-inspection of the stored credential (kestrel-p7th): identity, scopes, expiry +
        // display-clock status, API host, keypair enrollment, file path — no hand-parsing
        // credentials.json. LIGHT: pure-local read (node built-ins + fs only, ZERO network, no
        // bun/chdb). Lazy-imported to keep the meta fast-path graph minimal.
        const self = await import("./commands/self.ts");
        return self.whoamiCommand(args, ctx);
      }
      case "secrets": {
        // Operator/BYOK secret residency in `~/.kestrel/.env` (kestrel-jh9w.2 over the jh9w.1
        // store). LIGHT: node built-ins + fs only, ZERO network. A secret VALUE never comes from
        // argv (prompt or --stdin only) and never appears in any output path.
        const secrets = await import("./commands/secrets.ts");
        return await secrets.secretsCommand(args, ctx);
      }
      case "refresh": {
        // Renew the stored durable capability BEFORE it lapses (kestrel-p7th), projecting the
        // platform's existing `POST /capabilities/refresh` primitive onto the CLI face. LIGHT:
        // node built-ins + fetch, host-scoped to the credential's own `api`. The fresh token is
        // minted SERVER-SIDE — no client RNG, no client wall-clock in the token (determinism
        // doctrine). Lazy-imported to keep the meta fast-path graph minimal.
        const self = await import("./commands/self.ts");
        return await self.refreshCommand(args, ctx, globals);
      }
      case "sim": {
        // The one-command funnel (kestrel-585 / kestrel-vcn). LIGHT: it drives the hosted
        // funnel over fetch only (no bun/chdb), so the site's hero snippet runs under plain
        // node. It always routes remote (a sim positional NAMES platform data) — its own base
        // resolution, NOT `resolveBackend` (which defaults local). Lazy-imported to keep the
        // meta fast-path graph minimal, but the module stays node-light so it never faults exit 4.
        const sim = await import("./commands/sim.ts");
        return await sim.simCommand(args, ctx, globals);
      }
      case "prove": {
        // The zero-credential front door (kestrel-markets-n04e.1). LIGHT: it reuses `sim`'s
        // hosted core over fetch only (no bun/chdb) — so `npx kestrel.markets prove` runs under
        // plain node with no key, no config, no prompt. Lazy-imported to keep the meta fast-path
        // graph minimal; it stays node-light so it never faults exit 4.
        const prove = await import("./commands/prove.ts");
        return await prove.proveCommand(args, ctx, globals);
      }
      case "replay": {
        // The reproduction verb (n04e.1): re-run a local proof, or degrade to verifying a
        // published one. LIGHT — the same fetch-only hosted core + the local JSONL ledger.
        const replay = await import("./commands/replay.ts");
        return await replay.replayCommand(args, ctx, globals);
      }
      case "verify": {
        // The zero-trust proof check (n04e.1): re-verify a published proof's Ed25519 signature
        // against the INDEPENDENTLY-fetched published keys (node crypto). LIGHT, mints no trial.
        const verify = await import("./commands/verify.ts");
        return await verify.verifyCommand(args, ctx, globals);
      }
      case "certify": {
        // Open recomputation (gate G10, kestrel-8kvs): fetch the proof's evidence bundle, re-project
        // the Blotter LOCALLY with the shipped projector, and assert byte-identical reproduction of the
        // hosted result. LIGHT — project/serialize/readBusText + pure sha256, no bun/chdb, no engine drive.
        const certify = await import("./commands/certify.ts");
        return await certify.certifyCommand(args, ctx, globals);
      }

      // ── HEAVY *for the LOCAL transport only* (lazy import, then lazy bun/chdb inside) ──
      case "agent": {
        // The machine/agent mode — a LOSSLESS projection of the djm.5 SDK (kestrel-djm.6). Reads a JSONL
        // request stream and writes versioned protocol JSONL; endpoint selection (globals.api) is transport-
        // ONLY, and it is exactly what decides whether this verb is heavy at all (kestrel-z5lr).
        //
        // `agent` with NO `--api` builds `createSdk(localTransport())` — the local runtime — so it must pass
        // the same up-front bun guard every heavy verb passes (kestrel-mkn review B1): the module graph loads
        // fine under node (the Bun dependency is a LAZY global, not a `bun:*` specifier), so without the guard
        // {op:catalog} answered normally-looking data and {op:openSession} leaked a raw `Bun is not defined`
        // onto the PROTOCOL channel instead of the loud exit-4 RUNTIME_UNAVAILABLE.
        //
        // `agent --api <url>` builds `createSdk(remoteTransport(...))` — pure HTTP over `fetch`, ZERO bun
        // dependency, the node-only thin client the front door targets. Gating IT on the runtime over-fired:
        // it hard-refused exit 4 under bun-less node while the equivalent `run --api` (which gates at the
        // POINT OF USE — `LocalBackend.openSession`/`openDaySession` call `loadHeavy`, the RemoteBackend never
        // does) worked, breaking the documented transport parity. So gate on `globals.api === undefined`: the
        // LOCAL transport demands the heavy runtime, the REMOTE transport never did and no longer claims to.
        // `buildSdk` reads `globals.api` by the SAME rule, so the gate and the transport cannot disagree.
        const load = (): Promise<typeof import("./commands/agent.ts")> => import("./commands/agent.ts");
        const agent = globals.api === undefined ? await loadHeavy(load) : await load();
        return await agent.agentCommand(globals, io);
      }
      case "mcp": {
        // The MCP face over stdio (kestrel-0f7i) — `serveStdio(createKestrelMcpServer(...))` behind a
        // transport CHOICE. DEFAULT is the REMOTE transport (pure fetch, node-runnable — the npx stdio
        // drop-in an MCP client configures); `--local` opts into the in-process engine and is the ONE
        // heavy arm, gated through `loadHeavy` exactly like `agent`'s local transport so a bun-less
        // node refuses LOUD (exit 4) instead of leaking a raw `Bun is not defined` onto the MCP wire.
        // The gate keys on the SAME tokens `resolveMcpTransport` reads (`--local` present AND no
        // `--api`), so gate and transport cannot disagree — and a `--local --api` CONTRADICTION is
        // diagnosed as USAGE (inside the command, whose module graph is node-loadable) on every host,
        // never masked by a runtime-availability refusal.
        const load = (): Promise<typeof import("./commands/mcp.ts")> => import("./commands/mcp.ts");
        const wantsLocal = args.includes("--local") && globals.api === undefined;
        const mcp = wantsLocal ? await loadHeavy(load) : await load();
        return await mcp.mcpCommand(args, globals);
      }
      case "run": {
        const grade = await import("./commands/grade.ts");
        return await grade.runCommand(args, ctx, backend);
      }
      case "day": {
        const grade = await import("./commands/grade.ts");
        return await grade.dayCommand(args, ctx, backend);
      }
      case "paper": {
        // The PAPER session (kestrel-7o2.12) — a LIVE feed + the venue's paper gate under the SAME
        // engine `run`/`day` drive over recorded tape. Deliberately LOCAL-only (no `backend`): a paper
        // session is a socket to the operator's OWN client-launched IB Gateway on their OWN machine —
        // there is nothing here a remote backend could run on their behalf. PAPER-only by construction;
        // the driver can build no other gate.
        const paper = await import("./commands/paper.ts");
        return await paper.paperCommand(args, ctx);
      }
      case "runs": {
        const [sub, ...subRest] = args;
        const reg = await import("./commands/registry.ts");
        if (sub === "list") return await reg.runsListCommand(subRest, ctx);
        if (sub === "show") return await reg.runsShowCommand(subRest, ctx);
        if (sub === "compare") return await reg.runsCompareCommand(subRest, ctx);
        throw new CliError({
          code: "USAGE",
          exit: EXIT.USAGE,
          message: `unknown \`runs\` subcommand ${JSON.stringify(sub ?? "")} — one of list | show | compare`,
        });
      }
      case "lineage": {
        const reg = await import("./commands/registry.ts");
        return await reg.lineageCommand(args, ctx);
      }
      case "leaderboard": {
        const reg = await import("./commands/registry.ts");
        return await reg.leaderboardCommand(args, ctx);
      }

      default:
        throw unknownCommand(command!);
    }
  } catch (err) {
    return fail(ctx, err);
  }
}
