/**
 * Native-tunnel CA boot wiring. Builds the TWO P-256 CAs of the native-tunnel PKI — `frp-client-CA`
 * (host frp-client leaves) and `device-CA` (device data-path leaves) — as
 * `{ signer, caCertDer, anchorsDer, issuerName }`. Both are SEPARATE trust roots from the Ed25519
 * relay agent-CA (§1.3); each is P-256 (the VPS `gen-device-ca.sh` "P-256 never Ed25519" rule).
 *
 * DEV/TEST path (default): generate a self-signed P-256 CA whose key is `inProcessP256CaSigner`, so
 * every issued leaf chains to a REAL, re-parseable CA (mirrors `rotate.test.ts` `makeP256Ca`). This
 * is NOT a KMS — the plan mandates a real non-exportable key in production (§3.1).
 *
 * PROD path: resolve each CA's KMS-non-exportable P-256 signer via `buildCaSigner` (reusing its INV9
 * key-policy fail-fast) with a PER-CA KMS key ref, plus that CA's (public) cert DER loaded from
 * configured `material`. FAIL-FAST (INV9) if the native-CA material is unresolvable in production —
 * never silently fall back to an in-process dev CA. The env does not yet carry per-CA key refs /
 * native-CA cert paths, so production wiring supplies them via `material`; the documented follow-up
 * is to add `NATIVE_FRP_CLIENT_CA_*` / `NATIVE_DEVICE_CA_*` (KMS ref + cert path) to `env.ts` and map
 * them here.
 *
 * `reflect-metadata` must load before `@peculiar/x509` (tsyringe polyfill) — keep the side-effect first.
 */
import 'reflect-metadata'
import * as x509 from '@peculiar/x509'
import { webcrypto, generateKeyPairSync } from 'node:crypto'
import { readFileSync } from 'node:fs'
import type { ControlPlaneEnv, NativeCaEnvConfig } from '../env.js'
import { assembleCertificate } from '../ca/x509-assembler.js'
import {
  buildCaSigner,
  fileKeyRef,
  fileKmsResolver,
  inProcessP256CaSigner,
  type CaSigner,
  type KmsResolver,
} from './ca-wiring.js'

x509.cryptoProvider.set(webcrypto)

/** DNS zone the reachable subdomain lives under (leftmost label = the nginx :8470 enforcement key). */
export const DEFAULT_NATIVE_DNS_ZONE = 'terminal.yaojia.wang'

/** Dev CA validity: 1y forward, backdated 1d for clock skew (dev-only self-signed anchor). */
const DEV_CA_VALIDITY_MS = 365 * 24 * 60 * 60 * 1000
const DEV_CA_BACKDATE_MS = 24 * 60 * 60 * 1000

/** One native CA: its KMS-shaped signer, its (public) CA cert DER, the anchor set, and issuer DN. */
export interface NativeCa {
  readonly signer: CaSigner
  /** DER of the CA's own (self-signed in dev) certificate. */
  readonly caCertDer: Uint8Array
  /** Trust-anchor DER set the renew route chain-validates presented certs against (non-empty). */
  readonly anchorsDer: readonly Uint8Array[]
  /** The CA subject DN — used verbatim as the leaf issuer so `checkIssued` matches. */
  readonly issuerName: x509.Name
}

/** The two native-tunnel CAs. */
export interface NativeCas {
  readonly frpClientCa: NativeCa
  readonly deviceCa: NativeCa
}

/** Production material for one CA: its KMS key ref + its (public) CA cert DER. */
export interface NativeCaMaterialItem {
  readonly kmsKeyRef: string
  readonly caCertDer: Uint8Array
}

/** Production material for BOTH native CAs (supplied by the deployment, not generated). */
export interface NativeCaMaterial {
  readonly frpClientCa: NativeCaMaterialItem
  readonly deviceCa: NativeCaMaterialItem
}

export interface BuildNativeCasOpts {
  /** Production mode → fail-fast unless real KMS-backed material is supplied (INV9). */
  readonly production?: boolean
  /** KMS resolver (per-CA key ref → non-exportable P-256 signer). Required in production. */
  readonly kmsResolver?: KmsResolver
  /** Production-loaded per-CA material (KMS ref + public cert DER). Required in production. */
  readonly material?: NativeCaMaterial
  /** Clock (ms) — injectable for tests. */
  readonly now?: () => number
}

/** Parse a CA cert DER into an `x509.Name` issuer, failing loud on unparseable material (INV9). */
function issuerNameOf(caCertDer: Uint8Array): x509.Name {
  try {
    return new x509.X509Certificate(caCertDer).subjectName
  } catch {
    throw new Error('native-CA certificate material is not a parseable X.509 certificate — refusing to boot')
  }
}

/**
 * DEV/TEST: a self-signed P-256 CA (subject key == signer key) so issued leaves chain to a real CA.
 * BasicConstraints CA:true + KeyUsage keyCertSign|cRLSign, exactly like the test fixtures.
 */
