/**
 * # adapters/broker/authority — the PRODUCTION human-signature authority + the arm-minting
 * ceremony (kestrel-7o2, Wave A / A2).
 *
 * `armLive` (`src/adapters/broker.ts`) is the SOLE mint of a {@link LiveArm}, but it is an
 * open/closed seam: it delegates the proof to an injected {@link AuthoritySigner} and, until this
 * module, the only signer that existed was a mock in the test suite. This module supplies the
 * PRODUCTION half — the canonical serialization, the signing input, the Ed25519 verifier, and the
 * ceremony that composes them into `armLive`.
 *
 * ## The central invariant
 *
 * **A config flag can never arm live.** Not an env var, not a JSON field, not a CLI switch. The
 * protocol already says so structurally — `live` is excluded from `WALLET_SIGNABLE_SCOPES`
 * (`src/protocol/index.ts`), so no wallet/config authority reaches it; ADR-0034 §3 makes the
 * human-signed Kestrel arm the load-bearing gate, with the broker account only the backstop. This
 * module is where that becomes executable: the ONLY input that mints an arm is a genuine Ed25519
 * signature, by a `"human"`-class key, over the CANONICAL bytes of the exact {@link RiskLimits}
 * being armed.
 *
 * ## The thing signed IS the thing enforced
 *
 * The signature covers `klive1.<sha256(canonical(limits))>` and NOTHING else. Crucially, the
 * ceremony does not carry the caller's `limits` OBJECT forward: {@link assertSignableLimits} returns
 * a FROZEN, plain-number SNAPSHOT (each ceiling read exactly once, at signing time), and it is THAT
 * snapshot that flows into `grant.limits`, the minted token, and `makeLiveClamp`'s stored limits. So
 * verify, mint, and enforce all read the SAME immutable bytes. There is no gap between ceremony and
 * enforcement in which a limit set could be substituted or widened: widen any ceiling by a cent and
 * the digest changes and the signature stops verifying; mutate the caller's object AFTER minting
 * (`limits.maxOrderQty = 500`), or hand in a divergent getter, and enforcement is unaffected — it
 * reads the frozen snapshot, never the caller's object again.
 *
 * ## Reuse, not a parallel abstraction
 *
 * - the digest is `src/crypto/sha256.ts` (the repo's one portable, deterministic hash);
 * - the key shape is `Ed25519PublicJwk` from `src/cli/keypair.ts` (the repo's existing Ed25519 face);
 * - the signing-input convention (`<prefix>.<hex-digest>`, verified with node's `crypto.verify`
 *   against a published JWK) mirrors `src/cli/commands/verify.ts`'s `kgrade1.<root>` byte-for-byte
 *   in shape — a different domain prefix, the same discipline;
 * - the authority class is the protocol's own {@link SignerClass}, not a new vocabulary.
 *
 * ## Determinism
 *
 * Canonicalization, the digest, and Ed25519 verification are pure: no wall clock, no RNG on the
 * verify path. (The offline {@link signLiveArmGrant} half consumes the human's private key; it is
 * ceremony tooling, not a runtime path.)
 *
 * ## Wave B contract
 *
 * The live driver calls {@link performLiveArmCeremony} and gets a {@link LiveArm} or a typed
 * {@link LiveGateRefused}. It must NOT hand-roll an {@link AuthoritySigner}, and it must not accept
 * an arm from anywhere else.
 */

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

import { sha256 } from "../../crypto/sha256.ts";
import type { Ed25519PrivateJwk, Ed25519PublicJwk } from "../../cli/keypair.ts";
import type { SignerClass } from "../../protocol/index.ts";
import { armLive, LiveGateRefused, type AuthoritySigner, type LiveArm, type LiveAuthorityGrant, type RiskLimits } from "../broker.ts";

// ─────────────────────────────────────────────────────────────────────────────
// 1. The canonical serialization — the thing signed
// ─────────────────────────────────────────────────────────────────────────────

/**
 * The live-arm signing-input prefix (`klive1`) — domain separation, so a signature minted for any
 * OTHER Kestrel purpose (a grade proof's `kgrade1.<root>`, a request JWT) can never be replayed as
 * live authority. An OPAQUE wire constant: the human's signing tool and this verifier must agree on
 * it byte-for-byte. Versioned (`1`) so the canonical form can be revised without ambiguity.
 */
export const LIVE_ARM_SIGN_PREFIX = "klive1";

/** The canonical form's own version tag, inside the signed bytes — a v1 signature can never be
 * replayed as a v2 grant even if the ceilings are identical. */
