/**
 * # cli/commands/register — `kestrel register` (kestrel-markets-0t0 + gaz).
 *
 * The CLI half of autonomous-agent self-registration (PLAT-ADR-0021 tier 1 — the
 * middle rung between anon and a human account, NO human in the loop). It:
 *
 *   1. reads git `user.name` / `user.email` (unless `--no-git-identity`);
 *   2. DISCLOSES to stderr exactly what it will send + that the identity is UNVERIFIED
 *      + how to opt out (AX doctrine, bead gaz — the CLI never sends claimed identity
 *      silently);
 *   3. POSTs to the managed platform's `POST /agents/register`;
 *   4. stores the returned durable capability in `~/.kestrel/credentials.json` (0600)
 *      so subsequent `--api` calls present it instead of self-minting an anon trial.
 *
 * The claimed git identity is FREELY SPOOFABLE and the platform treats it as unverified
 * attribution only (bead gaz) — this command discloses that plainly rather than implying
 * the registration is authenticated.
 *
 * Dependency-light (node built-ins + fetch): no `bun:sqlite`, so it runs under bun-less
 * node like the other light verbs.
 */

import { execFileSync } from "node:child_process";
import type { GlobalFlags, OutputCtx } from "../context.ts";
import { parseArgs } from "../args.ts";
import { CliError, EXIT } from "../errors.ts";
import {
  defaultCredentialsPath,
  defaultKeypairPath,
  loadCredential,
  saveCredential,
  saveKeypair,
  type StoredCredential,
  type StoredKeypair,
} from "../credentials.ts";
import { generateAgentKeypair } from "../keypair.ts";

/** The canonical managed API base (mirrors backend/remote.ts DEFAULT_API). */
export const DEFAULT_API = "https://api.kestrel.markets";

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

/** Claimed (UNVERIFIED) git identity — freely spoofable, never trusted (bead gaz). */
export interface GitIdentity {
  readonly name?: string;
  readonly email?: string;
}

/** Injected seams so the command is hermetically testable (no network, no real git/home). */
export interface RegisterDeps {
  readonly fetch?: FetchLike;
  /** Read the local git identity; default shells out to `git config`. */
  readonly gitIdentity?: () => GitIdentity;
  /** Where to persist the credential; default `~/.kestrel/credentials.json`. */
  readonly credentialsPath?: string;
  /** Where to persist the enrolled keypair (--keypair); default `~/.kestrel/agent-key.json`. */
  readonly keypairPath?: string;
  /** Environment (for KESTREL_API); default `process.env`. */
  readonly env?: Record<string, string | undefined>;
}

/** The registration response body (mirrors the platform's AgentRegistrationBody). */
interface RegistrationResponse {
  readonly agent_id: string;
  readonly subject?: string;
  readonly capability: string;
  readonly token_type: string;
  readonly scopes: readonly string[];
  readonly expiry: string;
  readonly upgrade?: { readonly verify_email_hint?: string; readonly pm_authority_hint?: string };
}

/** The enroll-key response body (mirrors the platform's EnrollmentBody). */
interface EnrollmentResponse {
  readonly public_key_thumbprint: string;
  readonly granted_scopes: readonly string[];
  readonly jwt: { readonly audience: string; readonly max_ttl_seconds: number };
}

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

