/**
 * Agent identity — PLAN_RELAY_AGENT T3 (INV4: the private key NEVER leaves the host).
 *
 * Two key algorithms share one `AgentIdentity` shape (discriminated by `alg`):
 *   - `ed25519` — the relay E2E rendezvous path (unchanged).
 *   - `p256` — the native-tunnel HOST frp-client key (FIX H-host-2): the `frp-client-CA` is P-256,
 *     so the host key, its CSR (ECDSA-with-SHA256), and its leaf are all P-256.
 * `AgentIdentity` exposes ONLY the public key, the §4.2 enroll fingerprint, and an in-process
 * `sign()`; there is NO API that returns or serializes the private key. The raw private key material
 * is held in a module-private closure and, for P-256, NEVER leaves the host (only the pubkey + CSR do).
 */
import { createHash, createPrivateKey, createPublicKey, generateKeyPairSync, sign, verify } from 'node:crypto'
import type { KeyObject } from 'node:crypto'
import { encodeBase64UrlBytes } from 'relay-contracts'

/** Which key algorithm an identity carries. Drives CSR SPKI + signatureAlgorithm encoding. */
export type KeyAlg = 'ed25519' | 'p256'

export interface AgentIdentity {
  /** Which key algorithm this identity uses (`ed25519` = relay path, `p256` = native frp-client). */
  readonly alg: KeyAlg
  /**
   * The registry-stored public key bytes (§4.2 agent_pubkey):
   *   - `ed25519`: the raw 32-byte public key;
   *   - `p256`: the EC SubjectPublicKeyInfo DER (what the control-plane P-256 gate compares).
   */
  readonly publicKey: Uint8Array
  /** §4.2 enroll_fpr: base64url(SHA-256(publicKey)) — pinned by the browser E2E TOFU (§4.4). */
  readonly enrollFpr: string
  /**
   * Signature over `message` using the in-process private key (never returns the key):
   *   - `ed25519`: a raw 64-byte Ed25519 signature;
   *   - `p256`: a DER `ECDSA-Sig-Value` (ecdsa-with-SHA256) — the PKCS#10 signatureValue shape.
   */
  sign(message: Uint8Array): Uint8Array
  /** Export the PRIVATE key as PKCS#8 PEM — for on-disk 0600 persistence ONLY (keystore). */
  exportPrivatePkcs8Pem(): string
  /** The underlying private KeyObject — for in-process crypto (CSR/mTLS) ONLY, never serialized to the wire. */
  privateKeyObject(): KeyObject
}

/** SHA-256 fingerprint of a raw Ed25519 public key, base64url — §4.2 enroll_fpr. */
export function computeEnrollFpr(publicKey: Uint8Array): string {
  const digest = createHash('sha256').update(publicKey).digest()
  return encodeBase64UrlBytes(new Uint8Array(digest))
}

/** Raw 32-byte Ed25519 public key from a KeyObject (strips the SPKI DER prefix). */
function rawPublicKey(pub: KeyObject): Uint8Array {
  const der = pub.export({ type: 'spki', format: 'der' })
  // Ed25519 SPKI is a fixed 44-byte structure; the raw key is the trailing 32 bytes.
  return new Uint8Array(der.subarray(der.length - 32))
}

function buildIdentity(privateKey: KeyObject, publicKey: KeyObject): AgentIdentity {
  const rawPub = rawPublicKey(publicKey)
  const enrollFpr = computeEnrollFpr(rawPub)
  return {
    alg: 'ed25519',
    publicKey: rawPub,
    enrollFpr,
    sign(message: Uint8Array): Uint8Array {
      return new Uint8Array(sign(null, message, privateKey))
    },
    exportPrivatePkcs8Pem(): string {
      return privateKey.export({ type: 'pkcs8', format: 'pem' }).toString()
    },
    privateKeyObject(): KeyObject {
      return privateKey
    },
  }
}

/**
 * P-256 identity (FIX H-host-2). `publicKey` is the EC SubjectPublicKeyInfo DER (the exact bytes the
 * control-plane frp-client gate compares against the registry); `sign` produces a DER `ECDSA-Sig-Value`
 * over the SHA-256 digest — the PKCS#10 / X.509 signatureValue shape. The private key never leaves
 * this closure (INV4); only the SPKI + CSR are emitted off-host.
 */
function buildP256Identity(privateKey: KeyObject, publicKey: KeyObject): AgentIdentity {
  const spkiDer = new Uint8Array(publicKey.export({ type: 'spki', format: 'der' }))
  const enrollFpr = computeEnrollFpr(spkiDer)
  return {
    alg: 'p256',
    publicKey: spkiDer,
    enrollFpr,
    sign(message: Uint8Array): Uint8Array {
      // ecdsa-with-SHA256 → DER ECDSA-Sig-Value (node's default dsaEncoding is 'der').
      return new Uint8Array(sign('sha256', message, privateKey))
    },
    exportPrivatePkcs8Pem(): string {
      return privateKey.export({ type: 'pkcs8', format: 'pem' }).toString()
    },
    privateKeyObject(): KeyObject {
      return privateKey
    },
  }
}

/** Generate a fresh Ed25519 identity; the private key is held in-memory only (INV4). */
export function generateIdentity(): AgentIdentity {
  const { privateKey, publicKey } = generateKeyPairSync('ed25519')
  return buildIdentity(privateKey, publicKey)
}

/**
 * Generate a fresh EC P-256 identity for the native-tunnel host frp-client key (FIX H-host-2). The
 * private key is held in-memory only (INV4) and NEVER serialized off-host; only the pubkey + CSR leave.
 */
export function generateP256Identity(): AgentIdentity {
  const { privateKey, publicKey } = generateKeyPairSync('ec', { namedCurve: 'P-256' })
  return buildP256Identity(privateKey, publicKey)
}

/** Reconstruct an Ed25519 identity from a stored PKCS#8 PEM private key (keystore load path). */
export function identityFromPrivatePem(pem: string): AgentIdentity {
  const privateKey = createPrivateKey(pem)
  const publicKey = createPublicKey(privateKey)
  return buildIdentity(privateKey, publicKey)
}

/** Reconstruct a P-256 identity from a stored PKCS#8 PEM private key (keystore load path). */
export function p256IdentityFromPrivatePem(pem: string): AgentIdentity {
  const privateKey = createPrivateKey(pem)
  const publicKey = createPublicKey(privateKey)
  return buildP256Identity(privateKey, publicKey)
}

/** Verify an Ed25519 signature against a raw public key — helper for tests/handshake checks. */
export function verifySignature(
  publicKey: Uint8Array,
  message: Uint8Array,
  signature: Uint8Array,
): boolean {
  const spkiPrefix = Uint8Array.from([
    0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x03, 0x21, 0x00,
  ])
  const der = new Uint8Array(spkiPrefix.length + publicKey.length)
  der.set(spkiPrefix, 0)
  der.set(publicKey, spkiPrefix.length)
  const pub = createPublicKey({ key: Buffer.from(der), format: 'der', type: 'spki' })
  return verify(null, message, pub, signature)
}
