/**
 * T9 · Agent tunnel listener (INV4/INV7/INV12/INV14). Accepts the agent's mTLS dial-out, builds
 * a relay-side MuxSession per tunnel, and registers the route (P3) with a heartbeat TTL.
 *
 * Two-tier defense (Finding-4): the agent-facing TLS server is constructed with
 * `requestCert: true` AND `rejectUnauthorized: true` and a `ca` from config — so the TLS handshake
 * itself rejects a no-cert / non-chaining peer BEFORE `attach()`/`verifyPeer()` runs. `verifyPeer`
 * (P5) then binds the already-CA-validated cert's pubkey to the registry (INV14/INV4). No bearer/
 * shared-secret path exists. The node holds only an in-RAM Map (INV7); a crash loses only that.
 */
import type { DataPlaneConfig } from './config.js'
import type { WebSocketLike } from './ws-like.js'
import type { ScheduleHandle } from '../mux/heartbeat.js'
import { createMuxSession, type MuxSession } from '../mux/mux-session.js'

export interface MtlsVerifier {
  // P5 impl — verifies the peer cert against the host registry pubkey (INV14).
  verifyPeer(peerCert: Uint8Array): { hostId: string; accountId: string } | null
}

export interface RouteRegistrar {
  // P3 impl — Redis route:{host_id} with heartbeat TTL (INV7).
  register(hostId: string, relayNodeId: string, ttlMs: number): Promise<void>
  heartbeat(hostId: string): Promise<void>
  deregister(hostId: string): Promise<void>
}

export interface AgentTunnel {
  readonly hostId: string
  readonly accountId: string // carried for account-scope revocation fan-out (T14)
  readonly session: MuxSession
  closeTunnel(): void
}

/** Frozen mTLS server options — the ONLY shape the agent-facing TLS server is ever built with. */
export interface TlsServerOptions {
  readonly requestCert: true
  readonly rejectUnauthorized: true
  readonly ca: readonly Uint8Array[]
}

export interface TlsServerLike {
  close(): void
}

export type TlsServerFactory = (
  opts: TlsServerOptions,
  onPeer: (ws: WebSocketLike, peerCert: Uint8Array) => void,
) => TlsServerLike

export interface AgentListenerDeps {
  config: DataPlaneConfig
  mtls: MtlsVerifier
  registrar: RouteRegistrar
  onTunnel(t: AgentTunnel): void
  // CA bundle bytes (from config.agentCaCertPath/agentCaChainPath); injected for testability.
  caBundle: readonly Uint8Array[]
  // Optional seams (default: real setInterval; no TLS server started in unit tests).
  tlsServerFactory?: TlsServerFactory
  schedule?: (fn: () => void, ms: number) => ScheduleHandle
  onError?: (e: unknown) => void
}

export interface AgentListener {
  attach(ws: WebSocketLike, peerCert: Uint8Array): void
  tunnels(): ReadonlyMap<string, AgentTunnel>
  close(): void
}

function defaultSchedule(fn: () => void, ms: number): ScheduleHandle {
  const id = setInterval(fn, ms)
  id.unref?.()
  return { cancel: () => clearInterval(id) }
}

export function createAgentListener(deps: AgentListenerDeps): AgentListener {
  const schedule = deps.schedule ?? defaultSchedule
  const onError = deps.onError ?? (() => {})
  const tunnels = new Map<string, AgentTunnel>()

  function runAsync(p: Promise<void>): void {
    p.catch((e: unknown) => onError(e)) // boundary: route errors to onError, never swallow silently
  }

  // TLS layer: presentation enforced HERE (requestCert + rejectUnauthorized). attach() is only ever
  // reached for a peer whose cert already chained to our CA.
  const server = deps.tlsServerFactory?.(
    { requestCert: true, rejectUnauthorized: true, ca: deps.caBundle },
    (ws, peerCert) => attach(ws, peerCert),
  )

  function attach(ws: WebSocketLike, peerCert: Uint8Array): void {
    const verified = deps.mtls.verifyPeer(peerCert) // app-layer bind (INV14) — after CA validation
    if (verified === null) {
      ws.close(4401) // cert chains to our CA but pubkey not in the host registry → no route
      return
    }
    const { hostId, accountId } = verified

    // Agent reconnected on this node → replace the prior tunnel (latest wins).
    tunnels.get(hostId)?.closeTunnel()

    const session: MuxSession = createMuxSession({
      role: 'relay',
      sendWire: (frame) => ws.send(frame),
      onOpen: () => {}, // relay never receives OPEN
      onData: () => {}, // inbound routed per-stream via the handle (relay-node subscribes)
      onClose: () => {},
      onDead: () => teardown(),
      maxFrameBytes: deps.config.maxFrameBytes,
      initialWindowBytes: deps.config.initialWindowBytes,
    })

    let hbHandle: ScheduleHandle | null = schedule(() => {
      runAsync(deps.registrar.heartbeat(hostId))
    }, deps.config.heartbeatIntervalMs)

    let torn = false
    function teardown(): void {
      if (torn) return
      torn = true
      hbHandle?.cancel()
      hbHandle = null
      session.close()
      ws.close()
      tunnels.delete(hostId)
      runAsync(deps.registrar.deregister(hostId)) // stateless: ingress stops routing here (INV7)
    }

    ws.onMessage((bytes) => session.onWire(bytes))
    ws.onClose(() => teardown())

    const tunnel: AgentTunnel = { hostId, accountId, session, closeTunnel: teardown }
    tunnels.set(hostId, tunnel)
    runAsync(deps.registrar.register(hostId, deps.config.relayNodeId, deps.config.routeTtlMs))
    deps.onTunnel(tunnel)
  }

  return {
    attach,
    tunnels: () => tunnels,
    close: () => {
      for (const t of [...tunnels.values()]) t.closeTunnel()
      server?.close()
    },
  }
}