const CANONICAL_LIMITS_VERSION = "klive.limits.v1";

/**
 * The three {@link RiskLimits} ceilings in a FIXED order — hard-coded, never `Object.keys`, so the
 * canonical bytes cannot vary with property insertion order or with an extra field a caller bolted
 * on. Ordering is part of the contract.
 */
const CANONICAL_LIMIT_FIELDS = ["maxNotionalUsd", "maxOrderQty", "maxPositionQty"] as const satisfies readonly (keyof RiskLimits)[];

/**
 * FAIL-CLOSED validation of the {@link RiskLimits} a human is asked to sign / a grant presents.
 * Every ceiling must be a FINITE, non-negative number. Refuses (typed {@link LiveGateRefused}):
 *
 *  - **`Infinity` / unbounded** — unbounded live authority is the fail-open this whole epic exists
 *    to prevent (`UNBOUNDED_PAPER_LIMITS` is a PAPER convenience and must never be signable), and
 *    it is also a serialization trap: `JSON.stringify(Infinity)` is `"null"`, so a naive canonical
 *    form would make an unbounded ceiling indistinguishable from an absent one.
 *  - **`NaN`** — `NaN > limit` is `false`, so a NaN ceiling silently admits every order: a
 *    fail-open that looks like a limit.
 *  - **negative** — an incoherent ceiling; refuse rather than guess.
 *  - **non-number** (a string from a parsed JSON config) — never coerce; a silent coercion is a
 *    silent default.
 *
 * Returns a FROZEN, plain-number SNAPSHOT — NOT the caller's object. Each ceiling is read EXACTLY
 * ONCE (here, at signing time) and stored as a primitive, then the snapshot is `Object.freeze`d.
 * This is the substitution wall the module claims: threading THIS return value (not the caller's
 * object) through the grant, the minted token, and the clamp means verify/mint/enforce all read the
 * SAME immutable bytes, so neither a post-sign mutation of the caller's object (`limits.maxOrderQty
 * = 500` after minting) nor a divergent getter (returns 5 at verify, 500 at enforce) can reach
 * enforcement — the snapshot already captured the single, validated read.
 *
 * Exported so the ceremony can refuse EARLY with a legible reason, and so a caller can pre-check
 * limits before asking a human to sign them.
 */
export function assertSignableLimits(limits: RiskLimits): RiskLimits {
  if (limits === null || typeof limits !== "object") {
    throw new LiveGateRefused(
      `live authority requires a RiskLimits object to sign over (got ${JSON.stringify(limits)}) — fail-closed`,
    );
  }
  const snapshot = {} as { -readonly [K in keyof RiskLimits]: number };
  for (const field of CANONICAL_LIMIT_FIELDS) {
    // Read the ceiling EXACTLY ONCE — a divergent getter cannot return one value here and another at
    // enforcement, because the enforcement path reads the frozen snapshot, never this object again.
    const v: unknown = limits[field];
    if (typeof v !== "number") {
      throw new LiveGateRefused(
        `live authority: RiskLimits.${field} must be a number, got ${typeof v} (${JSON.stringify(v)}) — never coerced, fail-closed`,
      );
    }
    if (!Number.isFinite(v)) {
      throw new LiveGateRefused(
        `live authority: RiskLimits.${field} must be FINITE, got ${String(v)} — an unbounded/NaN live ceiling is a fail-open (a NaN ceiling admits every order; Infinity is not signable — UNBOUNDED_PAPER_LIMITS is paper-only)`,
      );
    }
    if (v < 0) {
      throw new LiveGateRefused(`live authority: RiskLimits.${field} must be >= 0, got ${String(v)} — fail-closed`);
    }
    // Store the once-read, validated primitive. `+v` is a no-op here (v is already a number) but makes
    // the "coerced to a plain number, no reference retained" intent explicit and total.
    snapshot[field] = +v;
  }
  // FROZEN so a caller holding a reference to the returned snapshot still cannot widen it after signing.
  return Object.freeze(snapshot);
}

/**
 * The CANONICAL serialization of {@link RiskLimits} — the exact bytes a human signature attests
 * over. Deterministic and total for any set {@link assertSignableLimits} accepts: fixed field
 * order, fixed separators, a version tag, and JS's canonical shortest-round-trip number rendering.
 * Refuses (never silently normalizes) anything unsignable.
 */
