/**
 * A1 / FIX C-1 — the SINGLE X.509 issuance primitive for the native-tunnel PKI.
 *
 * WHY THIS EXISTS: `@peculiar/x509`'s `X509CertificateGenerator.create` needs a `signingKey:
 * CryptoKey` and CANNOT delegate to an async KMS. Real-X.509 AND KMS-non-exportable signing are
 * therefore mutually exclusive as built. This module resolves that: it DER-encodes the v3
 * `TBSCertificate` itself (reusing the battle-tested `@peculiar/asn1-x509` schemas — no hand-rolled
 * full-certificate DER) and signs the SERIALIZED TBS by calling `CaSigner.sign(tbsDer)` — the KMS
 * boundary. The raw CA private key is NEVER loaded in this module (INV9). All future issuers (host
 * frp-client, device, renew, CRL) route through this one primitive.
 *
 * `reflect-metadata` must load before `@peculiar/asn1-schema`/`@peculiar/x509` (tsyringe polyfill) —
 * keep the side-effect import first.
 */
import 'reflect-metadata'
import * as x509 from '@peculiar/x509'
import { AsnConvert } from '@peculiar/asn1-schema'
import {
  TBSCertificate,
  Certificate,
  AlgorithmIdentifier,
  Name,
  Validity,
  SubjectPublicKeyInfo,
  Extension,
  Extensions,
  Version,
} from '@peculiar/asn1-x509'
import { ECDSASigValue } from '@peculiar/asn1-ecc'
import { webcrypto } from 'node:crypto'
import type { CaSigner } from '../boot/ca-wiring.js'

x509.cryptoProvider.set(webcrypto)

/** Signature-algorithm family the CA signs with. Drives OID + signatureValue encoding. */
export type SigAlgFamily = 'ed25519' | 'ecdsa-p256'

/** OID 1.3.101.112 (Ed25519); no algorithm parameters. */
const OID_ED25519 = '1.3.101.112'
/** OID 1.2.840.10045.4.3.2 (ecdsa-with-SHA256); no algorithm parameters. */
const OID_ECDSA_WITH_SHA256 = '1.2.840.10045.4.3.2'
/** Raw P1363 (r||s) length for a P-256 signature — the WebCrypto/hardware-native ECDSA shape. */
const P256_P1363_LEN = 64
/** Raw Ed25519 signature length — the fixed 64 bytes embedded verbatim in the signatureValue BIT STRING. */
const ED25519_SIG_LEN = 64

export interface AssembleCertificateInput {
  /** Subject public key: a WebCrypto public `CryptoKey` OR its SubjectPublicKeyInfo DER. */
  readonly subjectPublicKey: CryptoKey | Uint8Array
  /** Subject Distinguished Name (an `x509.Name` or a DN string like `CN=alice`). */
  readonly subject: x509.Name | string
  /** Issuer Distinguished Name — MUST equal the signing CA's subject so `checkIssued` matches. */
  readonly issuer: x509.Name | string
  /** Positive serial-number bytes (big-endian). Sign/leading-zero normalized internally. */
  readonly serialNumber: Uint8Array
  readonly notBefore: Date
  readonly notAfter: Date
  /** Pre-built extensions (SAN, BasicConstraints, KeyUsage, EKU, …) as `@peculiar/x509` objects. */
  readonly extensions: readonly x509.Extension[]
  /** KMS-shaped CA signer. `sign(tbsDer)` is the only crypto touchpoint; raw key never loaded. */
  readonly signer: CaSigner
  /** Declared signature-algorithm family — MUST match what `signer.sign` produces. */
  readonly sigAlg: SigAlgFamily
}

/** Copy a `Uint8Array` view into a standalone `ArrayBuffer` (never a `SharedArrayBuffer`). */
function toArrayBuffer(bytes: Uint8Array): ArrayBuffer {
  return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer
}

/**
 * Normalize big-endian bytes into the content octets of a DER positive INTEGER: strip leading zero
 * bytes (keeping at least one), then prepend `0x00` if the high bit is set so the value can never be
 * read as negative. The asn1 INTEGER converter uses these bytes verbatim, so this MUST run first.
 */
function toPositiveIntegerBytes(raw: Uint8Array): Uint8Array {
  if (raw.length === 0) throw new Error('integer bytes must be non-empty')
  let start = 0
  while (start < raw.length - 1 && raw[start] === 0x00) start++
  const trimmed = raw.subarray(start)
  if ((trimmed[0]! & 0x80) === 0) return trimmed
  const prefixed = new Uint8Array(trimmed.length + 1)
  prefixed.set(trimmed, 1)
  return prefixed
}

/**
 * Normalize an ECDSA signature to the DER `ECDSA-Sig-Value ::= SEQUENCE { INTEGER r, INTEGER s }`
 * the X.509 signatureValue BIT STRING requires. Accepts BOTH shapes a signer may return:
 *  - raw P1363 `r || s` (64 bytes for P-256, the WebCrypto / Secure-Enclave / StrongBox shape) →
 *    split and re-encode each half as a positive INTEGER;
 *  - already-DER `ECDSA-Sig-Value` (Node `crypto.sign` default) → validated and passed through.
 * Throws on anything that is neither, so a malformed signer surfaces at issuance, not on the wire.
 */
