/**
 * # cli/commands/certify — `certify <proof-url>` (LIGHT, node-runnable): OPEN RECOMPUTATION (G10, kestrel-8kvs).
 *
 * "Don't trust us — run it." From the PUBLISHED npm package, `kestrel certify <proof-url>` fetches the
 * proof's OPEN-RECOMPUTATION evidence bundle (`GET /proof/{id}/evidence`), RE-PROJECTS the Blotter
 * LOCALLY with the same shipped projector the platform ran, and asserts the local projection reproduces
 * the hosted PUBLISHED Blotter **byte-identically** — the determinism leg of platform launch gate G10
 * (the L1 minimum, clearly labeled; judged-Grade recomputation follows by L3). Without this, "certified"
 * in landing copy is a G6 claims violation.
 *
 * The check is `serialize(project([...readBusText(bundle.bus)])) === bundle.blotter`, plus a local
 * pure-projector self-leg (project the same bus twice ⇒ byte-identical, ADR-0011). Two byte-identity
 * claims, both reported:
 *   - `determinism_self` — the OSS projector is a pure function of the bus (project twice, same bytes);
 *   - `reproduced`       — the OSS re-projection equals the platform's PUBLISHED Blotter, byte-for-byte.
 *
 * Verdicts (DATA on stdout): `REPRODUCED` (both hold — exit 0) · `MISMATCH` (the local re-projection
 * differs from the published Blotter — genuine divergence, exit 1) · `NON_DETERMINISTIC` (the local
 * projector is not pure — fail-closed, exit 1) · `NOT_FOUND` (no re-projectable free-tier bundle for
 * this id — exit 3). Fail-closed everywhere: a bundle whose bus is not a graded bus (`project` refuses
 * it) is a loud typed refusal, never a fabricated verdict.
 *
 * BOUNDARY: `certify` re-projects the PUBLISHED Bus with OSS code only — it imports no platform business
 * logic and holds no key. It is the OSS half of G10 (SEAM edge); the platform half (kestrel-markets-tk82)
 * serves the identical `kestrel.evidence-bundle/v1` shape. LIGHT: `project`/`serialize`/`readBusText` and
 * `node:crypto`-free `sha256` — no bun/chdb, no engine drive, so it runs under bun-less node via npx.
 */

import { project, serialize, type Blotter } from "../../blotter/index.ts";
import { toProtocolBlotter } from "../../blotter/protocol-view.ts";
import { readBusText } from "../../bus/index.ts";
import { sha256 } from "../../crypto/sha256.ts";
// The numbers↔tape / numbers↔root binding legs (OSS-ADR-0051) recompute from the SAME dependency-free
// protocol Judge + pure root derivation an outsider runs — no platform code, keeping `certify` LIGHT.
import { grade, type GradeResult } from "../../protocol/index.ts";
import type { SessionId } from "../../protocol/session.ts";
import { certifiedRoot, ROOT_HASH_PREFIX } from "../../protocol/attestation.ts";
import { HostedFunnel, type PublishedEvidenceBundle } 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";
import { parseProofRef } from "./verify.ts";

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

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

/** The four honest verdicts an open-recomputation check resolves to. */
export type CertifyVerdict = "REPRODUCED" | "MISMATCH" | "NON_DETERMINISTIC" | "NOT_FOUND";

/** Verdict → CLI exit: REPRODUCED passes; every other outcome is a nonzero, machine-legible code. */
const VERDICT_EXIT: Readonly<Record<CertifyVerdict, number>> = {
  REPRODUCED: 0,
  MISMATCH: EXIT.GENERIC, // the local re-projection differs from the published Blotter — genuine divergence
  NON_DETERMINISTIC: EXIT.GENERIC, // the local projector is not pure — fail-closed (ADR-0011)
  NOT_FOUND: EXIT.NOT_FOUND, // no re-projectable free-tier evidence bundle for this id
};

/** `sha256:`-prefixed content hash of a UTF-8 string (the legible, byte-identity-equivalent digest). */
function contentHash(text: string): string {
  return `sha256:${sha256(text)}`;
}

/**
 * The PURE open-recomputation core: re-project the published Bus LOCALLY and compare to the published
 * Blotter, byte-for-byte. Deterministic — no wall clock, no RNG. `project` fail-closes (throws) on a
 * bus that is not a GRADED bus (the two-bus rule, ADR-0011); the caller turns that into a loud refusal
 * rather than a verdict. Returns both byte-identity legs so the verdict never over-claims.
 */
export interface ReprojectionResult {
  readonly verdict: Exclude<CertifyVerdict, "NOT_FOUND">;
  readonly determinismSelf: "pass" | "fail";
  readonly reproduced: boolean;
  readonly busHash: string;
  readonly publishedBlotterHash: string;
  readonly localBlotterHash: string;
  /** The re-projected engine Blotter object (the pure `project(bus)` result) — carried so the numbers↔tape
   * binding leg can grade it (`grade(reproject(bus))`, OSS-ADR-0051) without re-projecting a second time. */
  readonly localBlotter: Blotter;
}

