/**
 * # cli/commands/verify — `verify <url>` (LIGHT, node-runnable): the zero-trust proof check (n04e.1).
 *
 * A PURE, zero-trust signature check of a PUBLISHED proof. It accepts a full proof URL
 * (`https://kestrel.markets/proof/<id>`) or a bare id, then:
 *
 *   1. `GET {api}/proof/<id>`                → the proof's signature envelope (root + signature + kid);
 *   2. `GET {api}/.well-known/kestrel-markets` → the PUBLISHED verify-set, fetched INDEPENDENTLY;
 *   3. re-verifies the Ed25519 signature over `kgrade1.<bare-hex-root>` LOCALLY with node's
 *      crypto — it does NOT trust the proof body's own `verification.verified` self-assertion.
 *
 * WIRE-ROOT PREFIX (bead kestrel-markets-ha87): the proof's `root` is served with a `sha256:`
 * DISPLAY prefix (platform bead 7ea, so it equals the Grade artifact's `content_hash`), but the
 * platform signs the BARE lowercase-hex root — `kgrade1.<hex>`, never `kgrade1.sha256:<hex>`. The
 * verifier MUST strip the prefix before rebuilding the signed message; not doing so falsely
 * reported UNVERIFIED on every genuine prod proof (incl. fresh mints) — the flagship evidence
 * loop failing at the exact step a skeptical agent runs.
 *
 * Verdicts: `VERIFIED` (a held key verifies the signature) · `UNVERIFIED` (a held key does
 * NOT — genuine tampering/corruption) · `KEY_RETIRED` (the proof's kid is not in the published
 * set — we hold no key; honest, not tampering) · `NOT_FOUND` (no such proof). Mints NO trial —
 * two GETs, no funnel touch.
 *
 * BOUNDARY: `verify` re-checks the CRYPTOGRAPHIC signature against independently-published public
 * keys AND — when the proof carries the signature-covered grade fields — binds the published NUMBERS
 * to the signed root by recomputing `certifiedRoot(coveredFields)` locally (OSS-ADR-0051). The
 * grade-root derivation is now PUBLIC dependency-free protocol (`protocol/attestation.ts`), so
 * reproducing the root no longer drags any control-plane grade schema into OSS — this supersedes the
 * prior boundary note that `verify` "deliberately does NOT reproduce the canonical grade-root
 * derivation." The recompute is ADDITIVE-OPTIONAL: a proof without the covered fields leaves the leg
 * UNKNOWN and the signature-over-root check stands alone (never a silent false). LIGHT: node built-ins
 * (`node:crypto`) + WebCrypto-via-protocol only, no bun/chdb, no platform code.
 */

import { createPublicKey, verify as ed25519Verify } from "node:crypto";

// The signed-root WIRE constants + the pure `certifiedRoot` derivation live in the dependency-free
// protocol package (OSS-ADR-0046/0051), the ONE home both this LIGHT verifier and the platform signer
// import so the two can never drift. PLAIN consts + a pure function — importing them keeps verify's
// import graph node-only (no engine/frame; `certifiedRoot` hashes via WebCrypto globals).
import { GRADE_SIGN_PREFIX, ROOT_HASH_PREFIX, certifiedRoot } from "../../protocol/attestation.ts";
import { HostedFunnel, type PublishedProof, type PublishedVerifyKey } from "../backend/hosted.ts";
import type { OutputCtx, GlobalFlags } from "../context.ts";
import { parseArgs } from "../args.ts";
import { CliError, EXIT } from "../errors.ts";
import { resolveSimBase, resolveCopyToken } from "./sim.ts";

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

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

/** The four honest verdicts a zero-trust check resolves to. */
export type VerifyVerdict = "VERIFIED" | "UNVERIFIED" | "KEY_RETIRED" | "NOT_FOUND";

/** Verdict → CLI exit: VERIFIED passes; every other outcome is a nonzero, machine-legible code. */
const VERDICT_EXIT: Readonly<Record<VerifyVerdict, number>> = {
  VERIFIED: 0,
  UNVERIFIED: EXIT.GENERIC, // a held key does NOT verify — genuine tampering/corruption
  KEY_RETIRED: EXIT.GENERIC, // we hold no key for this kid — honest "cannot re-verify"
  NOT_FOUND: EXIT.NOT_FOUND,
};

/**
 * Extract the bare proof id from a full/partial proof URL or a `/proof/<id>` path; a bare id
 * passes through. A URL that is NOT a proof URL is a loud USAGE error (never silently treated
 * as an id). Pure — no network.
 */
