/**
 * # cli/commands/orient — the bare-npx ORIENTATION (ADR-0035 §c, PRD §0.2, bead kestrel-jvr4.3)
 *
 * `npx kestrel.markets` with NO args renders neither the generic help nor an alphabetical command
 * dump. It renders an **orientation**: (1) a **spectator Frame** — a default watchlist + rectangular
 * panes of scalar cells — and (2) the CLI's **commands grouped by task** (*see the market · write a
 * plan · test it · review · go live*). `kestrel help` / `--help` still print the full usage (help is
 * NOT the orientation); the orientation points AT them.
 *
 * ## Same content, two Renderings (ADR-0035 §c)
 * An agent / CI / pipe gets the orientation as static `text` and moves on (exit 0, no stdin read). A
 * **confident human** gets the *same* orientation as the **opening view** of the interactive session.
 * The full interactive session is a later phase of the epic; for now the human Rendering IS the static
 * orientation, presented as that opening view (it only adds a one-line "opening view" banner) — so
 * this bead ships the orientation both ways without pretending the TUI exists yet.
 *
 * ## Data honesty (PRD §7 acceptance: "nothing phoned home")
 * The spectator panes come from data shipped IN the package — never a network call, never an invented
 * number. The **provenance-honest** source is `artifacts/sdk/catalog-records.json` (a package `files`
 * entry: the public-baseline recorded sessions, each carrying its own `recordedAt` date). Its rows are
 * rendered with that recorded date, so a recorded tape is never passed off as live. The **levels**
 * pane is honestly all-`UNKNOWN` — no live market feed ships in-package, and a bare invocation makes no
 * request. `SpectatorFrame.asof` is the ONE permitted wall clock in the whole contract (invariant 4):
 * it has nothing at stake, and the DATA rows state their recorded date so the wall clock never launders
 * a stale number into a live one. If the shipped catalog is somehow absent, every pane degrades to
 * all-`UNKNOWN` with the watchlist + a provenance note — still honest, still an orientation.
 *
 * Node-light (built-ins only: `fs`/`path`/`url`) — no `bun:*`, no network — so it runs under plain node
 * and never reads stdin. `null` cells render as `UNKNOWN` (never blank); the contract object carries
 * only scalar cells, never pre-rendered ascii.
 */