export function reproject(bundle: PublishedEvidenceBundle): ReprojectionResult {
  const events = [...readBusText(bundle.bus)];
  // The local projection, computed TWICE — the pure-projector self-leg (ADR-0011: same bus ⇒
  // byte-identical Blotter). `project` refuses a non-graded bus loudly; that throw propagates.
  const localBlotter = project(events);
  const local = serialize(localBlotter);
  const localAgain = serialize(project(events));
  const determinismSelf: "pass" | "fail" = local === localAgain ? "pass" : "fail";
  // The open-recomputation claim: the local re-projection reproduces the PUBLISHED Blotter byte-for-byte.
  const reproduced = local === bundle.blotter;
  const verdict: Exclude<CertifyVerdict, "NOT_FOUND"> =
    determinismSelf === "fail" ? "NON_DETERMINISTIC" : reproduced ? "REPRODUCED" : "MISMATCH";
  return {
    verdict,
    determinismSelf,
    reproduced,
    busHash: contentHash(bundle.bus),
    publishedBlotterHash: contentHash(bundle.blotter),
    localBlotterHash: contentHash(local),
    localBlotter,
  };
}

/** A binding-leg outcome (OSS-ADR-0051): `yes` — the recomputation matches the published value; `no` —
 * it does NOT (a tampered published number); `unknown` — the bundle carried nothing to bind (the leg is
 * additive-optional and is skipped). */
export type BindLeg = "yes" | "no" | "unknown";

/** Do two metric maps agree exactly? Both come off the Judge's shared 1e-8 grid, so equality is exact
 * (not tolerance-based): same keys, same finite values. */
function sameMetrics(a: Readonly<Record<string, number>>, b: Readonly<Record<string, number>>): boolean {
  const ak = Object.keys(a);
  const bk = Object.keys(b);
  if (ak.length !== bk.length) return false;
  for (const k of ak) if (!Object.is(a[k], b[k])) return false;
  return true;
}

/**
 * The numbers↔tape binding (OSS-ADR-0051): grade the re-projected tape with the SAME dependency-free OSS
 * Judge and bind the result to the bundle's PUBLISHED numbers. `unknown` when the bundle publishes none.
 * Pure — no wall clock, no RNG. Grades the already-projected engine Blotter through the light
 * `toProtocolBlotter` view, so the numbers an outsider re-derives from the tape must equal the published ones.
 */
export function numbersBindTapeOf(result: ReprojectionResult, bundle: PublishedEvidenceBundle): BindLeg {
  if (bundle.publishedGrade === undefined) return "unknown";
  const local: GradeResult = grade([toProtocolBlotter(bundle.publishedGrade.subjectSessionId as SessionId, result.localBlotter)]);
  return sameMetrics(local.metrics, bundle.publishedGrade.metrics) ? "yes" : "no";
}

/**
 * The numbers↔root binding (OSS-ADR-0051): recompute `certifiedRoot(coveredFields)` and bind it to the
 * bundle's published `root` (stripping the `sha256:` display prefix). `unknown` when the bundle carries no
 * covered fields or no root. A tampered published number breaks this recompute (→ `no`). Fail-safe: a
 * derivation error resolves to `unknown` (inability to adjudicate), never a fabricated `yes`.
 */
export async function rootBindOf(bundle: PublishedEvidenceBundle): Promise<BindLeg> {
  if (bundle.coveredFields === undefined || bundle.root === undefined) return "unknown";
  try {
    const bare = bundle.root.startsWith(ROOT_HASH_PREFIX) ? bundle.root.slice(ROOT_HASH_PREFIX.length) : bundle.root;
    const recomputed = await certifiedRoot(bundle.coveredFields);
    return recomputed === bare ? "yes" : "no";
  } catch {
    return "unknown";
  }
}

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

/** The structured report `certify` emits (the `--json` twin + the source of the text line). */
export interface CertifyReport {
  readonly verdict: CertifyVerdict;
  readonly proof_id: string;
  /** The gate + leg this check settles — the L1 minimum, honestly labeled. */
  readonly leg: "determinism";
  readonly determinism_self?: "pass" | "fail";
  readonly reproduced?: boolean;
  readonly bus_sha256?: string;
  readonly published_blotter_sha256?: string;
  readonly local_blotter_sha256?: string;
  readonly engine_version?: string;
  readonly fill_model?: string;
  /** The numbers↔tape binding leg (OSS-ADR-0051): does `grade(reproject(bus))` reproduce the PUBLISHED
   * numbers? `no` (a tampered published number) forces MISMATCH; `unknown` when the bundle published none. */
  readonly numbers_bind_tape?: BindLeg;
  /** The numbers↔root binding leg (OSS-ADR-0051): does `certifiedRoot(coveredFields)` equal the published
   * root? `no` (a tampered published number) forces MISMATCH; `unknown` when the bundle carried no root. */
  readonly root_bind?: BindLeg;
}

