/**
 * Short-lived cert rotation — PLAN_RELAY_AGENT T13 (INV14). Renews the mTLS cert BEFORE expiry via
 * P3's renewal endpoint using a fresh CSR over the SAME Ed25519 key (pubkey unchanged, only the
 * cert rotates). Installs the new cert atomically (keystore writeFile is whole-file). A 403 from
 * the renewal endpoint ⇒ the host was revoked ⇒ onRevoked (⇒ T14 teardown, INV12).
 *
 * NOTE (cross-plan / open Q#5): the renewal URL + its auth (mTLS with the current cert vs a short
 * renewal token) is P3-owned. This derives `${enrollUrl}` → `.../renew` and injects fetch; when P3
 * freezes the route, adjust `renewalUrlFor`. Single integration point.
 */
import { X509Certificate } from 'node:crypto'
import type { AgentConfig } from '../config/agentConfig.js'
import type { AgentIdentity } from '../keys/identity.js'
import type { Keystore } from '../keys/keystore.js'
import type { TimerLike } from '../transport/seams.js'
import { buildCsr } from '../enroll/csr.js'

export const DEFAULT_RENEW_BEFORE_MS = 5 * 60_000 // renew 5 min before expiry

export interface CertRotator {
  start(): void
  stop(): void
  onRotated(cb: () => void): void
  onRevoked(cb: () => void): void
}

export type RenewOutcome = 'rotated' | 'revoked'

/** Derive P3's renewal route from the enroll URL (integration point, open Q#5). */
export function renewalUrlFor(cfg: AgentConfig): string {
  return cfg.enrollUrl.replace(/\/enroll$/, '/renew')
}

/** Ms until (validTo − renewBeforeMs), clamped to ≥ 0. */
export function computeRenewDelayMs(
  certPem: string,
  renewBeforeMs: number,
  now: Date,
  parse: (pem: string) => Date = (p) => new Date(new X509Certificate(p).validTo),
): number {
  const validTo = parse(certPem).getTime()
  return Math.max(0, validTo - renewBeforeMs - now.getTime())
}

/**
 * Perform one renewal round-trip. Returns 'rotated' (new cert stored) or 'revoked' (403). Any
 * other HTTP/network failure throws (caller retries with backoff; the tunnel stays up until the
 * cert actually expires).
 */
export async function renewCert(
  cfg: AgentConfig,
  id: AgentIdentity,
  ks: Keystore,
  fetchImpl: typeof fetch,
): Promise<RenewOutcome> {
  const csr = buildCsr(id, cfg.subdomain ?? 'web-terminal-agent')
  const res = await fetchImpl(renewalUrlFor(cfg), {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({ csr }),
  })
  if (res.status === 403) return 'revoked'
  if (!res.ok) throw new Error(`cert renewal failed: HTTP ${res.status}`)
  const json = (await res.json()) as { cert?: string; caChain?: string }
  if (typeof json.cert !== 'string' || typeof json.caChain !== 'string') {
    throw new Error('cert renewal response missing cert/caChain')
  }
  ks.saveCert(json.cert, json.caChain) // atomic whole-file install
  return 'rotated'
}

export function createCertRotator(
  cfg: AgentConfig,
  id: AgentIdentity,
  ks: Keystore,
  opts: {
    renewBeforeMs?: number
    timer?: TimerLike
    fetchImpl?: typeof fetch
    now?: () => Date
    parseCert?: (pem: string) => Date
  } = {},
): CertRotator {
  const renewBeforeMs = opts.renewBeforeMs ?? DEFAULT_RENEW_BEFORE_MS
  const parseCert = opts.parseCert ?? ((p) => new Date(new X509Certificate(p).validTo))
  const timer = opts.timer ?? {
    setTimeout: (cb, ms) => setTimeout(cb, ms),
    clearTimeout: (h) => clearTimeout(h as ReturnType<typeof setTimeout>),
    setInterval: (cb, ms) => setInterval(cb, ms),
    clearInterval: (h) => clearInterval(h as ReturnType<typeof setInterval>),
  }
  const doFetch = opts.fetchImpl ?? fetch
  const now = opts.now ?? (() => new Date())
  let handle: unknown = null
  let rotatedCb: (() => void) | null = null
  let revokedCb: (() => void) | null = null

  function schedule(): void {
    const certs = ks.loadCert()
    if (certs === null) return
    const delay = computeRenewDelayMs(certs.certPem, renewBeforeMs, now(), parseCert)
    handle = timer.setTimeout(runRenewal, delay)
  }

  function runRenewal(): void {
    void renewCert(cfg, id, ks, doFetch)
      .then((outcome) => {
        if (outcome === 'revoked') {
          revokedCb?.()
          return
        }
        rotatedCb?.()
        schedule()
      })
      .catch(() => {
        // network error: retry after renewBeforeMs; the tunnel stays up meanwhile.
        handle = timer.setTimeout(runRenewal, renewBeforeMs)
      })
  }

  return {
    start(): void {
      schedule()
    },
    stop(): void {
      if (handle !== null) timer.clearTimeout(handle)
      handle = null
    },
    onRotated(cb): void {
      rotatedCb = cb
    },
    onRevoked(cb): void {
      revokedCb = cb
    },
  }
}