export function canonicalRiskLimits(limits: RiskLimits): string {
  // Read from the FROZEN snapshot, never `limits` directly: the bytes we hash (and thus sign) are the
  // exact once-read primitives enforcement will see — a getter cannot render 5 here and 500 downstream.
  const signable = assertSignableLimits(limits);
  const body = CANONICAL_LIMIT_FIELDS.map((f) => `${f}=${String(signable[f])}`).join("\n");
  return `${CANONICAL_LIMITS_VERSION}\n${body}\n`;
}

/** The deterministic sha256 (64-char lowercase hex) of the {@link canonicalRiskLimits canonical bytes}. */
export function liveGrantDigest(limits: RiskLimits): string {
  return sha256(canonicalRiskLimits(limits));
}

/**
 * The exact message an Ed25519 human signature covers: `klive1.<digest>`. Both halves of the
 * ceremony — {@link signLiveArmGrant} offline and {@link makeHumanAuthoritySigner} at arm time —
 * build it through THIS function, so they cannot drift apart.
 */
export function liveArmSigningInput(limits: RiskLimits): string {
  return `${LIVE_ARM_SIGN_PREFIX}.${liveGrantDigest(limits)}`;
}

// ─────────────────────────────────────────────────────────────────────────────
// 2. The key set + the signatureRef wire format
// ─────────────────────────────────────────────────────────────────────────────

/**
 * One authority key in the verify-set the ceremony checks against. `signerClass` is the protocol's
 * own {@link SignerClass}: only a `"human"` key can ever mint a live arm, because the protocol
 * excludes `live` from `WALLET_SIGNABLE_SCOPES` (ADR-0034 §3). A `"wallet"` key may sit in the same
 * set — it simply can never unlock live, and the ceremony says so out loud rather than ignoring it.
 */
export interface LiveAuthorityKey {
  /** The key id the `signatureRef` names. Charset-restricted (see {@link KID_RE}) so it can never
   * collide with the wire format's `.` separators. */
  readonly kid: string;
  /** The authority class this key holds. Live requires `"human"`. */
  readonly signerClass: SignerClass;
  /** The PUBLIC half only — the ceremony never holds a private key (`src/cli/keypair.ts`). */
  readonly public_jwk: Ed25519PublicJwk;
}

/** kid/signature charset — base64url-safe, `.`-free, so `klive1.<kid>.<sig>` parses unambiguously. */
const KID_RE = /^[A-Za-z0-9_-]+$/;
const REF_RE = /^klive1\.([A-Za-z0-9_-]+)\.([A-Za-z0-9_-]+)$/;

/** The parsed halves of a `signatureRef`. */
export interface LiveSignatureRef {
  readonly kid: string;
  /** The base64url Ed25519 signature over {@link liveArmSigningInput}. */
  readonly signature: string;
}

/** Build the opaque `LiveAuthorityGrant.signatureRef` handle: `klive1.<kid>.<base64url-sig>`. */
export function formatLiveSignatureRef(kid: string, signature: string): string {
  if (!KID_RE.test(kid)) {
    throw new LiveGateRefused(`live authority: kid ${JSON.stringify(kid)} is not [A-Za-z0-9_-]+ — fail-closed`);
  }
  return `${LIVE_ARM_SIGN_PREFIX}.${kid}.${signature}`;
}

/**
 * Parse a `signatureRef` — TOTAL and fail-closed: anything that is not exactly a well-formed
 * `klive1.<kid>.<sig>` returns `undefined`, never a partial guess. This is the first place a config
 * flag dies: `"true"`, `"1"`, `"live=true"` and `""` are not signatures and never parse.
 */
export function parseLiveSignatureRef(ref: string): LiveSignatureRef | undefined {
  if (typeof ref !== "string") return undefined;
  const m = REF_RE.exec(ref);
  if (m === null) return undefined;
  return { kid: m[1]!, signature: m[2]! };
}

// ─────────────────────────────────────────────────────────────────────────────
// 3. The production AuthoritySigner
// ─────────────────────────────────────────────────────────────────────────────

/** Construction deps for {@link makeHumanAuthoritySigner}. */
export interface HumanAuthoritySignerDeps {
  /** The verify-set: the PUBLIC halves of the keys whose signatures may arm live. An EMPTY set
   * verifies nothing — it is not an open door (see {@link makeHumanAuthoritySigner}). */
  readonly authorities: readonly LiveAuthorityKey[];
}

