/**
 * T10 — relay-node registry + graceful drain (INV7). Every mutation is scoped to the caller's
 * authenticated `NodeIdentity`: a node can only register/heartbeat/drain ITSELF. Drain marks the
 * node 'draining', enumerates its live routes, and PUBLISHES a per-host `KillSignal` on the frozen
 * `relay:revocations` bus (INDEX §4.2 FIX 4 — SAME channel T13 revoke uses); P1 nodes translate it
 * into the §4.1 GOAWAY. This module does NOT encode the frame (that is P1). Drain touches NO
 * Postgres host/session row — the running task lives on the customer machine (PTY survives).
 *
 * OQ4 residual: the frozen RevocationScope has no `node` kind, so a single-node operator drain is
 * expressed as per-host KillSignals with a drain reason (flagged to the INDEX for a future scope).
 */
import type { RevocationBus } from 'relay-contracts'
import type { NodeStore, RouteStore } from '../store/ports.js'
import type { NodeIdentity } from '../node-auth/identity.js'
import type { AuditWriter } from '../audit/log.js'
import { noopAuditWriter } from '../audit/log.js'
import { nowIso } from '../util/ids.js'

export const DRAIN_REASON = 'operatorDrain'

export interface NodeCoordinator {
  registerNode(caller: NodeIdentity, addr: string): Promise<void>
  nodeHeartbeat(caller: NodeIdentity): Promise<void>
  beginDrain(caller: NodeIdentity): Promise<{ readonly hostIds: readonly string[] }>
  completeDrain(caller: NodeIdentity): Promise<void>
}

export interface NodeCoordinatorDeps {
  readonly nodes: NodeStore
  readonly routes: RouteStore
  readonly bus: RevocationBus
  readonly audit?: AuditWriter
}

export function createNodeCoordinator(deps: NodeCoordinatorDeps): NodeCoordinator {
  const audit = deps.audit ?? noopAuditWriter()
  return {
    async registerNode(caller, addr) {
      await deps.nodes.upsert({ nodeId: caller.nodeId, addr, status: 'active', lastSeen: nowIso() })
    },
    async nodeHeartbeat(caller) {
      await deps.nodes.touch(caller.nodeId, nowIso())
    },
    async beginDrain(caller) {
      await deps.nodes.setStatus(caller.nodeId, 'draining')
      const routed = await deps.routes.listByNode(caller.nodeId)
      const at = Math.floor(Date.now() / 1000)
      for (const { hostId } of routed) {
        // Per-host KillSignal with a drain reason on the frozen bus (P1 emits GOAWAY).
        // eslint-disable-next-line no-await-in-loop
        await deps.bus.publish({ scope: { kind: 'host', hostId }, at, reason: DRAIN_REASON })
      }
      await audit.writeAuditEvent({
        action: 'node.drain',
        principalId: `node:${caller.nodeId}`,
        accountId: 'system',
        hostId: null,
        ts: nowIso(),
        meta: { nodeId: caller.nodeId, routes: String(routed.length) },
      })
      return { hostIds: routed.map((r) => r.hostId) }
    },
    async completeDrain(caller) {
      await deps.nodes.delete(caller.nodeId) // deregister after routes re-homed
    },
  }
}