/** Read one git config value, or undefined if git is absent / unset / not a repo. */
function readGitConfig(key: string): string | undefined {
  try {
    const out = execFileSync("git", ["config", "--get", key], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
    const v = out.trim();
    return v.length > 0 ? v : undefined;
  } catch {
    return undefined;
  }
}

/** The default git-identity reader — `git config user.name` / `user.email`. */
export function defaultGitIdentity(): GitIdentity {
  const name = readGitConfig("user.name");
  const email = readGitConfig("user.email");
  return { ...(name !== undefined ? { name } : {}), ...(email !== undefined ? { email } : {}) };
}

/** Resolve the managed API base from `--api` > `KESTREL_API` > the canonical default. */
function resolveApiBase(globals: GlobalFlags, env: Record<string, string | undefined>): string {
  const raw = globals.api ?? env.KESTREL_API;
  if (raw === undefined || raw === "" || raw === "default") return DEFAULT_API;
  return raw.replace(/\/+$/, "");
}

/**
 * `register [--name <n>] [--scopes <s,s>] [--no-git-identity]`.
 *
 * Sends the git identity as CLAIMED attribution unless `--no-git-identity`. On success,
 * persists the durable capability and prints a summary (stdout is the payload channel;
 * disclosure goes to stderr).
 */
export async function registerCommand(
  argv: readonly string[],
  ctx: OutputCtx,
  globals: GlobalFlags,
  deps: RegisterDeps = {},
): Promise<number> {
  const { flags, bools } = parseArgs(argv, new Set(["no-git-identity", "keypair"]), new Set(["name", "scopes"]));
  const env = deps.env ?? (process.env as Record<string, string | undefined>);
  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 register",
    });
  }

  const base = resolveApiBase(globals, env);
  const withGit = !bools.has("no-git-identity");
  const identity: GitIdentity = withGit ? (deps.gitIdentity ?? defaultGitIdentity)() : {};
  const name = flags.get("name");
  // `--scopes a,b` → a normalized scope list. It is forwarded as the COMMA-SEPARATED STRING the
  // live `POST /agents/register` accepts on `scope_request` (verified against prod: a JSON ARRAY
  // 422s `scope_unknown` on any element the platform doesn't recognize; the comma-separated string
  // is the accepted shape). Serializing the array was the documented flag's 100%-broken bug (kestrel-wlym).
  const scopes = flags.get("scopes")?.split(",").map((s) => s.trim()).filter((s) => s.length > 0);

  // ── DISCLOSURE (AX doctrine, bead gaz) — say what we send, BEFORE we send it ──
  disclose(ctx, base, withGit, identity);

  const body: Record<string, unknown> = {};
  if (name !== undefined) body.name = name;
  if (withGit && (identity.name !== undefined || identity.email !== undefined)) {
    body.claimed_git = {
      ...(identity.name !== undefined ? { name: identity.name } : {}),
      ...(identity.email !== undefined ? { email: identity.email } : {}),
    };
  }
  if (scopes !== undefined && scopes.length > 0) body.scope_request = scopes.join(",");

  const res = await fetchImpl(`${base}/agents/register`, {
    method: "POST",
    headers: { "content-type": "application/json", accept: "application/json" },
    body: JSON.stringify(body),
  });

  if (res.status === 403) {
    const detail = await problemDetail(res);
    throw usageErr(`registration refused: ${detail}`);
  }
  if (res.status === 429) {
    throw new CliError({
      code: "HTTP_429",
      exit: EXIT.GENERIC,
      message: "registration rate-limited — retry later or reuse the credential you already hold",
    });
  }
  if (!res.ok) {
    // Surface the server's problem+json `detail`/`title` verbatim (kestrel-wlym): the old opaque
    // `registration failed (HTTP 422)` hid the one line that says HOW to fix it (e.g. an unknown
    // scope literal + "Remove it and retry"). A 422 is a client-shaped request the caller can act on.
    const detail = await problemDetail(res, `registration failed (HTTP ${res.status})`);
    throw new CliError({
      code: `HTTP_${res.status}`,
      exit: res.status >= 500 ? EXIT.RUNTIME_UNAVAILABLE : EXIT.GENERIC,
      message: `registration failed (HTTP ${res.status}): ${detail}`,
      ...(res.status === 422
        ? { hint: "--scopes is a COMMA-SEPARATED scope request (e.g. --scopes data,sim); an unknown scope literal is rejected — remove it and retry" }
        : {}),
    });
  }

  const reg = (await res.json()) as RegistrationResponse;
  const credPath = deps.credentialsPath ?? defaultCredentialsPath(env);

  // ── optional keypair enrollment (--keypair, kestrel-markets-0t0 slice 2) ──
  // Generate an Ed25519 keypair, enroll the PUBLIC half (authenticated by the durable
  // capability we just received), and store the PRIVATE half 0600. Thereafter the CLI
  // authenticates by minting short-lived JWTs signed with the private key — a possession
  // proof distinct from replaying the bearer. The kcap path stays fully working regardless.
  const wantKeypair = bools.has("keypair");
  let enrolled: { keyPath: string; thumbprint: string } | undefined;
  if (wantKeypair) {
    enrolled = await enrollKeypair(ctx, fetchImpl, base, reg, deps, env);
  }

  const credential: StoredCredential = {
    version: 1,
    api: base,
    agent_id: reg.agent_id,
    capability: reg.capability,
    token_type: reg.token_type,
    scopes: reg.scopes,
    expiry: reg.expiry,
    ...(reg.subject !== undefined ? { subject: reg.subject } : {}),
    ...(enrolled !== undefined ? { keypair_enrolled: true } : {}),
    ...(withGit && (identity.name !== undefined || identity.email !== undefined)
      ? { claimed: { ...(identity.name !== undefined ? { name: identity.name } : {}), ...(identity.email !== undefined ? { email: identity.email } : {}) } }
      : {}),
  };
  saveCredential(credential, credPath);

  emitResult(ctx, reg, credPath, enrolled);
  return 0;
}

/**
 * Generate + enroll an Ed25519 keypair for the freshly-registered agent. Returns the
 * keyfile path + thumbprint on success, or `undefined` (with a stderr note) when the
 * platform did not return a `subject` (older platform) or the enroll leg refused — the
 * registration itself still succeeded, so this is a soft degrade, never a hard failure.
 */