export function parseProofRef(input: string): string {
  const trimmed = input.trim();
  if (trimmed === "")
    throw new CliError({ code: "USAGE", exit: EXIT.USAGE, message: "verify needs a proof URL or id" });
  const m = /\/proof\/([^/?#]+)/.exec(trimmed);
  if (m !== null) return decodeURIComponent(m[1]!);
  if (/^https?:\/\//i.test(trimmed))
    throw new CliError({
      code: "USAGE",
      exit: EXIT.USAGE,
      message: `not a proof URL: ${JSON.stringify(trimmed)}`,
      hint: "pass a proof URL (https://kestrel.markets/proof/<id>) or a bare proof id",
    });
  return trimmed;
}

/**
 * The PURE zero-trust check: does a held key verify the proof's Ed25519 signature over
 * `kgrade1.<root>`? Selects the key by the proof's OWN kid (trusting active + accept-only). A
 * kid absent from the published set is `KEY_RETIRED` (honest); a held key that fails the
 * signature is `UNVERIFIED` (tampering) — never conflated. Fail-closed: any crypto/decoding
 * error resolves AWAY from VERIFIED.
 */
export function verifyProofSignature(proof: PublishedProof, keys: readonly PublishedVerifyKey[]): VerifyVerdict {
  const key = keys.find((k) => k.kid === proof.kid);
  if (key === undefined) return "KEY_RETIRED";
  try {
    const publicKey = createPublicKey({ key: { ...key.public_jwk }, format: "jwk" });
    const signature = Buffer.from(proof.signature, "base64url");
    // The wire `root` carries a `sha256:` DISPLAY prefix (platform bead 7ea); the Ed25519
    // signature covers the BARE hex root (`kgrade1.<hex>`). Strip the prefix before rebuilding
    // the signed message, or every genuine prod proof falsely reports UNVERIFIED (bead ha87).
    const bareRoot = proof.root.startsWith(ROOT_HASH_PREFIX) ? proof.root.slice(ROOT_HASH_PREFIX.length) : proof.root;
    const message = Buffer.from(`${GRADE_SIGN_PREFIX}.${bareRoot}`);
    const ok = ed25519Verify(null, message, publicKey, signature);
    return ok ? "VERIFIED" : "UNVERIFIED";
  } catch {
    return "UNVERIFIED";
  }
}

/**
 * The numbers↔root binding leg (OSS-ADR-0051): does the proof's PUBLISHED covered-field set rebuild
 * the signed root? `"yes"` — `certifiedRoot(coveredFields)` equals the bare signed root, so the
 * published numbers ARE what was signed. `"no"` — they do NOT rebuild it: a tampered published number
 * whose old signature still validates over the old root (the attack this leg closes). `"unknown"` — the
 * proof carries no covered-field set (additive-optional / absent = UNKNOWN), so the leg is not run.
 */
export type NumbersBindRoot = "yes" | "no" | "unknown";

/**
 * Recompute the numbers↔root binding of a published proof (pure): strip the `sha256:` display prefix
 * from the wire `root` and compare it to `certifiedRoot(coveredFields)`. Returns `"unknown"` when the
 * proof carries no covered fields (the leg is additive). Fail-safe: a derivation error (e.g. WebCrypto
 * unavailable) resolves to `"unknown"` — an inability to adjudicate the binding, never a fabricated
 * `"yes"` (the independent signature check remains the standing guarantee).
 */
export async function recomputeNumbersBindRoot(proof: PublishedProof): Promise<NumbersBindRoot> {
  if (proof.coveredFields === undefined) return "unknown";
  try {
    const bareRoot = proof.root.startsWith(ROOT_HASH_PREFIX) ? proof.root.slice(ROOT_HASH_PREFIX.length) : proof.root;
    const recomputed = await certifiedRoot(proof.coveredFields);
    return recomputed === bareRoot ? "yes" : "no";
  } catch {
    return "unknown";
  }
}

export interface VerifyDeps {
  readonly fetch?: FetchLike;
  readonly env?: Env;
}

/** The structured verdict `verify` emits (the `--json` twin + the source of the text line). */
export interface VerifyReport {
  readonly verdict: VerifyVerdict;
  readonly proof_id: string;
  readonly kid?: string;
  readonly epoch?: number;
  readonly proof_url?: string;
  /** The proof body's OWN self-asserted verdict — carried to CONTRAST with the independent one. */
  readonly claimed_verified?: boolean;
  /**
   * The numbers↔root binding leg (OSS-ADR-0051): `yes` — the published covered-field numbers rebuild
   * the signed root; `no` — they do NOT (a tampered published number is the cause; the verdict is
   * UNVERIFIED); `unknown` — the proof carried no covered fields, so only the signature was checked.
   */
  readonly numbers_bind_root?: NumbersBindRoot;
  /**
   * WHERE the published verify-set the check ran against lives (bead kestrel-markets-5bjn/fjq7).
   * Named on a non-VERIFIED verdict so a reader can go re-adjudicate the signature against the
   * SAME key material independently, instead of guessing where the key is published.
   */
  readonly verify_key_url?: string;
}

/** Verdicts where the reader needs to know WHERE the key lives to independently re-adjudicate. */
function needsVerifyKeyUrl(verdict: VerifyVerdict): boolean {
  return verdict === "UNVERIFIED" || verdict === "KEY_RETIRED";
}

function emit(ctx: OutputCtx, report: VerifyReport, notice?: string): void {
  if (ctx.mode === "json") {
    process.stdout.write(JSON.stringify({ schema: "kestrel.verify/v1", ...(notice !== undefined ? { notice } : {}), ...report }) + "\n");
    return;
  }
  if (notice !== undefined) process.stdout.write(`note: ${notice}\n`);
  const parts = [report.verdict, `proof=${report.proof_id}`];
  if (report.kid !== undefined) parts.push(`kid=${report.kid}`);
  if (report.epoch !== undefined) parts.push(`epoch=${report.epoch}`);
  if (report.verdict === "UNVERIFIED" && report.numbers_bind_root === "no")
    parts.push("note=published-numbers-do-not-rebuild-the-signed-root");
  else if (report.verdict === "UNVERIFIED" && report.claimed_verified === true)
    parts.push("note=the-proof-claimed-verified-but-its-signature-did-not-re-verify");
  if (report.numbers_bind_root !== undefined) parts.push(`numbers-bind-root=${report.numbers_bind_root}`);
  // Point a skeptical reader AT the published key material so they can re-check independently
  // (bead 5bjn/fjq7): "against the published verify key" is not an instruction until it names WHERE.
  if (needsVerifyKeyUrl(report.verdict) && report.verify_key_url !== undefined)
    parts.push(`verify-key=${report.verify_key_url}`);
  process.stdout.write(parts.join("\t") + "\n");
}

/**
 * The SHARED zero-trust verify path (reused by `verify` and `replay`'s degrade branch): fetch
 * the published proof, fetch the verify-set INDEPENDENTLY, re-check the signature locally, emit
 * the verdict as data, and return the verdict's exit code. `notice` prefixes an honest one-line
 * explanation (replay uses it when it has no local strategy to reproduce).
 */
export async function runVerify(
  client: HostedFunnel,
  id: string,
  ctx: OutputCtx,
  forward: { readonly copyToken?: string },
  notice?: string,
): Promise<number> {
  const proof = await client.publishedProof(id, forward);
  if (proof === null) {
    emit(ctx, { verdict: "NOT_FOUND", proof_id: id }, notice);
    return VERDICT_EXIT.NOT_FOUND;
  }
  // Fetch the verify-set INDEPENDENTLY — the zero-trust input (never the proof's own verdict).
  const keys = await client.verifyKeys(forward);
  const signatureVerdict = verifyProofSignature(proof, keys);
  // The numbers↔root binding leg (OSS-ADR-0051): recompute the signed root from the PUBLISHED covered
  // fields. `"no"` — the numbers do not rebuild the signed root (a tampered published number the
  // signature-over-root check alone cannot catch) — is genuine tampering, so it forces UNVERIFIED even
  // when the signature validates. `"unknown"` (no covered fields published) leaves the signature verdict.
  const numbersBind = await recomputeNumbersBindRoot(proof);
  const verdict: VerifyVerdict = numbersBind === "no" ? "UNVERIFIED" : signatureVerdict;
  emit(
    ctx,
    {
      verdict,
      proof_id: proof.proof_id,
      kid: proof.kid,
      epoch: proof.epoch,
      claimed_verified: proof.verificationClaim,
      numbers_bind_root: numbersBind,
      // Name the published key set on a non-VERIFIED verdict (bead 5bjn/fjq7) — the same
      // `/.well-known/kestrel-markets` the verify-set was fetched from, resolved at the API base.
      ...(needsVerifyKeyUrl(verdict) ? { verify_key_url: client.verifyKeysUrl } : {}),
    },
    notice,
  );
  return VERDICT_EXIT[verdict];
}

/**
 * `verify` — zero-trust re-verification of a published proof. The verdict is DATA on stdout
 * (VERIFIED/UNVERIFIED/KEY_RETIRED/NOT_FOUND) and the exit code reflects it (0 only for
 * VERIFIED). Never trusts the proof body's own `verification.verified`.
 */
export async function verifyCommand(
  argv: readonly string[],
  ctx: OutputCtx,
  globals: GlobalFlags,
  deps: VerifyDeps = {},
): Promise<number> {
  const { positionals, flags } = parseArgs(argv, new Set(), new Set(["copy-token"]));
  if (positionals.length === 0)
    throw new CliError({
      code: "USAGE",
      exit: EXIT.USAGE,
      message: "verify needs a proof URL or id",
      hint: "usage: kestrel verify <https://kestrel.markets/proof/<id> | <id>>",
    });
  const env = deps.env ?? (process.env as Env);
  const id = parseProofRef(positionals[0]!);
  const copyToken = resolveCopyToken(flags, env);
  const forward = copyToken !== undefined ? { copyToken } : {};

  const base = resolveSimBase(globals, env);
  const client = new HostedFunnel({ baseUrl: base, ...(deps.fetch !== undefined ? { fetch: deps.fetch } : {}) });
  return runVerify(client, id, ctx, forward);
}
