/**
 * T9 — Redis routing table (`route:{host_id}`, heartbeat-TTL, INV7). Redis holds only LOCATION,
 * never ownership or secrets; a missing/expired key fails CLOSED (host treated offline). Every
 * mutation is scoped to the AUTHENTICATED node identity: a node can only route hosts to ITSELF
 * (`entry.relayNodeId === caller.nodeId`) and can only heartbeat/drop a route it currently holds.
 */
import type { RouteEntry } from '../model/records.js'
import type { NodeStatus, RouteStore } from '../store/ports.js'
import type { NodeIdentity } from '../node-auth/identity.js'

export class RouteAuthError extends Error {}

export interface RoutingTable {
  upsertRoute(caller: NodeIdentity, hostId: string, entry: RouteEntry, ttlSec: number): Promise<void>
  heartbeatRoute(caller: NodeIdentity, hostId: string, ttlSec: number): Promise<void>
  resolveRoute(hostId: string): Promise<RouteEntry | null>
  dropRoute(caller: NodeIdentity, hostId: string): Promise<void>
  /** System teardown path (drain/revoke) — bypasses the holding-node check by design. */
  systemDropRoute(hostId: string): Promise<void>
}

export interface RoutingTableDeps {
  readonly routes: RouteStore
  /** Optional node-status hook: a draining node stops receiving new upserts (T10). */
  readonly nodeStatus?: (nodeId: string) => Promise<NodeStatus | null>
}

export function createRoutingTable(deps: RoutingTableDeps): RoutingTable {
  return {
    async upsertRoute(caller, hostId, entry, ttlSec) {
      // A node may ONLY route hosts to itself — never inject a route for another node's id.
      if (entry.relayNodeId !== caller.nodeId) {
        throw new RouteAuthError('entry.relayNodeId must equal the authenticated caller nodeId')
      }
      if (deps.nodeStatus !== undefined) {
        const status = await deps.nodeStatus(caller.nodeId)
        if (status === 'draining') throw new RouteAuthError('node is draining; not accepting new routes')
      }
      await deps.routes.set(hostId, entry, ttlSec)
    },
    async heartbeatRoute(caller, hostId, ttlSec) {
      const current = await deps.routes.get(hostId)
      // A stale/foreign node cannot refresh (hijack) another node's live tunnel; missing ⇒ no-op.
      if (current === null || current.relayNodeId !== caller.nodeId) return
      await deps.routes.refreshTtl(hostId, ttlSec)
    },
    async resolveRoute(hostId) {
      return deps.routes.get(hostId) // null ⇒ offline (fails closed, INV7)
    },
    async dropRoute(caller, hostId) {
      const current = await deps.routes.get(hostId)
      if (current === null) return
      if (current.relayNodeId !== caller.nodeId) {
        throw new RouteAuthError('only the holding node may drop its route')
      }
      await deps.routes.delete(hostId)
    },
    async systemDropRoute(hostId) {
      await deps.routes.delete(hostId)
    },
  }
}
