/**
 * Reconnection / backoff — PLAN_RELAY_AGENT T10. REUSES the base app's 1/2/4…cap-30s policy
 * (EXPLORE §3). `reconnectLoop` only CONSUMES an injected `isRevoked()` (the W0 seam) — a revoked
 * host short-circuits and never reconnects (INV12). No task edge to T14.
 */
import type { Tunnel } from './tunnel.js'

export const BACKOFF_BASE_MS = 1_000
export const BACKOFF_CAP_MS = 30_000

export interface BackoffPolicy {
  nextDelayMs(): number
  reset(): void
}

/** Exponential backoff 1s,2s,4s…capped at 30s, optional [0.5×,1×] jitter. */
export function createBackoff(
  opts: { baseMs?: number; capMs?: number; jitter?: boolean; rng?: () => number } = {},
): BackoffPolicy {
  const baseMs = opts.baseMs ?? BACKOFF_BASE_MS
  const capMs = opts.capMs ?? BACKOFF_CAP_MS
  const jitter = opts.jitter ?? false
  const rng = opts.rng ?? Math.random
  let attempt = 0
  return {
    nextDelayMs(): number {
      const raw = Math.min(baseMs * 2 ** attempt, capMs)
      attempt += 1
      if (!jitter) return raw
      return Math.round(raw * (0.5 + rng() * 0.5)) // [0.5×, 1×]
    },
    reset(): void {
      attempt = 0
    },
  }
}

export type Sleep = (ms: number) => Promise<void>
const realSleep: Sleep = (ms) => new Promise((r) => setTimeout(r, ms))

/**
 * Dial with backoff until success (resolves the connected Tunnel) or the host is revoked
 * (resolves null — never reconnect). Each dial failure waits `backoff.nextDelayMs()`; a success
 * resets the backoff.
 */
export async function reconnectLoop(
  dial: () => Promise<Tunnel>,
  backoff: BackoffPolicy,
  isRevoked: () => boolean,
  sleep: Sleep = realSleep,
): Promise<Tunnel | null> {
  while (!isRevoked()) {
    try {
      const tunnel = await dial()
      backoff.reset()
      return tunnel
    } catch {
      if (isRevoked()) return null
      await sleep(backoff.nextDelayMs())
    }
  }
  return null
}