/**
 * The PRODUCTION {@link AuthoritySigner} — re-verifies a {@link LiveAuthorityGrant}'s Ed25519 human
 * signature over the CANONICAL limits, against a held public key selected by the ref's own kid.
 *
 * TOTAL and FAIL-CLOSED: `verify` returns a boolean and NEVER throws. Every path that is not "a
 * held, `"human"`-class key verifies this exact signature over these exact limits" resolves to
 * `false`, so `armLive` refuses:
 *
 *  - the grant's scope is not `"live"`;
 *  - the ref does not parse as `klive1.<kid>.<sig>` (a config flag / env var / bare string);
 *  - no held key carries that kid (we cannot verify ⇒ we refuse — an unknown kid is not a pass);
 *  - the key is `"wallet"`-class (live is not wallet-signable, ADR-0034 §3);
 *  - the limits are unsignable (unbounded/NaN/negative — {@link assertSignableLimits});
 *  - the signature does not cover THESE limits (widened-after-signing, cross-grant replay);
 *  - any decoding/crypto error at all.
 */
export function makeHumanAuthoritySigner(deps: HumanAuthoritySignerDeps): AuthoritySigner {
  const authorities = [...deps.authorities];
  return {
    verify(grant: LiveAuthorityGrant): boolean {
      try {
        // Scope: only the non-wallet-signable "live" scope is what this signer attests to.
        if (grant?.scope !== "live") return false;
        const parsed = parseLiveSignatureRef(grant.signatureRef);
        if (parsed === undefined) return false;
        const key = authorities.find((k) => k.kid === parsed.kid);
        // Unknown kid: we hold no key, so we cannot re-verify, so we refuse. Never a pass.
        if (key === undefined) return false;
        // Class wall: a wallet signature — however cryptographically valid — can never arm live.
        if (key.signerClass !== "human") return false;
        // Rebuild the signed message from the limits IN THE GRANT (never from anything the caller
        // asserts alongside them): this is what makes the thing signed the thing enforced.
        const message = Buffer.from(liveArmSigningInput(grant.limits));
        const publicKey = createPublicKey({ key: { ...key.public_jwk }, format: "jwk" });
        return ed25519Verify(null, message, publicKey, Buffer.from(parsed.signature, "base64url"));
      } catch {
        // Fail closed: a malformed JWK, an undecodable signature, unsignable limits — all refuse.
        return false;
      }
    },
  };
}

// ─────────────────────────────────────────────────────────────────────────────
// 4. The ceremony
// ─────────────────────────────────────────────────────────────────────────────

/** The inputs to {@link performLiveArmCeremony}. */
export interface LiveArmCeremony {
  /** The EXACT risk ceilings to arm — the human signed over these and the clamp will enforce them. */
  readonly limits: RiskLimits;
  /** The human's signature handle (`klive1.<kid>.<sig>`), produced OFFLINE by
   * {@link signLiveArmGrant} with a private key this process never holds. */
  readonly signatureRef: string;
  /** The verify-set — the PUBLIC halves of the authority keys. */
  readonly authorities: readonly LiveAuthorityKey[];
}

/**
 * **The arm-minting ceremony** — the ONE production path from a human signature to a
 * {@link LiveArm}, and the entry point Wave B's live driver calls.
 *
 * It refuses EARLY and LEGIBLY (typed {@link LiveGateRefused}, one reason each) before delegating
 * to `armLive`, so an operator gets "your signature was not a signature" rather than a generic
 * "did not verify". Then it hands the grant to `armLive` with the PRODUCTION
 * {@link makeHumanAuthoritySigner} — `armLive` remains the sole mint, and the signature wall is
 * re-run there. Nothing here can substitute for the signature: every pre-check only ever REFUSES.
 */
