/**
 * Native-tunnel frpc supervisor — TASK B4/H4 (PLAN_TUNNEL_AUTOMATION §3.2).
 *
 * Spawns the pinned `frpc` child with the written `frpc.toml` and keeps it running: on every exit
 * it restarts the child after an exponential backoff (REUSES `transport/backoff.ts` — the same
 * 1s/2s/4s…cap-30s policy as the relay reconnect). A child that ran longer than a stability window
 * resets the backoff, so a healthy long-lived tunnel that finally dies restarts fast, while a
 * crash-loop stays capped at 30s. All IO — spawn, sleep, clock — is injected so the loop is
 * fully offline-testable (no real frpc / no real timers). No `console.log` (INV9 redacting logger).
 *
 * SCOPE (deferral #11): the real `frpc` binary run is pending B3 tar.gz extraction; the default
 * `spawn` shells out to the provisioned binary, but tests inject a fake child so nothing executes.
 *
 * LOG CAPTURE (B4/H4 wiring fix): when a `logFile` is supplied, the default spawn tees the frpc
 * child's stdout+stderr into that file (truncated per (re)spawn) so the health probe's log scanner
 * (`readFrpcLog` → `frpcProxyStarted`) has real content to match "start proxy success" against.
 * Truncate-per-spawn keeps the file bounded to the CURRENT child and free of a stale success line
 * from a dead one — a freshly connected frpc always re-logs the success line.
 */
import { spawn as nodeSpawn } from 'node:child_process'
import { createWriteStream, mkdirSync } from 'node:fs'
import { dirname } from 'node:path'
import { createBackoff, type BackoffPolicy, type Sleep } from './backoff.js'
import { createLogger, type Logger } from '../log/logger.js'

/** A child process seam the supervisor drives (a fake child in tests; a real frpc in prod). */
export interface FrpcChild {
  /** Register a one-shot exit handler (`null` code ⇒ killed by signal or spawn error). */
  onExit(cb: (code: number | null) => void): void
  /** Whether the child is still running (feeds the health probe's `isFrpcAlive`). */
  isAlive(): boolean
  /** Request termination. */
  kill(): void
}

/** Spawn a frpc child running `frpc -c <tomlPath>`. */
export type SpawnFrpc = (binPath: string, tomlPath: string) => FrpcChild

/** Injectable seams for the supervisor; unset fields default to real spawn/sleep/clock/logger. */
export interface FrpSuperviseDeps {
  spawn: SpawnFrpc
  backoff: BackoffPolicy
  sleep: Sleep
  logger: Logger
  now: () => number
}

/**
 * Supervisor overrides: any `FrpSuperviseDeps` seam plus an optional `logFile`. When `logFile` is
 * set and no explicit `spawn` is given, the default spawn tees frpc's stdout/stderr into that file
 * so the health probe can scan it. An explicit `spawn` (tests) always wins over `logFile`.
 */
export interface FrpSuperviseOptions extends Partial<FrpSuperviseDeps> {
  readonly logFile?: string
}

/** Handle returned by `superviseFrpc`: stop the loop, or await its terminal exit code. */
export interface FrpSuperviseHandle {
  /** Request graceful shutdown (kills the child); resolves once the loop has fully stopped. */
  stop(): Promise<void>
  /** Resolves with an exit code (always 0 — a stopped supervisor is a clean shutdown). */
  readonly done: Promise<number>
  /** True while a frpc child is currently running (for the health probe). */
  isChildAlive(): boolean
  /**
   * Kill the current child WITHOUT stopping the supervisor (A5): the restart-on-exit loop respawns a
   * fresh frpc that re-reads the (now rotated) cert/key/CA files. A no-op once `stop()` was called —
   * it must never resurrect a supervisor that is shutting down.
   */
  restartChild(): void
}

/** A frpc run lasting at least this long is "stable" ⇒ reset the restart backoff. */
export const STABLE_RUN_MS = 60_000

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