import { existsSync, readFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";

import type { GlobalFlags, OutputCtx } from "../context.ts";
import type { Caller } from "../caller.ts";
import { VERSION } from "./meta.ts";

// ─────────────────────────────────────────────────────────────────────────────
// The spectator contract shapes (oversight contract §3.4). Mirrored here — the canonical home is
// `src/protocol/oversight.ts` (a NEW leaf that lands with the full oversight contract bead); this
// bead needs only the spectator subset and does not fork the shape.
// ─────────────────────────────────────────────────────────────────────────────

/** A cell is a scalar. `null` is UNKNOWN and renders as such — never blank, never invented. */
export type SpectatorCell = string | number | null;

/** A RECTANGULAR pane of scalar cells — so any Rendering can draw it without inventing a value.
 *  NOT pre-rendered ascii (oversight contract §3.4). */
export interface SpectatorPane {
  readonly paneId: string;
  readonly columns: readonly string[];
  readonly rows: readonly (readonly SpectatorCell[])[];
}

/** Spectator context: NOTHING AT STAKE (the bare-invocation orientation). Off the deterministic path,
 *  so `asof` (a wall clock) is permitted HERE and nowhere else (contract invariant 4). */
export interface SpectatorFrame {
  readonly watchlist: readonly string[];
  readonly asof: string;
  readonly panes: readonly SpectatorPane[];
}

/** How each pane's rows were sourced — Rendering-level provenance, kept OFF the scalar contract object
 *  (the frame stays pure). `recorded` names the shipped recorded date; `unknown` = no live source ships. */
export interface OrientationProvenance {
  readonly paneId: string;
  readonly note: string;
}

/** The default watchlist — generic illustrative instruments only (ARCHITECTURE §7). */
export const DEFAULT_WATCHLIST = ["SPX", "SPY", "QQQ"] as const;

// ─────────────────────────────────────────────────────────────────────────────
// Task-grouped commands (ADR-0035 §c) — never an alphabetical dump. Each verb is a REAL shipped verb.
// ─────────────────────────────────────────────────────────────────────────────

export interface TaskGroup {
  readonly task: string;
  readonly steps: readonly { readonly cmd: string; readonly purpose: string }[];
}

export const TASK_GROUPS: readonly TaskGroup[] = [
  {
    task: "see the market",
    steps: [
      { cmd: "kestrel sim", purpose: "run a curated hosted scenario, free (bare: the menu)" },
      { cmd: "kestrel prove", purpose: "the zero-credential proof — no key, no config, no prompt" },
    ],
  },
  {
    task: "write a plan",
    steps: [
      { cmd: "kestrel card first-plan", purpose: "author your first document (offline walkthrough)" },
      { cmd: "kestrel parse <file>", purpose: "parse + validate a plan document" },
    ],
  },
  {
    task: "test it",
    steps: [
      { cmd: "kestrel run --bus <p> --plans <p> --fill <m> --r-usd <n>", purpose: "grade a session against a recorded bus" },
      { cmd: "kestrel day --bus <p> --dir <d> --fill <m> --r-usd <n>", purpose: "a stepped/wake session" },
    ],
  },
  {
    task: "review",
    steps: [
      { cmd: "kestrel runs list", purpose: "query recorded runs" },
      { cmd: "kestrel runs show <id>", purpose: "show one run + its plans (or a hosted proof URL)" },
      { cmd: "kestrel leaderboard", purpose: "the ranked leaderboard over recorded runs" },
    ],
  },
  {
    task: "go live",
    steps: [
      { cmd: "kestrel register", purpose: "self-register as an autonomous agent" },
      { cmd: "kestrel paper --instrument <s> …", purpose: "a PAPER session on a live IB Gateway feed (never live money)" },
    ],
  },
];

// ─────────────────────────────────────────────────────────────────────────────
// Shipped data source — artifacts/sdk/catalog-records.json (a package `files` entry).
// ─────────────────────────────────────────────────────────────────────────────

interface CatalogEntry {
  readonly id: string;
  readonly permittedFillModel?: string;
  readonly provenance?: { readonly recordedAt?: string };
}
interface CatalogRecord {
  readonly blotter?: { readonly settlement?: { readonly realized?: number; readonly asset?: string } };
}
interface CatalogFile {
  readonly catalogEntries?: readonly CatalogEntry[];
  readonly records?: Readonly<Record<string, CatalogRecord>>;
}

/**
 * Resolve the shipped `artifacts/sdk/catalog-records.json` by ascending from THIS module's own
 * location until it is found — handles both the repo checkout (`src/cli/commands/`, three up) and the
 * bundled install (`dist/cli.js`, one up) without a hard-coded depth, exactly like `card.ts`. Returns
 * `null` (never throws) if it is somehow absent — the orientation then degrades to all-UNKNOWN.
 */
function resolveCatalogPath(): string | null {
  let dir = dirname(fileURLToPath(import.meta.url));
  const rel = join("artifacts", "sdk", "catalog-records.json");
  for (let i = 0; i < 8; i++) {
    const candidate = join(dir, rel);
    if (existsSync(candidate)) return candidate;
    const parent = dirname(dir);
    if (parent === dir) break;
    dir = parent;
  }
  return null;
}

/** Load the shipped catalog, or `null` if absent / unreadable / malformed (fail-soft: honesty over a crash). */
export function loadCatalog(): CatalogFile | null {
  const path = resolveCatalogPath();
  if (path === null) return null;
  try {
    return JSON.parse(readFileSync(path, "utf8")) as CatalogFile;
  } catch {
    return null;
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// Build the spectator frame (pure, given the catalog + a wall-clock ISO).
// ─────────────────────────────────────────────────────────────────────────────

/** The recorded date (YYYY-MM-DD) of a catalog entry, or `null` if unrecorded. */
function recordedDate(e: CatalogEntry): string | null {
  const at = e.provenance?.recordedAt;
  return typeof at === "string" && at.length >= 10 ? at.slice(0, 10) : null;
}

/**
 * Build the {@link SpectatorFrame} + its Rendering-level provenance. Two panes:
 *  - **levels** — the watchlist with all-`null` (UNKNOWN) live cells: no live feed ships in-package,
 *    and a bare invocation makes no request. This is the honest all-UNKNOWN case, in-product.
 *  - **public-baseline** — the shipped recorded sessions (id · fill model · settled usd · recorded
 *    date). Real scalar values, each stamped with its own recorded date. Omitted only if the catalog
 *    is absent, in which case the frame is watchlist + the levels pane alone — still an orientation.
 */
export function buildSpectatorFrame(nowIso: string, catalog: CatalogFile | null): { frame: SpectatorFrame; provenance: readonly OrientationProvenance[] } {
  const watchlist = [...DEFAULT_WATCHLIST];

  const levels: SpectatorPane = {
    paneId: "levels",
    columns: ["symbol", "last", "chg", "status"],
    // Every live cell is UNKNOWN (null): no bundled live feed. `symbol` is the only known scalar.
    rows: watchlist.map((sym) => [sym, null, null, null] as const),
  };

  const panes: SpectatorPane[] = [levels];
  const provenance: OrientationProvenance[] = [
    { paneId: "levels", note: "no live market feed ships in this package — levels UNKNOWN; no request is made." },
  ];

  const entries = catalog?.catalogEntries ?? [];
  const records = catalog?.records ?? {};
  if (entries.length > 0) {
    const rows = entries.map((e): readonly SpectatorCell[] => {
      const settled = records[e.id]?.blotter?.settlement?.realized;
      return [
        e.id,
        e.permittedFillModel ?? null,
        typeof settled === "number" ? settled : null, // absent settlement ⇒ UNKNOWN, never a silent 0
        recordedDate(e), // the row's OWN recorded date — never the wall clock
      ];
    });
    panes.push({ paneId: "public-baseline", columns: ["session", "fill model", "settled (usd)", "recorded"], rows });
    provenance.push({
      paneId: "public-baseline",
      note: "recorded public-baseline catalog, shipped in this package (artifacts/sdk/catalog-records.json) — not live.",
    });
  }

  return { frame: Object.freeze({ watchlist, asof: nowIso, panes }), provenance };
}

// ─────────────────────────────────────────────────────────────────────────────
// Rendering — glyphs/layout only, invents no value. `null` → "UNKNOWN".
// ─────────────────────────────────────────────────────────────────────────────

const BOLD = "[1m";
const DIM = "[2m";
const RESET = "[0m";

/** The visible glyph for a scalar cell: `UNKNOWN` for null (never blank), else the value's string. */
export function cellGlyph(c: SpectatorCell): string {
  return c === null ? "UNKNOWN" : String(c);
}

function padEnd(s: string, w: number): string {
  return s.length >= w ? s : s + " ".repeat(w - s.length);
}

/** Render one pane as a fixed-width table (header + rows), null → UNKNOWN. */
function renderPane(pane: SpectatorPane, color: boolean): string {
  const header = (s: string): string => (color ? `${BOLD}${s}${RESET}` : s);
  const body = pane.rows.map((r) => r.map(cellGlyph));
  const widths = pane.columns.map((h, c) => Math.max(h.length, ...body.map((row) => (row[c] ?? "").length), 0));
  const line = (cells: readonly string[]): string => cells.map((cell, c) => padEnd(cell, widths[c]!)).join("  ");
  const lines: string[] = [];
  lines.push("  " + header(line(pane.columns)));
  for (const row of body) lines.push("  " + line(row));
  return lines.join("\n");
}

/**
 * The machine JSON Rendering of the same orientation (an explicit `--json` on the bare invocation —
 * ladder rung 1: the flag wins). The spectator frame rides as DATA — scalar cells, `null` = UNKNOWN —
 * which is exactly the contract §3.4 shape both Renderings consume; nothing is pre-rendered.
 */
export function renderOrientationJson(
  frame: SpectatorFrame,
  provenance: readonly OrientationProvenance[],
  caller: Caller,
): string {
  return (
    JSON.stringify({
      schema: "kestrel.orient/v1",
      version: VERSION,
      caller: { kind: caller.kind, detectedBy: caller.detectedBy, interactive: caller.interactive },
      spectator: frame,
      provenance,
      tasks: TASK_GROUPS,
      see: ["kestrel help", "kestrel card"],
    }) + "\n"
  );
}

/**
 * Render the full orientation to a string. `text`/agent = plain; `human` = bold headers when
 * `ctx.color`. When `caller.interactive` (a confident human), an "opening view" banner is prepended —
 * the human Rendering of the same content as the session's opening view.
 */
export function renderOrientation(
  frame: SpectatorFrame,
  provenance: readonly OrientationProvenance[],
  ctx: OutputCtx,
  caller: Caller,
): string {
  const color = ctx.mode === "human" && ctx.color;
  const header = (s: string): string => (color ? `${BOLD}${s}${RESET}` : s);
  const dim = (s: string): string => (color ? `${DIM}${s}${RESET}` : s);
  const lines: string[] = [];

  lines.push(`kestrel ${VERSION} — orient`);
  if (caller.interactive) {
    // The opening view of the interactive session (a local paper Pod). The full session is a later
    // phase; for now this banner is the only difference between the two Renderings.
    lines.push(dim("opening view — interactive session (local paper Pod) is coming; showing your orientation"));
  }
  lines.push("");

  // ── the spectator frame ──
  lines.push(header("the market"));
  lines.push(`  watchlist: ${frame.watchlist.join(" ")}`);
  lines.push(`  ${dim(`as of ${frame.asof}  (wall clock — spectator only, nothing at stake)`)}`);
  const provByPane = new Map(provenance.map((p) => [p.paneId, p.note]));
  for (const pane of frame.panes) {
    lines.push("");
    lines.push(`  ${header(pane.paneId)}`);
    lines.push(renderPane(pane, color));
    const note = provByPane.get(pane.paneId);
    if (note !== undefined) lines.push(`  ${dim(note)}`);
  }
  lines.push("");

  // ── task-grouped commands ──
  lines.push(header("next moves"));
  for (const group of TASK_GROUPS) {
    lines.push("");
    lines.push(`  ${header(group.task)}`);
    const w = Math.max(...group.steps.map((s) => s.cmd.length));
    for (const step of group.steps) lines.push(`    ${padEnd(step.cmd, w)}  ${dim(step.purpose)}`);
  }
  lines.push("");
  lines.push(dim("full command list: `kestrel help`   ·   learn the language (offline): `kestrel card`"));

  return lines.join("\n") + "\n";
}

/**
 * The bare-invocation orientation command. Builds the spectator frame from shipped data (no network,
 * no stdin read), renders it for the resolved Caller, writes it to stdout, and returns exit 0.
 *
 * The single wall clock (`SpectatorFrame.asof`) is read HERE — the one place the whole contract permits
 * it (invariant 4), because the spectator frame has nothing at stake.
 */
export function orientCommand(ctx: OutputCtx, caller: Caller, globals: GlobalFlags = {}): number {
  // Diagnostic (stderr only — stdout stays a pure orientation channel): under KESTREL_DEBUG, emit how
  // the Caller was resolved, so the detection ladder is observable THROUGH the real driver (a spawned
  // binary) as well as in the pure unit matrix — the env-beats-TTY rule is asserted both ways.
  const dbg = process.env.KESTREL_DEBUG;
  if (dbg !== undefined && dbg !== "" && dbg !== "0") {
    process.stderr.write(
      `caller\tkind=${caller.kind}\tharness=${caller.harness ?? ""}\tdetectedBy=${caller.detectedBy}\tinteractive=${caller.interactive}\n`,
    );
  }
  const { frame, provenance } = buildSpectatorFrame(new Date().toISOString(), loadCatalog());
  if (ctx.mode === "json") {
    // An explicit `--json` on the bare invocation (ladder rung 1: the flag wins) — machine JSON.
    process.stdout.write(renderOrientationJson(frame, provenance, caller));
    return 0;
  }
  // Ladder rung 2 (ADR-0035 §b): a DETECTED agent/CI gets machine `text` even while holding a PTY —
  // `resolveOutputCtx` probes only the TTY + its own agent vars, so a PTY-holding agent would resolve
  // `human` there; for the BARE invocation the Caller (env-first) governs the skin. An explicit
  // `--format` still wins (rung 1) — only the un-flagged agent/CI case is downgraded. One-shot verbs
  // are untouched: this adjustment lives inside the orientation only.
  const explicitFormat = globals.json === true || globals.format !== undefined;
  const effCtx: OutputCtx =
    !explicitFormat && caller.kind === "agent" && ctx.mode === "human"
      ? { mode: "text", color: false, width: Infinity, interactive: false, stream: false }
      : ctx;
  process.stdout.write(renderOrientation(frame, provenance, effCtx, caller));
  return 0;
}