export function performLiveArmCeremony(ceremony: LiveArmCeremony): LiveArm {
  // Wall 1 — the ceilings must be signable at all (bounded, finite, non-negative). A legible
  // refusal here beats a cryptic signature failure, and unbounded live authority never gets close.
  // CAPTURE the frozen plain-number snapshot (each field read exactly once, HERE): this — not the
  // caller's mutable object — is what flows into the grant, the minted token, and the clamp, so a
  // post-sign `ceremony.limits.maxOrderQty = 500` (or a divergent getter) can never reach enforcement.
  const signedLimits = assertSignableLimits(ceremony.limits);

  // Wall 2 — there must BE a verify-set. An empty set is not an open door; with no held key there
  // is nothing that could verify, so refuse loudly rather than fall through to a `false`.
  if (ceremony.authorities.length === 0) {
    throw new LiveGateRefused(
      "live arm ceremony: no authority keys were supplied — with no held public key nothing can verify a human signature, so live can never be armed (fail-closed)",
    );
  }

  // Wall 3 — at least one HUMAN-class key. A wallet-only verify-set can never arm live (the
  // protocol excludes `live` from WALLET_SIGNABLE_SCOPES; ADR-0034 §3).
  if (!ceremony.authorities.some((k) => k.signerClass === "human")) {
    throw new LiveGateRefused(
      "live arm ceremony: the authority key set holds no `human`-class key — live is NOT wallet-signable (protocol/index.ts WALLET_SIGNABLE_SCOPES, ADR-0034 §3), so a wallet/config authority can never arm it (fail-closed)",
    );
  }

  // Wall 4 — THE CENTRAL INVARIANT, stated where an operator will read it: the thing presented must
  // be a SIGNATURE. A config flag, an env var, a JSON `armed: true`, a CLI switch — none of them
  // parse as `klive1.<kid>.<sig>`, and there is no other input to this function that could arm.
  if (parseLiveSignatureRef(ceremony.signatureRef) === undefined) {
    throw new LiveGateRefused(
      `live arm ceremony: ${JSON.stringify(ceremony.signatureRef)} is not a human signature handle (expected ${LIVE_ARM_SIGN_PREFIX}.<kid>.<base64url-signature>) — a config flag / env var / CLI switch can NEVER arm live; only a human signature over the canonical risk limits can (fail-closed)`,
    );
  }

  // Wall 5 — the real signature check, through the SOLE mint. `armLive` re-checks the scope and
  // calls the production signer; if the signature does not cover exactly these canonical limits it
  // throws and no arm exists.
  const grant: LiveAuthorityGrant = { scope: "live", limits: signedLimits, signatureRef: ceremony.signatureRef };
  return armLive(grant, makeHumanAuthoritySigner({ authorities: ceremony.authorities }));
}

// ─────────────────────────────────────────────────────────────────────────────
// 5. The human's offline half
// ─────────────────────────────────────────────────────────────────────────────

/** Inputs to {@link signLiveArmGrant} — the HUMAN's side of the ceremony. */
export interface SignLiveArmGrantInput {
  /** The exact ceilings being authorized. */
  readonly limits: RiskLimits;
  /** The kid the verify-set knows this key by. */
  readonly kid: string;
  /** The human's PRIVATE Ed25519 JWK. This never comes from config/env in a real ceremony — it is
   * the operator's own key material, held offline (`src/cli/keypair.ts` shape). */
  readonly privateJwk: Ed25519PrivateJwk;
}

/**
 * Sign a live-arm grant — the ceremony's HUMAN half, and the tool a real operator (or a fixture)
 * uses to produce a `signatureRef`. Returns the opaque handle {@link performLiveArmCeremony}
 * consumes.
 *
 * Deliberately requires the caller to already hold a private key: there is no "generate and
 * self-sign" path here, because a process that can mint its own authority has no authority. It
 * validates the limits SYNCHRONOUSLY (before any await), so an unbounded/NaN ceiling throws a typed
 * {@link LiveGateRefused} at the call rather than rejecting a promise — nothing unsignable can even
 * be signed, so no such signature can exist for the verifier to be tricked by.
 */
export function signLiveArmGrant(input: SignLiveArmGrantInput): Promise<string> {
  // SYNCHRONOUS validation (both walls) so the refusal is a throw, not a rejection.
  const message = liveArmSigningInput(input.limits);
  if (!KID_RE.test(input.kid)) {
    throw new LiveGateRefused(`live authority: kid ${JSON.stringify(input.kid)} is not [A-Za-z0-9_-]+ — fail-closed`);
  }
  return signEd25519(message, input.privateJwk).then((sig) => formatLiveSignatureRef(input.kid, sig));
}

/** Ed25519 sign → base64url, via WebCrypto (node ≥18 / bun) — the same face `src/cli/keypair.ts` uses. */
async function signEd25519(message: string, privateJwk: Ed25519PrivateJwk): Promise<string> {
  const c = (globalThis as { crypto?: Crypto }).crypto;
  if (!c?.subtle) {
    throw new LiveGateRefused("live authority: WebCrypto (crypto.subtle) unavailable — cannot sign a live arm grant (need node >= 18 or bun)");
  }
  const key = await c.subtle.importKey("jwk", { ...privateJwk }, { name: "Ed25519" }, false, ["sign"]);
  const sig = await c.subtle.sign({ name: "Ed25519" }, key, Buffer.from(message));
  return Buffer.from(new Uint8Array(sig)).toString("base64url");
}