/**
 * Default spawn (no log capture): launch the provisioned frpc binary, discarding its stdout/stderr.
 * `stdio: 'ignore'` (not `'pipe'`) is deliberate — an unconsumed pipe fills its OS buffer (~64KB) and
 * then BLOCKS the child. Log capture is opt-in via `createFileLoggingSpawn` (see `logFile`).
 */
const realSpawn: SpawnFrpc = (binPath, tomlPath) => {
  const cp = nodeSpawn(binPath, ['-c', tomlPath], { stdio: ['ignore', 'ignore', 'ignore'] })
  let alive = true
  return {
    onExit(cb: (code: number | null) => void): void {
      cp.once('exit', (code) => {
        alive = false
        cb(code)
      })
      cp.once('error', () => {
        alive = false
        cb(null)
      })
    },
    isAlive: () => alive,
    kill: () => {
      cp.kill()
    },
  }
}

/**
 * Spawn frpc and TEE its stdout+stderr into `logFile` (truncated per spawn) so the health probe's
 * `readFrpcLog` scanner has real content. A log-write failure is swallowed — persisting the log for
 * the probe must never crash the supervised tunnel.
 */
export function createFileLoggingSpawn(logFile: string): SpawnFrpc {
  return (binPath, tomlPath) => {
    mkdirSync(dirname(logFile), { recursive: true })
    const cp = nodeSpawn(binPath, ['-c', tomlPath], { stdio: ['ignore', 'pipe', 'pipe'] })
    // flags:'w' truncates so the file reflects only the current child (no stale success line).
    const log = createWriteStream(logFile, { flags: 'w' })
    log.on('error', () => {
      /* a log-write failure is non-fatal to the tunnel; the probe just sees no success line */
    })
    cp.stdout?.pipe(log, { end: false })
    cp.stderr?.pipe(log, { end: false })
    let alive = true
    const closeLog = (): void => {
      log.end()
    }
    return {
      onExit(cb: (code: number | null) => void): void {
        cp.once('exit', (code) => {
          alive = false
          closeLog()
          cb(code)
        })
        cp.once('error', () => {
          alive = false
          closeLog()
          cb(null)
        })
      },
      isAlive: () => alive,
      kill: () => {
        cp.kill()
      },
    }
  }
}

function resolveDeps(o?: FrpSuperviseOptions): FrpSuperviseDeps {
  const defaultSpawn = o?.logFile !== undefined ? createFileLoggingSpawn(o.logFile) : realSpawn
  return {
    spawn: o?.spawn ?? defaultSpawn,
    backoff: o?.backoff ?? createBackoff({ jitter: true }),
    sleep: o?.sleep ?? realSleep,
    logger: o?.logger ?? createLogger('info'),
    now: o?.now ?? Date.now,
  }
}

/**
 * Start supervising frpc. Returns immediately with a handle; the restart loop runs in the
 * background. `stop()` sets the stop flag, kills the live child, and awaits loop termination.
 */
export function superviseFrpc(
  binPath: string,
  tomlPath: string,
  overrides?: FrpSuperviseOptions,
): FrpSuperviseHandle {
  const { spawn, backoff, sleep, logger, now } = resolveDeps(overrides)

  let stopped = false
  let child: FrpcChild | null = null

  /** Spawn one frpc child and resolve when it exits. */
  function runOnce(): Promise<number | null> {
    return new Promise<number | null>((resolve) => {
      const c = spawn(binPath, tomlPath)
      child = c
      c.onExit((code) => {
        child = null
        resolve(code)
      })
    })
  }

  async function loop(): Promise<number> {
    while (!stopped) {
      const startedAt = now()
      const exitCode = await runOnce()
      if (stopped) break
      if (now() - startedAt >= STABLE_RUN_MS) backoff.reset()
      const delayMs = backoff.nextDelayMs()
      logger.log('warn', 'frpc exited — restarting after backoff', { exitCode, delayMs })
      await sleep(delayMs)
    }
    return 0
  }

  const done = loop()

  return {
    async stop(): Promise<void> {
      stopped = true
      child?.kill()
      await done
    },
    done,
    isChildAlive: () => child?.isAlive() ?? false,
    restartChild(): void {
      if (!stopped) child?.kill()
    },
  }
}