export function normalizeEcdsaSignatureToDer(sig: Uint8Array): Uint8Array {
  if (sig.length === P256_P1363_LEN) {
    const half = sig.length / 2
    const r = toPositiveIntegerBytes(sig.subarray(0, half))
    const s = toPositiveIntegerBytes(sig.subarray(half))
    const value = new ECDSASigValue({ r: toArrayBuffer(r), s: toArrayBuffer(s) })
    return new Uint8Array(AsnConvert.serialize(value))
  }
  // Not raw P1363 — require a valid DER ECDSA-Sig-Value, else reject (fail loud at issuance).
  try {
    AsnConvert.parse(toArrayBuffer(sig), ECDSASigValue)
    return sig
  } catch {
    throw new Error('ECDSA signature is neither raw P1363 (64 bytes) nor DER ECDSA-Sig-Value')
  }
}

/** Resolve a subject key (CryptoKey or SPKI DER) to an asn1 `SubjectPublicKeyInfo`. */
async function toSpki(key: CryptoKey | Uint8Array): Promise<SubjectPublicKeyInfo> {
  const der = key instanceof Uint8Array ? key : new Uint8Array(await webcrypto.subtle.exportKey('spki', key))
  return AsnConvert.parse(toArrayBuffer(der), SubjectPublicKeyInfo)
}

/** Convert an `x509.Name` or DN string to an asn1 `Name` via its canonical DER. */
function toAsnName(name: x509.Name | string): Name {
  const der = name instanceof x509.Name ? name.toArrayBuffer() : new x509.Name(name).toArrayBuffer()
  return AsnConvert.parse(der, Name)
}

/** The OID for a signature family; identical instance-value in tbs.signature and outer sigAlg. */
function algorithmOid(sigAlg: SigAlgFamily): string {
  return sigAlg === 'ed25519' ? OID_ED25519 : OID_ECDSA_WITH_SHA256
}

/** Encode the raw CA-signer output into the certificate signatureValue for the declared family. */
function encodeSignatureValue(sigAlg: SigAlgFamily, rawSig: Uint8Array): Uint8Array {
  // Ed25519: the raw 64-byte signature goes directly into the BIT STRING. Validate the length first —
  // a signer that returns anything but exactly 64 bytes (truncated/malformed) would otherwise embed a
  // structurally-invalid signature; fail loud at issuance rather than emit an unverifiable cert.
  if (sigAlg === 'ed25519') {
    if (rawSig.length !== ED25519_SIG_LEN) {
      throw new Error(
        `Ed25519 signature must be exactly ${ED25519_SIG_LEN} bytes, got ${rawSig.length}`,
      )
    }
    return rawSig
  }
  return normalizeEcdsaSignatureToDer(rawSig)
}

/**
 * Assemble a signed X.509 v3 leaf certificate DER. Builds the `TBSCertificate`, serializes it,
 * signs the SERIALIZED TBS via `signer.sign` (KMS boundary — raw key never loaded), then wraps it as
 * `Certificate { tbsCertificate, signatureAlgorithm, signatureValue }`. The SAME `AlgorithmIdentifier`
 * OID is set in BOTH `tbsCertificate.signature` and `certificate.signatureAlgorithm` (X.509 requires
 * them identical). The exact signed TBS bytes are embedded verbatim (`tbsCertificateRaw`), so the
 * emitted certificate's signature always covers precisely what was signed.
 */
export async function assembleCertificate(input: AssembleCertificateInput): Promise<Uint8Array> {
  const oid = algorithmOid(input.sigAlg)
  const tbs = new TBSCertificate({
    version: Version.v3,
    serialNumber: toArrayBuffer(toPositiveIntegerBytes(input.serialNumber)),
    signature: new AlgorithmIdentifier({ algorithm: oid }),
    issuer: toAsnName(input.issuer),
    validity: new Validity({ notBefore: input.notBefore, notAfter: input.notAfter }),
    subject: toAsnName(input.subject),
    subjectPublicKeyInfo: await toSpki(input.subjectPublicKey),
    extensions: new Extensions(input.extensions.map((e) => AsnConvert.parse(e.rawData, Extension))),
  })

  const tbsDer = new Uint8Array(AsnConvert.serialize(tbs))
  const rawSig = await input.signer.sign(tbsDer)
  const signatureValue = encodeSignatureValue(input.sigAlg, rawSig)

  const certificate = new Certificate({
    tbsCertificate: tbs,
    tbsCertificateRaw: toArrayBuffer(tbsDer), // embed the EXACT bytes that were signed
    signatureAlgorithm: new AlgorithmIdentifier({ algorithm: oid }),
    signatureValue: toArrayBuffer(signatureValue),
  })
  return new Uint8Array(AsnConvert.serialize(certificate))
}