async function buildDevNativeCa(cn: string, nowMs: number): Promise<NativeCa> {
  const caKeys = generateKeyPairSync('ec', { namedCurve: 'P-256' })
  const caSpki = new Uint8Array(caKeys.publicKey.export({ format: 'der', type: 'spki' }))
  const signer = inProcessP256CaSigner(caKeys.privateKey)
  const caCertDer = await assembleCertificate({
    subjectPublicKey: caSpki,
    subject: `CN=${cn}`,
    issuer: `CN=${cn}`,
    serialNumber: Uint8Array.from([0x01]),
    notBefore: new Date(nowMs - DEV_CA_BACKDATE_MS),
    notAfter: new Date(nowMs + DEV_CA_VALIDITY_MS),
    extensions: [
      new x509.BasicConstraintsExtension(true, undefined, true),
      new x509.KeyUsagesExtension(x509.KeyUsageFlags.keyCertSign | x509.KeyUsageFlags.cRLSign, true),
    ],
    signer,
    sigAlg: 'ecdsa-p256',
  })
  return { signer, caCertDer, anchorsDer: [caCertDer], issuerName: issuerNameOf(caCertDer) }
}

/**
 * PROD: resolve a KMS-non-exportable P-256 signer for this CA's key ref (reusing `buildCaSigner`'s
 * INV9 policy fail-fast) and pair it with the supplied (public) CA cert DER as the trust anchor.
 */
async function buildProdNativeCa(
  item: NativeCaMaterialItem,
  env: ControlPlaneEnv,
  kmsResolver: KmsResolver,
): Promise<NativeCa> {
  // Reuse the intermediate-key policy check by targeting this CA's key ref (INV9, §3.1).
  const signer = await buildCaSigner({ ...env, caIntermediateKmsKeyRef: item.kmsKeyRef }, kmsResolver)
  return {
    signer,
    caCertDer: item.caCertDer,
    anchorsDer: [item.caCertDer],
    issuerName: issuerNameOf(item.caCertDer),
  }
}

/**
 * Build both native-tunnel CAs. DEV default → self-signed in-process P-256 CAs. PRODUCTION → resolve
 * KMS-backed signers + loaded CA certs from `material`; FAIL-FAST if either is missing (INV9 — never
 * boot a public-shell PKI on ephemeral, unattested in-process keys).
 */
export async function buildNativeCas(env: ControlPlaneEnv, opts: BuildNativeCasOpts = {}): Promise<NativeCas> {
  const production = opts.production ?? false
  if (production) {
    if (opts.material === undefined || opts.kmsResolver === undefined) {
      throw new Error(
        'native-tunnel CA material is unresolvable in production (per-CA KMS key refs + CA certs required) — refusing to boot',
      )
    }
    const [frpClientCa, deviceCa] = await Promise.all([
      buildProdNativeCa(opts.material.frpClientCa, env, opts.kmsResolver),
      buildProdNativeCa(opts.material.deviceCa, env, opts.kmsResolver),
    ])
    return { frpClientCa, deviceCa }
  }
  const nowMs = opts.now?.() ?? Date.now()
  const [frpClientCa, deviceCa] = await Promise.all([
    buildDevNativeCa('frp-client-CA', nowMs),
    buildDevNativeCa('device-CA', nowMs),
  ])
  return { frpClientCa, deviceCa }
}

/** Load one CA's (public) cert PEM from disk as anchor DER, failing loud on unreadable/unparseable material (INV9). */
function loadCaCertDer(certPath: string): Uint8Array {
  let pem: string
  try {
    pem = readFileSync(certPath, 'utf8')
  } catch {
    // Never echo the file contents — only that it was unreadable (INV9).
    throw new Error('native-CA certificate file is unreadable — refusing to boot')
  }
  try {
    return new Uint8Array(new x509.X509Certificate(pem).rawData)
  } catch {
    throw new Error('native-CA certificate material is not a parseable X.509 certificate — refusing to boot')
  }
}

/**
 * PRODUCTION wiring from ON-DISK material — the A2-prep prerequisite for deploying the control-plane.
 * Builds both native-tunnel CAs from the VPS `gen-device-ca.sh` output: each CA's (public) cert PEM
 * becomes its trust-anchor DER and its PKCS#8 PEM private key path becomes a `file:` KMS ref resolved
 * by `fileKmsResolver` (single-owner file-custody; a non-exportable KMS is the upgrade). Delegates to
 * `buildNativeCas` in production mode so the SAME INV9 fail-fast applies — any unreadable/unparseable
 * cert or non-P-256 key refuses to boot. Raw key bytes are never loaded/logged at this layer.
 */
export async function buildFileBackedNativeCas(
  env: ControlPlaneEnv,
  config: NativeCaEnvConfig,
  opts: { readonly now?: () => number } = {},
): Promise<NativeCas> {
  const material: NativeCaMaterial = {
    frpClientCa: {
      kmsKeyRef: fileKeyRef(config.frpClientCaKeyPath),
      caCertDer: loadCaCertDer(config.frpClientCaCertPath),
    },
    deviceCa: {
      kmsKeyRef: fileKeyRef(config.deviceCaKeyPath),
      caCertDer: loadCaCertDer(config.deviceCaCertPath),
    },
  }
  return buildNativeCas(env, {
    production: true,
    kmsResolver: fileKmsResolver(),
    material,
    ...(opts.now !== undefined ? { now: opts.now } : {}),
  })
}
