/**
 * # cli/commands/self — `kestrel whoami` + `kestrel refresh` (kestrel-p7th).
 *
 * The CLI is one of the FOUR EQUAL FACES (HTTP/SDK/CLI/MCP). The credential lifecycle — inspect
 * who you are, renew before the 1h durable capability lapses — existed only over MCP/HTTP
 * (`capability_refresh`); a CLI-only fleet operator had to `cat ~/.kestrel/credentials.json` to see
 * their identity and had NO renewal path, so CLI-only credentials silently aged out. This projects
 * that lifecycle onto the CLI face:
 *
 *   - `whoami`  → read the stored credential (+ any enrolled keypair) and print identity, scopes,
 *     expiry (with a CLI-side clock STATUS: valid / expiring / EXPIRED — display only, outside any
 *     Session), the API host it is bound to, and the on-disk file path. Pure-local: zero network.
 *   - `refresh` → re-mint the durable capability BEFORE it expires via the platform's existing
 *     `POST /capabilities/refresh` primitive (authenticated by the current capability), then
 *     overwrite the stored credential in place. The SERVER mints the fresh token — the CLI adds NO
 *     client-side RNG and no wall-clock into the token (determinism doctrine); the only clock use is
 *     the human-facing expiry STATUS, which is outside the Episode (OSS-ADR-0055).
 *
 * Dependency-light (node built-ins + fetch): no `bun:sqlite`, so both run under bun-less node like
 * the other light verbs.
 */

import type { GlobalFlags, OutputCtx } from "../context.ts";
import { parseArgs } from "../args.ts";
import { CliError, EXIT } from "../errors.ts";
import {
  defaultCredentialsPath,
  defaultKeypairPath,
  loadCredential,
  loadKeypair,
  saveCredential,
  type StoredCredential,
} from "../credentials.ts";

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

/** Injected seams so both verbs are hermetically testable (no real network, home, or clock). */
export interface SelfDeps {
  readonly fetch?: FetchLike;
  /** Where the credential is read/written; default `~/.kestrel/credentials.json`. */
  readonly credentialsPath?: string;
  /** Where the enrolled keypair is read; default `~/.kestrel/agent-key.json`. */
  readonly keypairPath?: string;
  /** Environment (for KESTREL_HOME); default `process.env`. */
  readonly env?: Record<string, string | undefined>;
  /** The DISPLAY clock for the expiry status line (never enters the token). Default `Date.now`. */
  readonly now?: () => number;
}

function notRegistered(credPath: string): CliError {
  return new CliError({
    code: "NOT_FOUND",
    exit: EXIT.NOT_FOUND,
    message: `no stored credential at ${credPath} — this CLI is anonymous (no registered identity)`,
    hint: "run `kestrel register` to self-register and mint a durable capability (proof-before-account: `sim`/`prove` still work anonymously)",
  });
}

/* ────────────────────────────────── whoami ─────────────────────────────────── */

/** The expiry STATUS derived from the DISPLAY clock (never the token). */
interface ExpiryStatus {
  readonly state: "valid" | "expiring" | "expired" | "unknown";
  readonly remaining_ms: number | null;
  readonly label: string;
}

/** The "expiring soon" display threshold — a durable capability lives 1h, so flag the last 5 min. */
const EXPIRING_SOON_MS = 5 * 60 * 1000;

/** Derive the human expiry status from `expiry` (ISO-8601) vs the display clock. Display only —
 * this clock never enters a token or a Session. An unparseable expiry is `unknown`, never a crash. */
export function expiryStatus(expiry: string, nowMs: number): ExpiryStatus {
  const at = Date.parse(expiry);
  if (Number.isNaN(at)) return { state: "unknown", remaining_ms: null, label: "unknown (unparseable expiry)" };
  const remaining = at - nowMs;
  if (remaining <= 0) return { state: "expired", remaining_ms: remaining, label: `EXPIRED ${fmtDuration(-remaining)} ago — run \`kestrel refresh\`` };
  if (remaining <= EXPIRING_SOON_MS) return { state: "expiring", remaining_ms: remaining, label: `expiring in ${fmtDuration(remaining)} — run \`kestrel refresh\`` };
  return { state: "valid", remaining_ms: remaining, label: `valid, expires in ${fmtDuration(remaining)}` };
}

