/**
 * T9 · mTLS agent-cert verification (INV4/INV14). Deny-by-default: an expired leaf, a chain that
 * does not verify against the CA, or a SPIFFE-ID whose account/host is not enrolled in the registry
 * is refused. The CA never validates a pubkey not enrolled (INV4).
 *
 * Cert parsing/chain verification is delegated to an injectable `ParseCert` seam (default: node
 * `X509Certificate`) so the P5-owned decision logic (SPIFFE match + registry lookup + expiry) is
 * deterministically testable and the real X.509 verification still ships in production.
 *
 * `caChainPem` is a PEM **bundle** that may hold multiple concatenated certificates (intermediate(s)
 * + root, INV14). `defaultParseX509` splits every PEM block and performs full path validation —
 * walking leaf → issuer → … until it terminates at a self-signed root that is present in the bundle
 * (the trust anchor set). A single-hop issuer check is NOT sufficient: a real chain must be walked so
 * a leaf signed by an untrusted or missing intermediate is refused.
 */
import { X509Certificate } from 'node:crypto'
import type { HostRegistryPort } from '../types.js'
import { parseSpiffeId } from './spiffe.js'

/** Cap chain-walk depth so a malformed/cyclic bundle can never loop unboundedly. */
const MAX_CHAIN_DEPTH = 8

const PEM_CERT_RE = /-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g

/** Parse every PEM certificate block in a bundle into X509Certificate objects (order preserved). */
export function splitPemBundle(bundle: string): readonly X509Certificate[] {
  const blocks = bundle.match(PEM_CERT_RE) ?? []
  return blocks.map((block) => new X509Certificate(block))
}

/**
 * Full X.509 path validation: walk leaf → issuer → … through the trusted CA set until a self-signed
 * root in the set is reached. Each hop must both name the issuer (`checkIssued`) AND cryptographically
 * verify against the issuer's public key (`verify`). Returns false if no path terminates at a trusted
 * self-signed root, or if the bundle is empty, or on a cycle. This rejects a leaf whose intermediate
 * is missing/untrusted — unlike a single-hop check against just the first cert in the bundle.
 */
export function verifyChain(
  leaf: X509Certificate,
  caCerts: readonly X509Certificate[],
): boolean {
  if (caCerts.length === 0) return false
  let current = leaf
  const seen = new Set<string>()
  for (let depth = 0; depth < MAX_CHAIN_DEPTH; depth++) {
    const issuer = caCerts.find((ca) => current.checkIssued(ca) && current.verify(ca.publicKey))
    if (issuer === undefined) return false
    if (seen.has(issuer.fingerprint256)) return false // cycle guard
    seen.add(issuer.fingerprint256)
    if (issuer.checkIssued(issuer)) return true // reached a trusted self-signed root
    current = issuer
  }
  return false
}

export interface MtlsVerifyResult {
  readonly ok: boolean
  readonly hostId?: string
  readonly accountId?: string
  readonly reason?: string
}

/** Minimal parsed-cert surface the decision logic needs. */
export interface ParsedCert {
  readonly spiffeUri: string | null // SPIFFE-ID from the SAN URI, if present
  readonly notBefore: number // epoch seconds
  readonly notAfter: number // epoch seconds
  readonly chainValid: boolean // leaf verifies against the provided CA chain
}

export type ParseCert = (leafPem: string, caChainPem: string) => ParsedCert

/** Default parser using node's X509Certificate with full multi-cert chain-path validation. */
export const defaultParseX509: ParseCert = (leafPem, caChainPem) => {
  const leaf = new X509Certificate(leafPem)
  const caCerts = splitPemBundle(caChainPem)
  const san = leaf.subjectAltName ?? ''
  const uriMatch = /URI:(spiffe:\/\/[^,\s]+)/.exec(san)
  return {
    spiffeUri: uriMatch !== null ? uriMatch[1]! : null,
    notBefore: Math.floor(new Date(leaf.validFrom).getTime() / 1000),
    notAfter: Math.floor(new Date(leaf.validTo).getTime() / 1000),
    chainValid: verifyChain(leaf, caCerts),
  }
}

export async function verifyAgentCert(
  leafPem: string,
  caChainPem: string,
  now: number,
  hosts: HostRegistryPort,
  parse: ParseCert = defaultParseX509,
): Promise<MtlsVerifyResult> {
  let cert: ParsedCert
  try {
    cert = parse(leafPem, caChainPem)
  } catch {
    return { ok: false, reason: 'unparseable_cert' }
  }
  if (!cert.chainValid) return { ok: false, reason: 'chain_invalid' }
  if (now < cert.notBefore) return { ok: false, reason: 'not_yet_valid' }
  if (now > cert.notAfter) return { ok: false, reason: 'expired' }
  if (cert.spiffeUri === null) return { ok: false, reason: 'no_spiffe_san' }

  let ids: ReturnType<typeof parseSpiffeId>
  try {
    ids = parseSpiffeId(cert.spiffeUri)
  } catch {
    return { ok: false, reason: 'bad_spiffe_id' }
  }
  // Host channel is host-only. A /device/ cert must never be accepted here (its enforcement
  // gate is nginx :8470), so an id that collides with an enrolled hostId can't cross tenants.
  if (ids.kind !== 'host') return { ok: false, reason: 'not_host_cert' }

  const host = await hosts.getById(ids.id)
  if (host === null) return { ok: false, reason: 'host_not_enrolled' } // INV4: not in registry
  if (host.accountId !== ids.accountId) return { ok: false, reason: 'spiffe_account_mismatch' }
  if (host.status === 'revoked') return { ok: false, reason: 'host_revoked' }

  return { ok: true, hostId: ids.id, accountId: ids.accountId }
}