async function enrollKeypair(
  ctx: OutputCtx,
  fetchImpl: FetchLike,
  base: string,
  reg: RegistrationResponse,
  deps: RegisterDeps,
  env: Record<string, string | undefined>,
): Promise<{ keyPath: string; thumbprint: string } | undefined> {
  if (reg.subject === undefined) {
    process.stderr.write("disclose: --keypair skipped — the platform did not return a subject to bind the key to\n");
    return undefined;
  }
  const { publicJwk, privateJwk } = await generateAgentKeypair();
  process.stderr.write("disclose: generated a local Ed25519 keypair; enrolling the PUBLIC half (the private key never leaves this machine)\n");

  const enrollRes = await fetchImpl(`${base}/agents/enroll-key`, {
    method: "POST",
    headers: { "content-type": "application/json", accept: "application/json", authorization: `Bearer ${reg.capability}` },
    body: JSON.stringify({ public_key: publicJwk }),
  });
  if (!enrollRes.ok) {
    const detail = await problemDetail(enrollRes);
    process.stderr.write(`disclose: --keypair enrollment failed (HTTP ${enrollRes.status}: ${detail}); the durable capability still works\n`);
    return undefined;
  }
  const body = (await enrollRes.json()) as EnrollmentResponse;
  const keyPath = deps.keypairPath ?? defaultKeypairPath(env);
  const keypair: StoredKeypair = {
    version: 1,
    api: base,
    subject: reg.subject,
    public_jwk: publicJwk,
    private_jwk: privateJwk,
    audience: body.jwt.audience,
    max_ttl_seconds: body.jwt.max_ttl_seconds,
    enrolled_at: new Date().toISOString(),
  };
  saveKeypair(keypair, keyPath);
  return { keyPath, thumbprint: body.public_key_thumbprint };
}

/** Disclose (to stderr) exactly what will be sent + the unverified caveat + opt-out. */
function disclose(ctx: OutputCtx, base: string, withGit: boolean, identity: GitIdentity): void {
  if (ctx.mode === "json") {
    // In json mode the disclosure rides in the machine record too (see emitResult); still
    // echo a one-line stderr note so a human watching the stream sees it.
    process.stderr.write(
      JSON.stringify({
        disclosure: {
          endpoint: `${base}/agents/register`,
          sends_claimed_git_identity: withGit,
          claimed: withGit ? identity : null,
          note: "claimed git identity is UNVERIFIED (freely spoofable); pass --no-git-identity to omit it",
        },
      }) + "\n",
    );
    return;
  }
  const lines: string[] = [];
  lines.push(`registering with ${base}/agents/register`);
  if (withGit && (identity.name !== undefined || identity.email !== undefined)) {
    lines.push(`will send CLAIMED git identity (UNVERIFIED): ${identity.name ?? "(no name)"} <${identity.email ?? "no-email"}>`);
    lines.push("this identity is freely spoofable and is NOT trusted as authentication; pass --no-git-identity to omit it");
  } else if (withGit) {
    lines.push("no git user.name/user.email found — sending no claimed identity");
  } else {
    lines.push("--no-git-identity: sending NO claimed git identity");
  }
  for (const l of lines) process.stderr.write(`disclose: ${l}\n`);
}

/** Print the success summary to stdout (the payload channel). */
function emitResult(
  ctx: OutputCtx,
  reg: RegistrationResponse,
  credPath: string,
  enrolled?: { keyPath: string; thumbprint: string },
): void {
  if (ctx.mode === "json") {
    process.stdout.write(
      JSON.stringify({
        schema: "kestrel.register/v1",
        agent_id: reg.agent_id,
        token_type: reg.token_type,
        scopes: reg.scopes,
        expiry: reg.expiry,
        credential_path: credPath,
        keypair: enrolled ? { enrolled: true, keyfile_path: enrolled.keyPath, thumbprint: enrolled.thumbprint } : null,
        upgrade: reg.upgrade ?? null,
      }) + "\n",
    );
    return;
  }
  if (ctx.mode === "text") {
    const kp = enrolled ? `\tkeyfile=${enrolled.keyPath}` : "";
    process.stdout.write(`registered\tagent_id=${reg.agent_id}\tscopes=${reg.scopes.join(",")}\texpiry=${reg.expiry}\tcredential=${credPath}${kp}\n`);
    return;
  }
  process.stdout.write(`registered as ${reg.agent_id}\n`);
  process.stdout.write(`  scopes:     ${reg.scopes.join(", ")}\n`);
  process.stdout.write(`  expiry:     ${reg.expiry}\n`);
  process.stdout.write(`  credential: ${credPath}\n`);
  if (enrolled) {
    process.stdout.write(`  keyfile:    ${enrolled.keyPath} (0600 — Ed25519 private key; JWT auth)\n`);
    process.stdout.write(`  key id:     ${enrolled.thumbprint}\n`);
  }
  if (reg.upgrade?.verify_email_hint) process.stdout.write(`  upgrade:    ${reg.upgrade.verify_email_hint}\n`);
}

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