/**
 * Enrolment identifiers (`hostId` / `subdomain`) persisted next to the keystore.
 *
 * WHY: `pair --install` learns both from the control-plane's enroll response, but nothing ever wrote
 * them down — so the long-running `run` process had `cfg.subdomain === null` and `cfg.hostId === null`
 * and every log line came out as `{"subdomain":null,"hostId":null}`. When the tunnel broke in
 * production, 6380 warnings named no host at all, which is exactly the moment you want them to.
 *
 * They are NOT secrets (the subdomain is a public DNS label), so this is a plain JSON file — kept in
 * `stateDir` only because that is the one directory the agent already owns on every platform.
 *
 * Legacy installs enrolled before this existed have no record. Their leaf still carries the
 * subdomain in its SPIFFE URI SAN, so `resolveHostIdentity` recovers it from there rather than
 * forcing a re-pair just to get an identifier back into the logs.
 */
import { X509Certificate } from 'node:crypto'
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
import { join } from 'node:path'
import type { AgentConfig } from './agentConfig.js'

const RECORD_FILE = 'host.json'
const DIR_MODE = 0o700

/** Non-secret identifiers assigned by the control-plane at enrolment. */
export interface HostRecord {
  readonly hostId: string | null
  readonly subdomain: string | null
}

/** A non-empty string, or null — anything else on disk is treated as absent (never trusted). */
function stringOrNull(value: unknown): string | null {
  return typeof value === 'string' && value.length > 0 ? value : null
}

/** Persist the enrolment identifiers into `stateDir`. Overwrites any previous record. */
export function saveHostRecord(stateDir: string, record: HostRecord): void {
  if (!existsSync(stateDir)) mkdirSync(stateDir, { recursive: true, mode: DIR_MODE })
  writeFileSync(join(stateDir, RECORD_FILE), `${JSON.stringify(record, null, 2)}\n`)
}

/**
 * Read the persisted identifiers, or null if this host has none. A missing, unreadable, or
 * malformed file is reported as "no record" — this feeds a logging path and must never throw into
 * the run loop.
 */
export function loadHostRecord(stateDir: string): HostRecord | null {
  const path = join(stateDir, RECORD_FILE)
  if (!existsSync(path)) return null
  try {
    const parsed: unknown = JSON.parse(readFileSync(path, 'utf8'))
    if (typeof parsed !== 'object' || parsed === null) return null
    const rec = parsed as Record<string, unknown>
    return { hostId: stringOrNull(rec['hostId']), subdomain: stringOrNull(rec['subdomain']) }
  } catch {
    return null
  }
}

/** SPIFFE IDs issued for hosts end in `/host/<subdomain>` (see relay-auth `spiffeIdFor`). */
const SPIFFE_HOST_RE = /URI:(spiffe:\/\/[^\s,]*\/host\/([^\s,/]+))/

/**
 * Recover the subdomain from a leaf's SPIFFE URI SAN, or null if it carries none. Parse failures
 * are null, never throws — a legacy install with a damaged cert must still start.
 */
export function subdomainFromCertPem(certPem: string): string | null {
  try {
    const san = new X509Certificate(certPem).subjectAltName ?? ''
    const match = SPIFFE_HOST_RE.exec(san)
    return match?.[2] ?? null
  } catch {
    return null
  }
}

/**
 * Resolve the identifiers to log for this host, most authoritative first:
 *   1. explicit config (argv/env `SUBDOMAIN` / `HOST_ID`) — an operator override always wins;
 *   2. the enrolment record written by `pair`;
 *   3. the subdomain embedded in the stored leaf (legacy installs; yields no hostId).
 *
 * `readCertPem` is injected so this stays a pure decision over a supplied cert.
 */
export function resolveHostIdentity(
  cfg: AgentConfig,
  readCertPem: () => string | null,
): HostRecord {
  const record = loadHostRecord(cfg.stateDir)
  const subdomain =
    cfg.subdomain ??
    record?.subdomain ??
    ((): string | null => {
      const pem = readCertPem()
      return pem === null ? null : subdomainFromCertPem(pem)
    })()
  return { hostId: cfg.hostId ?? record?.hostId ?? null, subdomain }
}