/** A compact, deterministic mm/ss / hh:mm duration for the status label (display only). */
function fmtDuration(ms: number): string {
  const totalSec = Math.floor(ms / 1000);
  const h = Math.floor(totalSec / 3600);
  const m = Math.floor((totalSec % 3600) / 60);
  const s = totalSec % 60;
  if (h > 0) return `${h}h${String(m).padStart(2, "0")}m`;
  return `${m}m${String(s).padStart(2, "0")}s`;
}

/**
 * `whoami` — introspect the stored credential without hand-parsing credentials.json. Prints the
 * identity, scopes, expiry + status, the API host, whether a keypair is enrolled, and the file path.
 * No stored credential → a typed NOT_FOUND naming how to register. Zero network.
 */
export function whoamiCommand(argv: readonly string[], ctx: OutputCtx, deps: SelfDeps = {}): number {
  parseArgs(argv, new Set(), new Set()); // no flags — reject stray ones fail-closed
  const env = deps.env ?? (process.env as Record<string, string | undefined>);
  const credPath = deps.credentialsPath ?? defaultCredentialsPath(env);
  const cred = loadCredential(credPath);
  if (cred === undefined) throw notRegistered(credPath);

  const keyPath = deps.keypairPath ?? defaultKeypairPath(env);
  const keypair = loadKeypair(keyPath);
  const nowMs = (deps.now ?? Date.now)();
  const status = expiryStatus(cred.expiry, nowMs);

  if (ctx.mode === "json") {
    process.stdout.write(
      JSON.stringify({
        schema: "kestrel.whoami/v1",
        agent_id: cred.agent_id,
        subject: cred.subject ?? null,
        api: cred.api,
        token_type: cred.token_type,
        scopes: cred.scopes,
        expiry: cred.expiry,
        expiry_status: status.state,
        expiry_remaining_ms: status.remaining_ms,
        keypair_enrolled: keypair !== undefined,
        credential_path: credPath,
        ...(keypair !== undefined ? { keyfile_path: keyPath } : {}),
        ...(cred.claimed !== undefined ? { claimed_unverified: cred.claimed } : {}),
      }) + "\n",
    );
    return 0;
  }
  if (ctx.mode === "text") {
    const lines = [
      `agent_id\t${cred.agent_id}`,
      `subject\t${cred.subject ?? ""}`,
      `api\t${cred.api}`,
      `token_type\t${cred.token_type}`,
      `scopes\t${cred.scopes.join(",")}`,
      `expiry\t${cred.expiry}`,
      `expiry_status\t${status.state}`,
      `keypair_enrolled\t${keypair !== undefined}`,
      `credential_path\t${credPath}`,
    ];
    process.stdout.write(lines.join("\n") + "\n");
    return 0;
  }
  // human
  const out = [
    `agent_id     ${cred.agent_id}`,
    `subject      ${cred.subject ?? "(none)"}`,
    `api          ${cred.api}`,
    `token_type   ${cred.token_type}`,
    `scopes       ${cred.scopes.join(", ")}`,
    `expiry       ${cred.expiry}  (${status.label})`,
    `keypair      ${keypair !== undefined ? `enrolled — ${keyPath}` : "not enrolled"}`,
    `credential   ${credPath}`,
  ];
  if (cred.claimed !== undefined)
    out.push(`claimed      ${cred.claimed.name ?? "(no name)"} <${cred.claimed.email ?? "no-email"}>  (UNVERIFIED)`);
  process.stdout.write(out.join("\n") + "\n");
  return 0;
}

/* ────────────────────────────────── refresh ────────────────────────────────── */

/** The `POST /capabilities/refresh` response (mirrors the live platform primitive). */
interface RefreshResponse {
  readonly capability: string;
  readonly token_type?: string;
  readonly subject?: string;
  readonly family?: string;
  readonly scopes?: readonly string[];
  readonly expiry?: string;
  readonly ttl_ms?: number;
}

/** Extract a human-readable detail from a problem+json body, fail-soft to `fallback`. */
async function problemDetail(res: Response, fallback: string): Promise<string> {
  try {
    const b = (await res.json()) as { detail?: string; title?: string };
    return b.detail ?? b.title ?? fallback;
  } catch {
    return fallback;
  }
}

