/**
 * T5 · Heartbeat / liveness (§4.1) — 15s PING/PONG with an injectable timer.
 *
 * PING carries a fresh 8-byte crypto-random token every interval; the matching PONG must echo
 * the EXACT outstanding token to reset the miss counter. An unmatched/stale/replayed PONG is
 * ignored (INV13-adjacent: a replayed PONG can't keep a dead/hijacked tunnel "alive").
 * `HEARTBEAT_MISS_LIMIT` consecutive missed PONGs (~45s) ⇒ `onDead` fires exactly once.
 */
import { randomBytes } from 'node:crypto'

export const HEARTBEAT_INTERVAL_MS = 15_000
export const HEARTBEAT_MISS_LIMIT = 3
const TOKEN_BYTES = 8

export interface ScheduleHandle {
  cancel(): void
}

export interface HeartbeatDeps {
  sendPing(token: Uint8Array): void
  onDead(): void
  now?: () => number
  schedule?: (fn: () => void, ms: number) => ScheduleHandle
  intervalMs?: number
}

export interface Heartbeat {
  start(): void
  stop(): void
  onPong(token: Uint8Array): void
}

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

function bytesEqual(a: Uint8Array, b: Uint8Array): boolean {
  if (a.length !== b.length) return false
  for (let i = 0; i < a.length; i++) {
    if (a[i] !== b[i]) return false
  }
  return true
}

export function createHeartbeat(deps: HeartbeatDeps): Heartbeat {
  const schedule = deps.schedule ?? defaultSchedule
  const intervalMs = deps.intervalMs ?? HEARTBEAT_INTERVAL_MS

  let handle: ScheduleHandle | null = null
  let outstanding: Uint8Array | null = null // token awaiting a PONG, or null once matched
  let misses = 0
  let dead = false

  function tick(): void {
    if (dead) return
    // If the previous PING was never answered, count a miss.
    if (outstanding !== null) {
      misses += 1
      if (misses >= HEARTBEAT_MISS_LIMIT) {
        dead = true
        stop()
        deps.onDead()
        return
      }
    }
    const token = new Uint8Array(randomBytes(TOKEN_BYTES))
    outstanding = token
    deps.sendPing(token)
  }

  function stop(): void {
    if (handle !== null) {
      handle.cancel()
      handle = null
    }
  }

  return {
    start() {
      if (handle !== null || dead) return
      // Send the first PING immediately, then on every interval.
      tick()
      handle = schedule(tick, intervalMs)
    },
    stop,
    onPong(token) {
      if (dead || outstanding === null) return
      if (bytesEqual(token, outstanding)) {
        outstanding = null
        misses = 0
      }
      // A stale/unknown token is ignored — it does NOT reset the miss counter.
    },
  }
}
