/**
 * Server initialization — preloaded via `node --require ./server-init.js server.js`
 *
 * Handles four concerns that the Next.js-generated server.js does not:
 *   1. mDNS bypass: wraps http.createServer to 302-redirect *.local to LAN IP
 *   2. systemd sd_notify: sends READY=1 on startup, WATCHDOG=1 every 15s
 *   3. Graceful shutdown: drains connections on SIGTERM before exit
 *   4. Startup logging
 *
 * Framework-agnostic — survives a Next.js replacement.
 */

'use strict'

const http = require('node:http')
const { spawnSync } = require('node:child_process')
const { existsSync, readFileSync, writeFileSync } = require('node:fs')
const os = require('node:os')
const path = require('node:path')
const { monitorEventLoopDelay } = require('node:perf_hooks')

// server.log receives these lines via systemd `append:` (StandardOutput/Error),
// which bypasses journald — so the file carries no timestamp of its own. Prefix
// the restart-loop forensic lines with an ISO timestamp so loop cadence is
// reconstructable from server.log alone, without live systemd state (Task 504).
// The timestamp goes BEFORE the `[server-init]` tag so the grep substrings
// `[server-init] start` and `[server-init] SIGTERM received` stay intact.
function ts() {
  return new Date().toISOString()
}

// ---------------------------------------------------------------------------
// 0. Start counter — Task 173
// ---------------------------------------------------------------------------
//
// Boot loops in the brand process are otherwise invisible: each cycle writes
// one `[server-init] SIGTERM received` line, and the operator has to grep and
// count to know the loop exists. Increment a persistent counter at every
// fresh start and log it so a single `grep "[server-init] start"` answers
// "is the brand process in a restart loop, and how deep?"
//
// The counter lives under /tmp keyed on ACCOUNT_ID so brands on the same
// device do not collide. /tmp is cleared on reboot, which is correct
// semantics: a clean reboot starts the count fresh; a restart loop in the
// brand process keeps the same /tmp and shows count>1.

function bumpStartCounter() {
  const accountId = process.env.ACCOUNT_ID || 'default'
  const counterPath = path.join('/tmp', `server-init-${accountId}-restart.count`)
  let prior = 0
  if (existsSync(counterPath)) {
    try {
      const raw = readFileSync(counterPath, 'utf8').trim()
      const parsed = Number.parseInt(raw, 10)
      if (Number.isFinite(parsed) && parsed >= 0) prior = parsed
    } catch { /* unreadable counter — treat as 0 */ }
  }
  const next = prior + 1
  try {
    writeFileSync(counterPath, String(next), 'utf8')
  } catch (err) {
    // Counter persistence is best-effort; the start line still emits.
    console.error(`[server-init] start-counter-write-failed path=${counterPath} err=${err && err.message ? err.message : String(err)}`)
  }
  console.error(`${ts()} [server-init] start count=${next} account=${accountId} counter-path=${counterPath}`)
}

bumpStartCounter()

// ---------------------------------------------------------------------------
// 1. mDNS bypass — redirect *.local to LAN IP
// ---------------------------------------------------------------------------

function getLanIp() {
  const nets = os.networkInterfaces()
  let fallback = null
  for (const entries of Object.values(nets)) {
    if (!entries) continue
    for (const entry of entries) {
      if (entry.family !== 'IPv4' || entry.internal) continue
      if (entry.address.startsWith('192.168.') || entry.address.startsWith('10.')) {
        return entry.address
      }
      if (!fallback) fallback = entry.address
    }
  }
  return fallback
}

// Paths that must NOT be redirected (API calls, static assets, VNC)
const SKIP_REDIRECT_RE = /^\/(api|assets|brand|novnc|vnc-viewer|vnc-popout|favicon)/

const _originalCreateServer = http.createServer.bind(http)