/**
 * `refresh` — renew the stored durable capability BEFORE it lapses, via the platform's existing
 * `POST /capabilities/refresh` primitive (authenticated by the current capability). The fresh token
 * is minted SERVER-SIDE (no client RNG, no client wall-clock in the token) and written back in place,
 * preserving the durable identity (agent_id / subject / keypair enrollment / claimed attribution).
 *
 * Host-scoped: it always refreshes against the credential's OWN `api` host — a credential is bound to
 * the host that minted it. No stored credential → a typed NOT_FOUND naming how to register; a server
 * refusal (an already-expired capability past its refresh window, a revoked family) surfaces the
 * server's problem+json detail, never a bare status.
 */
export async function refreshCommand(
  argv: readonly string[],
  ctx: OutputCtx,
  _globals: GlobalFlags,
  deps: SelfDeps = {},
): Promise<number> {
  parseArgs(argv, new Set(), new Set()); // no flags — reject stray ones fail-closed
  const env = deps.env ?? (process.env as Record<string, string | undefined>);
  const credPath = deps.credentialsPath ?? defaultCredentialsPath(env);
  const cred = loadCredential(credPath);
  if (cred === undefined) throw notRegistered(credPath);

  const fetchImpl = deps.fetch ?? (globalThis.fetch as FetchLike | undefined);
  if (fetchImpl === undefined)
    throw new CliError({
      code: "RUNTIME_UNAVAILABLE",
      exit: EXIT.RUNTIME_UNAVAILABLE,
      message: "global fetch unavailable — need node ≥18 or bun to refresh",
    });

  const base = cred.api.replace(/\/+$/, "");
  const res = await fetchImpl(`${base}/capabilities/refresh`, {
    method: "POST",
    headers: { "content-type": "application/json", accept: "application/json", authorization: `Bearer ${cred.capability}` },
    body: JSON.stringify({}),
  });
  if (!res.ok) {
    const detail = await problemDetail(res, `capability refresh failed (HTTP ${res.status})`);
    throw new CliError({
      code: `HTTP_${res.status}`,
      exit: res.status >= 500 ? EXIT.RUNTIME_UNAVAILABLE : EXIT.GENERIC,
      message: `capability refresh failed (HTTP ${res.status}): ${detail}`,
      ...(res.status === 401 || res.status === 403
        ? { hint: "the current capability may already be past its refresh window — run `kestrel register` to mint a fresh durable capability" }
        : {}),
    });
  }

  const body = (await res.json()) as RefreshResponse;
  if (typeof body.capability !== "string" || body.capability.length === 0)
    throw new CliError({
      code: "HTTP_502",
      exit: EXIT.RUNTIME_UNAVAILABLE,
      message: "capability refresh returned no usable capability token",
    });

  // Re-mint IN PLACE: the fresh server-signed token + its scopes/expiry, preserving the durable
  // identity fields the refresh response does not (re)issue. NO client RNG, no local clock in the token.
  const next: StoredCredential = {
    ...cred,
    capability: body.capability,
    ...(body.token_type !== undefined ? { token_type: body.token_type } : {}),
    ...(body.subject !== undefined ? { subject: body.subject } : {}),
    ...(body.scopes !== undefined ? { scopes: body.scopes } : {}),
    ...(body.expiry !== undefined ? { expiry: body.expiry } : {}),
  };
  saveCredential(next, credPath);

  emitRefreshResult(ctx, next, credPath);
  return 0;
}

/** Print the refresh summary (stdout is the payload channel). */
function emitRefreshResult(ctx: OutputCtx, cred: StoredCredential, credPath: string): void {
  if (ctx.mode === "json") {
    process.stdout.write(
      JSON.stringify({
        schema: "kestrel.refresh/v1",
        agent_id: cred.agent_id,
        subject: cred.subject ?? null,
        api: cred.api,
        token_type: cred.token_type,
        scopes: cred.scopes,
        expiry: cred.expiry,
        credential_path: credPath,
      }) + "\n",
    );
    return;
  }
  if (ctx.mode === "text") {
    process.stdout.write(
      `refreshed\tagent_id=${cred.agent_id}\tscopes=${cred.scopes.join(",")}\texpiry=${cred.expiry}\tcredential=${credPath}\n`,
    );
    return;
  }
  process.stdout.write(`refreshed ${cred.agent_id}\n`);
  process.stdout.write(`  scopes:     ${cred.scopes.join(", ")}\n`);
  process.stdout.write(`  expiry:     ${cred.expiry}\n`);
  process.stdout.write(`  credential: ${credPath}\n`);
}
