/**
 * T8 — bind-time mTLS leaf signing, registry-gated (INV14 registry half). ORDER OF CHECKS (all
 * must pass, SAME reject path):
 *   1. CSR proof-of-possession — verify the PKCS#10 self-signature against its embedded pubkey.
 *   2. embedded pubkey == caller-supplied `agentPubkey` (no substitution).
 *   3. (hostId, agentPubkey) is an ACTIVE, non-revoked row in the host registry.
 * A failure at ANY step rejects identically; issuance is NEVER reached when any check fails.
 *
 * Two `LeafSigner` implementations share the `assertLeafGate` gate below:
 *   - `createLeafSigner` (this file) — DEV/legacy placeholder cert (a signed JSON blob, NOT X.509);
 *     used only when no real CA material is configured (see `main.ts` fallback).
 *   - `createRealLeafSigner` (`ca/issue.ts`) — the production issuer that emits a real X.509 v3
 *     Ed25519 leaf with a SPIFFE SAN, signed by the intermediate key.
 */
import type { HostStore } from '../store/ports.js'
import type { HostRecord } from '../model/records.js'
import type { CaSigner } from '../boot/ca-wiring.js'
import { verifyCsrPoP } from './csr.js'
import { timingSafeEqualBytes, bytesToBase64 } from '../util/bytes.js'

export class LeafSignError extends Error {
  constructor(public readonly code: 'csr_rejected' | 'not_registered') {
    super('leaf signing refused') // uniform message — do not leak which check failed
  }
}

export interface LeafSigner {
  signHostLeaf(
    hostId: string,
    agentPubkey: Uint8Array,
    csr: Uint8Array,
  ): Promise<{ cert: Uint8Array; caChain: readonly Uint8Array[] }>
}

/** Leaf validity in seconds (short-lived, INV14). Default 24h. */
export const DEFAULT_LEAF_TTL_SEC = 24 * 60 * 60

/**
 * Shared registry + proof-of-possession gate (INV14). Runs the three ordered checks and, on
 * success, returns the gated host record so the issuer can build the SPIFFE SAN from the
 * authoritative `accountId`/`hostId` binding. Throws `LeafSignError` (uniform) on any failure.
 */
export async function assertLeafGate(
  hosts: HostStore,
  hostId: string,
  agentPubkey: Uint8Array,
  csr: Uint8Array,
): Promise<HostRecord> {
  // 1. proof-of-possession (independent of registry state)
  const pop = await verifyCsrPoP(csr)
  if (!pop.ok) throw new LeafSignError('csr_rejected')
  // 2. no substitution: CSR pubkey must equal the presented agentPubkey
  if (!timingSafeEqualBytes(pop.embeddedPub, agentPubkey)) throw new LeafSignError('csr_rejected')
  // 3. registry gate: host bound, active, non-revoked, pubkey matches (INV14)
  const host = await hosts.get(hostId)
  if (host === null || host.status === 'revoked') throw new LeafSignError('not_registered')
  if (!timingSafeEqualBytes(host.agentPubkey, agentPubkey)) throw new LeafSignError('not_registered')
  return host
}

export interface LeafSignerDeps {
  readonly hosts: HostStore
  readonly signer: CaSigner
  readonly caChainDer: readonly Uint8Array[]
  readonly leafTtlSec?: number
}

/**
 * DEV/legacy signer — emits a signed JSON placeholder (NOT real X.509). Retained so tests and dev
 * boots without configured CA material still exercise the gate/reject path. Production uses
 * `createRealLeafSigner` (`ca/issue.ts`).
 */
export function createLeafSigner(deps: LeafSignerDeps): LeafSigner {
  const ttl = deps.leafTtlSec ?? DEFAULT_LEAF_TTL_SEC
  return {
    async signHostLeaf(hostId, agentPubkey, csr) {
      const host = await assertLeafGate(deps.hosts, hostId, agentPubkey, csr)

      // Only now do we invoke KMS sign() over the to-be-signed placeholder.
      const notAfter = Math.floor(Date.now() / 1000) + ttl
      const tbs = new TextEncoder().encode(
        JSON.stringify({
          v: 1,
          hostId: host.hostId,
          subjectSpki: bytesToBase64(agentPubkey), // subject pubkey == agentPubkey (assertable in tests)
          notAfter,
        }),
      )
      const sig = await deps.signer.sign(tbs)
      const cert = new TextEncoder().encode(
        JSON.stringify({ tbs: bytesToBase64(tbs), sig: bytesToBase64(sig) }),
      )
      return { cert, caChain: deps.caChainDer }
    },
  }
}