http.createServer = function patchedCreateServer(...args) {
  const requestListener = typeof args[0] === 'function' ? args[0] : args[1]
  const opts = typeof args[0] === 'function' ? undefined : args[0]
  function wrappedListener(req, res) {
    const host = req.headers.host || ''
    if (/^[a-z0-9-]+\.local(:|$)/.test(host) && !SKIP_REDIRECT_RE.test(req.url || '')) {
      const ip = getLanIp()
      if (ip) {
        const port = host.includes(':') ? host.split(':')[1] : '19200'
        res.writeHead(302, { Location: `http://${ip}:${port}${req.url || '/'}` })
        res.end()
        return
      }
    }
    requestListener(req, res)
  }
  return opts
    ? _originalCreateServer(opts, wrappedListener)
    : _originalCreateServer(wrappedListener)
}

console.error('[server-init] mDNS bypass active (*.local → LAN IP)')

// ---------------------------------------------------------------------------
// 2. systemd sd_notify via systemd-notify command
// ---------------------------------------------------------------------------

const NOTIFY_SOCKET = process.env.NOTIFY_SOCKET

function sdNotify(message) {
  if (!NOTIFY_SOCKET) return
  try {
    spawnSync('systemd-notify', [message], { stdio: 'pipe', timeout: 3000 })
  } catch { /* non-critical */ }
}

if (NOTIFY_SOCKET) {
  // Send READY=1 once the event loop is established (next tick after server.js loads)
  setImmediate(() => {
    sdNotify('--ready')
    console.error('[server-init] sd_notify READY=1 sent')
  })

  // Task 249: WATCHDOG_USEC is injected by systemd when Type=notify +
  // WatchdogSec= is configured (microseconds, integer). Reading it at runtime
  // means a future unit-file bump (e.g. 120 → 180) propagates without code
  // changes. Ping every 15s regardless of deadline: trivially satisfies
  // systemd's "≤ half-deadline" requirement for any deadline ≥ 30s, and
  // gives the near-miss check 15s observation windows so a long block is
  // surfaced within 15s of unblocking.
  const WATCHDOG_USEC = Number.parseInt(process.env.WATCHDOG_USEC ?? '', 10)
  const watchdogDeadlineMs = Number.isFinite(WATCHDOG_USEC) && WATCHDOG_USEC > 0
    ? Math.floor(WATCHDOG_USEC / 1000)
    : null

  // monitorEventLoopDelay records every libuv tick's lag; a sync block of N
  // seconds shows up as histogram.max ≈ N*1000 ms in the cycle where the
  // block ends. When max crosses 50% of the deadline we emit one line so
  // `grep '[server-init] watchdog ping miss' server.log` answers "did we
  // approach the deadline, and how close" without needing systemd state or
  // journald. Histogram is reset every cycle so max reflects this window.
  // Only enabled when the deadline is known — no point recording samples no
  // one reads.
  const loopHist = watchdogDeadlineMs !== null ? monitorEventLoopDelay({ resolution: 50 }) : null
  if (loopHist) loopHist.enable()

  const watchdogInterval = setInterval(() => {
    sdNotify('WATCHDOG=1')
    if (loopHist && watchdogDeadlineMs !== null) {
      const maxMs = loopHist.max / 1_000_000
      loopHist.reset()
      if (maxMs >= watchdogDeadlineMs / 2) {
        console.error(`[server-init] watchdog ping miss lag-ms=${Math.round(maxMs)} deadline-ms=${watchdogDeadlineMs}`)
      }
    }
  }, 15_000)
  watchdogInterval.unref() // don't keep the process alive for this
  console.error(`[server-init] watchdog enabled (ping-interval-ms=15000 deadline-ms=${watchdogDeadlineMs ?? 'unset'})`)
} else {
  console.error('[server-init] no NOTIFY_SOCKET, watchdog disabled')
}

// ---------------------------------------------------------------------------
// 3. Graceful shutdown on SIGTERM
// ---------------------------------------------------------------------------

let shuttingDown = false

function gracefulShutdown(signal) {
  if (shuttingDown) return
  shuttingDown = true
  console.error(`${ts()} [server-init] ${signal} received, shutting down...`)
  sdNotify('STOPPING=1\nSTATUS=Draining connections')

  // Give in-flight requests 5s to complete, then exit
  setTimeout(() => {
    console.error('[server-init] drain timeout, exiting')
    process.exit(0)
  }, 5_000).unref()
}

process.on('SIGTERM', () => gracefulShutdown('SIGTERM'))
process.on('SIGINT', () => gracefulShutdown('SIGINT'))
