/**
 * §4.1 heartbeat — PLAN_RELAY_AGENT T9. PING every 15s on streamId 0; a missed PONG within the
 * interval ⇒ the tunnel is dead (⇒ T10 reconnect). Also replies PONG (echoing the 8-byte token)
 * to an inbound PING. Timers are injectable (TimerLike) for deterministic fake-timer tests.
 */
import { randomBytes } from 'node:crypto'
import type { TimerLike } from './seams.js'
import { pingHeader, pongHeader, type Tunnel } from './tunnel.js'

export const HEARTBEAT_INTERVAL_MS = 15_000

export interface Heartbeat {
  onPing(token: Uint8Array): void
  onPong(token: Uint8Array): void
  start(): void
  stop(): void
  onDead(cb: () => void): void
}

const realTimer: TimerLike = {
  setTimeout: (cb, ms) => setTimeout(cb, ms),
  clearTimeout: (h) => clearTimeout(h as ReturnType<typeof setTimeout>),
  setInterval: (cb, ms) => setInterval(cb, ms),
  clearInterval: (h) => clearInterval(h as ReturnType<typeof setInterval>),
}

export function createHeartbeat(
  tunnel: Tunnel,
  opts: { intervalMs?: number; timer?: TimerLike; genToken?: () => Uint8Array } = {},
): Heartbeat {
  const intervalMs = opts.intervalMs ?? HEARTBEAT_INTERVAL_MS
  const timer = opts.timer ?? realTimer
  const genToken = opts.genToken ?? (() => new Uint8Array(randomBytes(8)))

  let interval: unknown = null
  let deadline: unknown = null
  let pending = false
  let deadCb: (() => void) | null = null
  let dead = false

  function fireDead(): void {
    if (dead) return
    dead = true
    stop()
    deadCb?.()
  }

  function sendPing(): void {
    pending = true
    tunnel.send(pingHeader(), genToken())
    deadline = timer.setTimeout(() => {
      if (pending) fireDead()
    }, intervalMs)
  }

  function stop(): void {
    if (interval !== null) timer.clearInterval(interval)
    if (deadline !== null) timer.clearTimeout(deadline)
    interval = null
    deadline = null
  }

  return {
    onPing(token: Uint8Array): void {
      tunnel.send(pongHeader(), token) // echo the token byte-exact
    },
    onPong(): void {
      pending = false
      if (deadline !== null) {
        timer.clearTimeout(deadline)
        deadline = null
      }
    },
    start(): void {
      dead = false
      sendPing()
      interval = timer.setInterval(sendPing, intervalMs)
    },
    stop,
    onDead(cb: () => void): void {
      deadCb = cb
    },
  }
}
