import { spawn } from "node:child_process";
import { createServer as createNetServer } from "node:net";
import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync } from "node:fs";
import { join } from "node:path";
import { getConfig } from "../config.js";
import { createLogger } from "../logger.js";
import { venvBinPath } from "../python-env.js";
const log = createLogger("searxng");
const PORT_RANGE_START = 8888;
const PORT_RANGE_END = 8899;
const HEALTH_TIMEOUT_MS = 1e4;
const HEALTH_POLL_MS = 500;
const MAX_CRASH_RESTARTS = 3;
const CRASH_WINDOW_MS = 6e4;
const SHUTDOWN_GRACE_MS = 5e3;
function findAvailablePort(startPort) {
  return new Promise((resolve, reject) => {
    let port = startPort;
    function tryPort() {
      if (port > PORT_RANGE_END) {
        reject(new Error(`No available port in range ${PORT_RANGE_START}-${PORT_RANGE_END}`));
        return;
      }
      const server = createNetServer();
      server.on("error", () => {
        port++;
        tryPort();
      });
      server.listen(port, () => {
        const addr = server.address();
        const foundPort = typeof addr === "object" && addr ? addr.port : port;
        server.close(() => resolve(foundPort));
      });
    }
    tryPort();
  });
}
function isProcessAlive(pid) {
  try {
    process.kill(pid, 0);
    return true;
  } catch {
    return false;
  }
}
function tryUnlink(path) {
  try {
    unlinkSync(path);
  } catch (err) {
    const code = err.code;
    if (code !== "ENOENT") {
      log.debug("unable to remove file", { path, code: code ?? "unknown" });
    }
  }
}
function acquireLock(dataDir) {
  const lockFile = join(dataDir, "searxng.lock");
  if (existsSync(lockFile)) {
    try {
      const data = JSON.parse(readFileSync(lockFile, "utf-8"));
      if (data.pid && isProcessAlive(data.pid)) {
        return { acquired: false, existingPid: data.pid, existingPort: data.port };
      }
      log.info("cleaning stale lock file", { stalePid: data.pid });
      tryUnlink(lockFile);
    } catch {
      tryUnlink(lockFile);
    }
  }
  mkdirSync(dataDir, { recursive: true });
  writeFileSync(lockFile, JSON.stringify({ pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() }));
  return { acquired: true };
}
function releaseLock(dataDir) {
  const lockFile = join(dataDir, "searxng.lock");
  if (existsSync(lockFile)) tryUnlink(lockFile);
  const portFile = join(dataDir, "searxng.port");
  if (existsSync(portFile)) tryUnlink(portFile);
}
async function waitForHealth(url, timeoutMs) {
  const start = Date.now();
  while (Date.now() - start < timeoutMs) {
    try {
      const response = await fetch(`${url}/healthz`, { signal: AbortSignal.timeout(2e3) });
      if (response.ok) return true;
    } catch {
    }
    await new Promise((r) => setTimeout(r, HEALTH_POLL_MS));
  }
  return false;
}
class SearxngProcess {
  constructor(searxngPath, dataDir, callbacks = {}) {
    this.searxngPath = searxngPath;
    this.dataDir = dataDir;
    this.callbacks = callbacks;
  }
  searxngPath;
  dataDir;
  callbacks;
  child = null;
  port = null;
  crashTimes = [];
  stopped = false;
  isCurrentlyUnhealthy = false;
  healthProbeFailures = 0;
  healthProbeTimer = null;
  getUrl() {
    return this.port ? `http://127.0.0.1:${this.port}` : null;
  }
  async start() {
    const config = getConfig();
    const lock = acquireLock(this.dataDir);
    if (!lock.acquired) {
      if (lock.existingPort) {
        log.info("connecting to existing SearXNG instance", { port: lock.existingPort });
        this.port = lock.existingPort;
        return this.getUrl();
      }
      log.warn("could not acquire lock", { existingPid: lock.existingPid });
      return null;
    }
    try {
      this.port = await findAvailablePort(config.searxngPort);
    } catch (err) {
      log.error("no available port", { error: String(err) });
      releaseLock(this.dataDir);
      return null;
    }
    const settingsPath = join(this.searxngPath, "settings.yml");
    const pythonBin = venvBinPath(this.dataDir, "python");
    if (!existsSync(pythonBin)) {
      log.error("SearXNG python not found \u2014 run warmup first", { path: pythonBin });
      releaseLock(this.dataDir);
      return null;
    }
    this.child = spawn(pythonBin, ["-m", "searx.webapp"], {
      env: { ...process.env, SEARXNG_SETTINGS_PATH: settingsPath },
      stdio: ["ignore", "ignore", "pipe"]
    });
    this.child.on("error", (err) => {
      log.error("SearXNG spawn error", { error: String(err) });
    });
    let stderrTail = "";
    this.child.stderr?.on("data", (chunk) => {
      stderrTail = (stderrTail + chunk.toString("utf8")).slice(-4096);
    });
    this.child.on("exit", (code) => {
      if (code !== 0 && code !== null && stderrTail) {
        log.warn("SearXNG stderr tail", { code, stderrTail: stderrTail.slice(-1024) });
      }
    });
    writeFileSync(join(this.dataDir, "searxng.port"), String(this.port));
    writeFileSync(
      join(this.dataDir, "searxng.lock"),
      JSON.stringify({ pid: process.pid, port: this.port, startedAt: (/* @__PURE__ */ new Date()).toISOString() })
    );
    const url = this.getUrl();
    const healthy = await waitForHealth(url, HEALTH_TIMEOUT_MS);
    if (!healthy) {
      log.error("SearXNG failed to start within timeout");
      await this.stop();
      return null;
    }
    this.monitorCrashes();
    this.startHealthProbe();
    log.info("SearXNG started", { port: this.port, url });
    return url;
  }
  monitorCrashes() {
    if (!this.child) return;
    this.child.on("exit", (code) => {
      if (this.stopped) return;
      log.warn("SearXNG process exited unexpectedly", { code });
      this.crashTimes.push(Date.now());
      const cutoff = Date.now() - CRASH_WINDOW_MS;
      this.crashTimes = this.crashTimes.filter((t) => t > cutoff);
      if (this.crashTimes.length >= MAX_CRASH_RESTARTS) {
        log.error("too many crashes, giving up on SearXNG", { crashes: this.crashTimes.length });
        releaseLock(this.dataDir);
        if (!this.isCurrentlyUnhealthy) {
          this.isCurrentlyUnhealthy = true;
          this.callbacks.onUnhealthy?.(
            `SearXNG process crashed ${this.crashTimes.length} times in ${CRASH_WINDOW_MS / 1e3}s`
          );
        }
        return;
      }
      const backoff = Math.min(1e3 * Math.pow(2, this.crashTimes.length - 1), 3e4);
      log.info("restarting SearXNG after backoff", { backoffMs: backoff });
      setTimeout(() => {
        if (!this.stopped) this.start();
      }, backoff);
    });
  }
  startHealthProbe() {
    if (this.healthProbeTimer) clearInterval(this.healthProbeTimer);
    const config = getConfig();
    const intervalMs = config.healthProbeIntervalMs;
    this.healthProbeTimer = setInterval(() => {
      void this.probeOnce();
    }, intervalMs);
  }
  async probeOnce() {
    if (this.stopped || !this.port) return;
    try {
      const r = await fetch(`http://127.0.0.1:${this.port}/healthz`, { signal: AbortSignal.timeout(2e3) });
      if (r.ok) {
        this.healthProbeFailures = 0;
        if (this.isCurrentlyUnhealthy) {
          this.isCurrentlyUnhealthy = false;
          this.callbacks.onHealthy?.();
        }
      } else {
        this.notePotentialFailure();
      }
    } catch {
      this.notePotentialFailure();
    }
  }
  notePotentialFailure() {
    this.healthProbeFailures++;
    if (this.healthProbeFailures === 3 && !this.isCurrentlyUnhealthy) {
      this.isCurrentlyUnhealthy = true;
      this.callbacks.onUnhealthy?.("SearXNG /healthz unreachable for 3 consecutive probes");
    }
  }
  async stop() {
    if (this.healthProbeTimer) {
      clearInterval(this.healthProbeTimer);
      this.healthProbeTimer = null;
    }
    this.stopped = true;
    if (this.child) {
      this.child.kill("SIGTERM");
      await Promise.race([
        new Promise((resolve) => this.child.on("exit", resolve)),
        new Promise((resolve) => setTimeout(() => {
          this.child?.kill("SIGKILL");
          resolve();
        }, SHUTDOWN_GRACE_MS))
      ]);
      this.child = null;
    }
    releaseLock(this.dataDir);
    this.port = null;
  }
}
export {
  SearxngProcess,
  acquireLock,
  findAvailablePort,
  isProcessAlive,
  releaseLock
};
//# sourceMappingURL=process.js.map