/**
 * B4 · relay-run revocation subscriber (INV12 data-plane teardown). Bridges the frozen §4.2
 * `relay:revocations` Redis pub/sub bus onto the running relay node so a revocation tears the live
 * tunnel(s) down within the INV12 budget.
 *
 * Flow: an injected ioredis subscriber-mode client SUBSCRIBEs the one named channel; each message is
 * validated as a `KillSignal` with the relay-contracts schema (malformed → dropped + counted, never
 * a teardown and never a bus crash — a poisoned message can neither kill the bus nor fire a spurious
 * kill); then for every live tunnel THIS node serves the pure relay-auth predicate
 * `killsScope(signal, hostAccountId, hostId)` decides coverage and, on a match, the whole host is
 * torn down (revoked ⇒ grace forced to 0 upstream in T11).
 *
 * Reuse, don't re-derive: scope math is `killsScope` (relay-auth) and validation is `KillSignalSchema`
 * (relay-contracts) — this module only wires those pure primitives to a concrete ioredis client and
 * the relay node. Security: scope selection is blast-radius-bounded (host ⊂ account ⊂ global); a host
 * this node does not serve is a no-op (no cross-tenant reach). `signal.reason` is metadata only and is
 * never written as terminal payload or log content (INV10). The owning `accountId` used for
 * account-scope fan-out comes only from the authenticated tunnel (INV3).
 */
import { KillSignalSchema, RELAY_REVOCATIONS_CHANNEL, type KillSignal } from 'relay-contracts'
import { killsScope } from 'relay-auth'

/** One live tunnel this node currently serves: the host and the account that owns it. Both are
 *  authenticated material carried on the tunnel (INV3) — never derived from the untrusted signal. */
export interface ActiveTunnelRef {
  readonly hostId: string
  readonly accountId: string
}

/**
 * What the subscriber needs from the running relay node: enumerate the tunnels it serves and tear a
 * whole host's live streams/devices down (INV12). Kept narrow + injectable so the Phase-1 entry (B5)
 * adapts the real data-plane {node, listener}: `activeTunnels` ← `listener.tunnels()` values,
 * `closeStream(hostId)` ← `node.closeTunnel(hostId)` (the whole-host revocation lever).
 */
export interface RevocableNode {
  /** Snapshot-friendly view of the tunnels currently attached to this node. */
  activeTunnels(): Iterable<ActiveTunnelRef>
  /** Immediately tear down every live stream/device on `hostId` (whole-host revocation, INV12). */
  closeStream(hostId: string): void
}

/**
 * Minimal structural view of an ioredis client in subscriber mode — only the members used here — so
 * this module carries no hard dependency on ioredis and stays unit-testable with a fake. A real
 * ioredis `Redis` instance satisfies this shape.
 */
export interface RedisSubscriber {
  subscribe(channel: string): Promise<unknown>
  unsubscribe(channel: string): Promise<unknown>
  on(event: 'message', listener: (channel: string, message: string) => void): unknown
  off(event: 'message', listener: (channel: string, message: string) => void): unknown
}

export interface RevocationSubscriberDeps {
  readonly redisSubscriber: RedisSubscriber
  readonly node: RevocableNode
  /** Observability (metadata only, INV10): a valid signal was applied to `hostsAffected` hosts here. */
  readonly onApplied?: (signal: KillSignal, hostsAffected: number) => void
  /** Malformed-message counter — a dropped signal never fires a teardown (INV12 safety). */
  readonly onDropped?: () => void
  /** Boundary error routing for subscribe/unsubscribe failures — never silently swallowed. */
  readonly onError?: (error: unknown) => void
}

export interface RevocationSubscription {
  close(): void
}

function parseKillSignal(raw: string): KillSignal | null {
  let json: unknown
  try {
    json = JSON.parse(raw)
  } catch {
    return null
  }
  const result = KillSignalSchema.safeParse(json)
  return result.success ? result.data : null
}

export function startRevocationSubscriber(deps: RevocationSubscriberDeps): RevocationSubscription {
  const { redisSubscriber, node, onApplied, onDropped, onError } = deps

  const onMessage = (channel: string, message: string): void => {
    if (channel !== RELAY_REVOCATIONS_CHANNEL) return // this client may also carry other channels
    const signal = parseKillSignal(message)
    if (signal === null) {
      onDropped?.() // dropped + counted; bus stays alive, no teardown fired
      return
    }
    // Snapshot first: closeStream tears down (mutates) the node's live-tunnel set as we iterate.
    const tunnels = [...node.activeTunnels()]
    let hostsAffected = 0
    for (const { hostId, accountId } of tunnels) {
      if (!killsScope(signal, accountId, hostId)) continue // blast-radius bounded; unrelated host = no-op
      node.closeStream(hostId)
      hostsAffected += 1
    }
    onApplied?.(signal, hostsAffected)
  }

  redisSubscriber.on('message', onMessage)
  // Fire the SUBSCRIBE; route a rejected subscribe to onError (boundary I/O — never swallowed).
  Promise.resolve(redisSubscriber.subscribe(RELAY_REVOCATIONS_CHANNEL)).catch((error: unknown) => {
    onError?.(error)
  })

  let closed = false
  return {
    close(): void {
      if (closed) return // idempotent teardown
      closed = true
      redisSubscriber.off('message', onMessage)
      Promise.resolve(redisSubscriber.unsubscribe(RELAY_REVOCATIONS_CHANNEL)).catch(
        (error: unknown) => {
          onError?.(error)
        },
      )
    },
  }
}