function emit(ctx: OutputCtx, report: CertifyReport): void {
  if (ctx.mode === "json") {
    process.stdout.write(JSON.stringify({ schema: "kestrel.certify/v1", ...report }) + "\n");
    return;
  }
  const parts = [report.verdict, `proof=${report.proof_id}`, `leg=${report.leg}(L1)`];
  if (report.reproduced !== undefined) parts.push(`reproduced=${report.reproduced}`);
  if (report.published_blotter_sha256 !== undefined) parts.push(`published_blotter=${report.published_blotter_sha256}`);
  if (report.local_blotter_sha256 !== undefined) parts.push(`local_blotter=${report.local_blotter_sha256}`);
  if (report.verdict === "MISMATCH" && report.numbers_bind_tape === "no")
    parts.push("note=published-numbers-differ-from-grade-of-the-reprojected-tape");
  else if (report.verdict === "MISMATCH" && report.root_bind === "no")
    parts.push("note=published-numbers-do-not-rebuild-the-signed-root");
  else if (report.verdict === "MISMATCH") parts.push("note=local-reprojection-differs-from-the-published-blotter");
  if (report.verdict === "NON_DETERMINISTIC") parts.push("note=the-local-projector-is-not-a-pure-function-of-the-bus");
  if (report.numbers_bind_tape !== undefined) parts.push(`numbers-bind-tape=${report.numbers_bind_tape}`);
  if (report.root_bind !== undefined) parts.push(`root-bind=${report.root_bind}`);
  process.stdout.write(parts.join("\t") + "\n");
}

/**
 * The open-recomputation path: fetch the evidence bundle, re-project the Bus LOCALLY, byte-compare to
 * the published Blotter, emit the verdict as data, and return the verdict's exit code. A bundle whose
 * Bus is not a graded bus (`project` refuses) is a loud typed refusal (never a fabricated verdict).
 */
export async function runCertify(
  client: HostedFunnel,
  id: string,
  ctx: OutputCtx,
  forward: { readonly copyToken?: string },
): Promise<number> {
  const bundle = await client.evidenceBundle(id, forward);
  if (bundle === null) {
    emit(ctx, { verdict: "NOT_FOUND", proof_id: id, leg: "determinism" });
    return VERDICT_EXIT.NOT_FOUND;
  }

  let result: ReprojectionResult;
  try {
    result = reproject(bundle);
  } catch (e) {
    // The Bus is not a re-projectable GRADED bus (the two-bus rule / a malformed tape): the projector
    // refused. Refuse LOUDLY rather than emit a bogus MISMATCH — the bundle itself is uncertifiable.
    throw new CliError({
      code: "GENERIC",
      exit: EXIT.GENERIC,
      message: `certify could not re-project the evidence bus for proof ${id}: ${(e as Error).message}`,
      hint: "the published evidence bundle's bus is not a graded bus the OSS projector can re-project (ADR-0011)",
    });
  }

  // The numbers-binding legs (OSS-ADR-0051), computed only when the reprojection itself held (a bundle
  // that already FAILED determinism / byte-reproduction is uncertifiable — do not over-claim a binding on
  // top of it). A `no` on either leg is a tampered published number: it forces MISMATCH even though the
  // Blotter bytes reproduced. Both are additive-optional (UNKNOWN when the bundle publishes nothing to bind).
  const numbersBindTape: BindLeg = result.verdict === "REPRODUCED" ? numbersBindTapeOf(result, bundle) : "unknown";
  const rootBind: BindLeg = result.verdict === "REPRODUCED" ? await rootBindOf(bundle) : "unknown";
  const bindingFailed = numbersBindTape === "no" || rootBind === "no";
  const verdict: CertifyVerdict = result.verdict === "REPRODUCED" && bindingFailed ? "MISMATCH" : result.verdict;

  emit(ctx, {
    verdict,
    proof_id: id,
    leg: "determinism",
    determinism_self: result.determinismSelf,
    reproduced: result.reproduced,
    bus_sha256: result.busHash,
    published_blotter_sha256: result.publishedBlotterHash,
    local_blotter_sha256: result.localBlotterHash,
    ...(bundle.engine_version !== undefined ? { engine_version: bundle.engine_version } : {}),
    ...(bundle.fill_model !== undefined ? { fill_model: bundle.fill_model } : {}),
    numbers_bind_tape: numbersBindTape,
    root_bind: rootBind,
  });
  return VERDICT_EXIT[verdict];
}

/**
 * `certify` — open recomputation of a PUBLISHED proof (gate G10). Fetches the evidence bundle, re-projects
 * the Blotter locally, and asserts byte-identical reproduction of the hosted result. The verdict is DATA
 * on stdout (REPRODUCED/MISMATCH/NON_DETERMINISTIC/NOT_FOUND); the exit code reflects it (0 only for
 * REPRODUCED). Accepts a full proof URL (`https://kestrel.markets/proof/<id>`) or a bare id.
 */
export async function certifyCommand(
  argv: readonly string[],
  ctx: OutputCtx,
  globals: GlobalFlags,
  deps: CertifyDeps = {},
): 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: "certify needs a proof URL or id",
      hint: "usage: kestrel certify <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 runCertify(client, id, ctx, forward);
}
