#!/usr/bin/env node

const { spawn, exec, execSync } = require("child_process");
const path = require("path");
const fs = require("fs");
const https = require("https");
const os = require("os");

// Native spinner - no external dependency
function createSpinner(text) {
  const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
  let i = 0;
  let interval = null;
  let currentText = text;
  return {
    start() {
      if (process.stdout.isTTY) {
        process.stdout.write(`\r${frames[0]} ${currentText}`);
        interval = setInterval(() => {
          process.stdout.write(`\r${frames[i++ % frames.length]} ${currentText}`);
        }, 80);
      }
      return this;
    },
    stop() {
      if (interval) {
        clearInterval(interval);
        interval = null;
      }
      if (process.stdout.isTTY) {
        process.stdout.write("\r\x1b[K");
      }
    },
    succeed(msg) {
      this.stop();
      console.log(`✅ ${msg}`);
    },
    fail(msg) {
      this.stop();
      console.log(`❌ ${msg}`);
    }
  };
}

const pkg = require("./package.json");
const { ensureSqliteRuntime, buildEnvWithRuntime } = require("./hooks/sqliteRuntime");
const { ensureTrayRuntime } = require("./hooks/trayRuntime");
const args = process.argv.slice(2);

// Self-heal SQLite runtime deps (sql.js + better-sqlite3) into ~/.bluerouter/runtime
// so the server can resolve them via NODE_PATH. Best-effort — sql.js is required,
// better-sqlite3 is optional. Logs to stderr only on failure.
try { ensureSqliteRuntime({ silent: true }); } catch {}

// Self-heal tray runtime (systray for macOS/Linux only). Windows skipped.
try { ensureTrayRuntime({ silent: true }); } catch {}

// Configuration constants
const APP_NAME = pkg.name; // Use from package.json
const INSTALL_CMD_LATEST = `npm i -g ${APP_NAME}@latest --prefer-online`;

const DEFAULT_PORT = 20128;
const DEFAULT_HOST = "0.0.0.0";
const DEFAULT_DATA_DIR = process.platform === "win32"
  ? path.join(process.env.PROGRAMDATA || "C:\\ProgramData", "bluerouter")
  : "/var/lib/bluerouter";
const DEFAULT_SERVICE_NAME = "bluerouter";
const MAX_PORT_ATTEMPTS = 10;
// Identifiers for killAllAppProcesses - only kill bluerouter specifically
const PROCESS_IDENTIFIERS = [
  'bluerouter'  // Only package name - avoid killing other apps
];

// Parse a CLI option from an argv array.
function getArgValue(argv, names, fallback = "") {
  for (let i = 0; i < argv.length; i++) {
    if (names.includes(argv[i])) {
      return argv[i + 1] !== undefined ? argv[i + 1] : fallback;
    }

    const matched = names.find((name) => argv[i].startsWith(`${name}=`));
    if (matched) {
      return argv[i].slice(matched.length + 1);
    }
  }

  return fallback;
}

function hasArg(argv, names) {
  return argv.some((arg) => names.includes(arg));
}

function detectServerIp() {
  try {
    const nets = os.networkInterfaces();

    for (const entries of Object.values(nets)) {
      for (const entry of entries || []) {
        if (entry.family === "IPv4" && !entry.internal) {
          return entry.address;
        }
      }
    }
  } catch {}

  return "127.0.0.1";
}

function requireRootForService() {
  if (process.platform === "win32") {
    throw new Error("install-service is currently supported on Linux systemd only.");
  }

  if (typeof process.getuid === "function" && process.getuid() !== 0) {
    throw new Error("Please run with sudo/root: sudo bluerouter install-service");
  }
}

function shellQuote(value) {
  return `'${String(value).replace(/'/g, `'\\''`)}'`;
}

function systemdEscapeEnv(value) {
  return String(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"');
}

function systemdEscapePath(value) {
  // systemd directives are not shell commands. Do not wrap paths in single
  // quotes; systemd treats the quote as part of the path and then rejects
  // WorkingDirectory as non-absolute. Escape spaces as \x20 instead.
  return String(value).replace(/ /g, "\\x20");
}

function run(cmd, options = {}) {
  return execSync(cmd, {
    stdio: options.stdio || "inherit",
    encoding: options.encoding || "utf8",
    timeout: options.timeout || 120000,
  });
}

function ensureDir(dir) {
  fs.mkdirSync(dir, { recursive: true });
}

function generateSystemApiKeyValue() {
  // Random API token used for the auto-created SYSTEM key.
  // Keep the OpenAI-like sk- prefix so it is friendly for clients/tools.
  const nodeCrypto = require("node:crypto");
  return `sk-${nodeCrypto.randomBytes(32).toString("base64url")}`;
}

function writeMitmAutostartScript({ serviceName, port, dataDir, baseUrl, systemApiKey }) {
  const scriptsDir = path.join(dataDir, "scripts");
  ensureDir(scriptsDir);
  ensureDir(path.join(dataDir, "logs"));

  const scriptPath = path.join(scriptsDir, `${serviceName}-mitm-autostart.sh`);
  const delaySeconds = process.env.MITM_AUTO_DELAY_SECONDS || "8";
  const apiKey = systemApiKey || process.env.MITM_AUTO_API_KEY || "sk_bluerouter";
  const autostartBaseUrl = baseUrl || `http://127.0.0.1:${port}`;

  const script = `#!/usr/bin/env bash
set -Eeuo pipefail

BASE_URL="\${BASE_URL:-${autostartBaseUrl}}"
PORT="\${PORT:-${port}}"
DATA_DIR="\${DATA_DIR:-${dataDir}}"
API_KEY="\${MITM_AUTO_API_KEY:-${apiKey}}"
DELAY_SECONDS="\${MITM_AUTO_DELAY_SECONDS:-${delaySeconds}}"
MAX_WAIT_SECONDS="\${MITM_AUTO_MAX_WAIT_SECONDS:-60}"

log() {
  printf '[%s] %s\\n' "$(date -Is)" "$*"
}

if [[ -n "\${DELAY_SECONDS}" && "\${DELAY_SECONDS}" != "0" ]]; then
  sleep "\${DELAY_SECONDS}" || true
fi

# Prefer localhost for service readiness; BASE_URL may point to LAN/public IP.
LOCAL_BASE_URL="http://127.0.0.1:\${PORT}"

ready=0
for i in $(seq 1 "\${MAX_WAIT_SECONDS}"); do
  if curl -fsS "\${LOCAL_BASE_URL}/api/auth/status" >/dev/null 2>&1; then
    ready=1
    break
  fi
  if curl -fsS "\${BASE_URL}/api/auth/status" >/dev/null 2>&1; then
    ready=1
    break
  fi
  sleep 1
done

if [[ "\${ready}" != "1" ]]; then
  log "Bluerouter API is not ready; skip MITM autostart"
  exit 0
fi

TOKEN="$(DATA_DIR="\${DATA_DIR}" node <<'NODE' 2>/dev/null || true
const fs = require("node:fs");
const path = require("node:path");
const os = require("node:os");
const crypto = require("node:crypto");
const { machineIdSync } = require("node-machine-id");

const DATA_DIR = process.env.DATA_DIR || (
  process.platform === "win32"
    ? path.join(process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming"), "bluerouter")
    : path.join(os.homedir(), ".bluerouter")
);
const MACHINE_ID_FILE = path.join(DATA_DIR, "machine-id");
const AUTH_DIR = path.join(DATA_DIR, "auth");
const CLI_SECRET_FILE = path.join(AUTH_DIR, "cli-secret");
const CLI_TOKEN_SALT = "9r-cli-auth";

function loadRawMachineId() {
  try {
    const raw = fs.readFileSync(MACHINE_ID_FILE, "utf8").trim();
    if (raw) return raw;
  } catch {}

  let raw = "";
  try {
    raw = machineIdSync();
  } catch {
    raw = crypto.randomUUID();
  }

  try {
    fs.mkdirSync(DATA_DIR, { recursive: true });
    fs.writeFileSync(MACHINE_ID_FILE, raw, { mode: 0o600 });
  } catch {}

  return raw;
}

function loadCliSecret() {
  try {
    const secret = fs.readFileSync(CLI_SECRET_FILE, "utf8").trim();
    if (secret) return secret;
  } catch {}

  const secret = crypto.randomBytes(32).toString("hex");

  try {
    fs.mkdirSync(AUTH_DIR, { recursive: true });
    fs.writeFileSync(CLI_SECRET_FILE, secret, { mode: 0o600 });
  } catch {}

  return secret;
}

const raw = loadRawMachineId();
const secret = loadCliSecret();
const token = crypto.createHash("sha256")
  .update(raw + CLI_TOKEN_SALT + secret)
  .digest("hex")
  .substring(0, 16);

process.stdout.write(token);
NODE
)"

if [[ -z "\${TOKEN}" ]]; then
  log "Could not generate CLI token; skip MITM autostart"
  exit 0
fi

ensure_system_api_key_and_settings() {
  SYSTEM_API_KEY_NAME="\${SYSTEM_API_KEY_NAME:-SYSTEM}"
  SYSTEM_API_KEY_VALUE="\${SYSTEM_API_KEY_VALUE:-\${API_KEY}}"
  DB_FILE="\${DATA_DIR}/db/data.sqlite"

  if [[ ! -f "\${DB_FILE}" ]]; then
    log "DB file not found yet: \${DB_FILE}; skip SYSTEM API key ensure"
    return 0
  fi

  DATA_DIR="\${DATA_DIR}" DB_FILE="\${DB_FILE}" SYSTEM_API_KEY_NAME="\${SYSTEM_API_KEY_NAME}" SYSTEM_API_KEY_VALUE="\${SYSTEM_API_KEY_VALUE}" node <<'NODE' 2>/dev/null || true
const fs = require("node:fs");
const path = require("node:path");
const os = require("node:os");
const crypto = require("node:crypto");

const DATA_DIR = process.env.DATA_DIR;
const DB_FILE = process.env.DB_FILE;
const SYSTEM_API_KEY_NAME = process.env.SYSTEM_API_KEY_NAME || "SYSTEM";
const SYSTEM_API_KEY_VALUE = process.env.SYSTEM_API_KEY_VALUE;

function loadMachineId() {
  const file = path.join(DATA_DIR, "machine-id");

  try {
    const raw = fs.readFileSync(file, "utf8").trim();
    if (raw) return raw;
  } catch {}

  const raw = crypto.randomUUID();

  try {
    fs.mkdirSync(DATA_DIR, { recursive: true });
    fs.writeFileSync(file, raw, { mode: 0o600 });
  } catch {}

  return raw;
}

function loadBetterSqlite3() {
  const candidates = [
    "better-sqlite3",
    path.join(os.homedir(), ".bluerouter", "runtime", "node_modules", "better-sqlite3"),
    path.join(DATA_DIR, "runtime", "node_modules", "better-sqlite3"),
  ];

  for (const mod of candidates) {
    try {
      return require(mod);
    } catch {}
  }

  return null;
}

function parseSettings(raw) {
  try {
    return raw ? JSON.parse(raw) : {};
  } catch {
    return {};
  }
}

function ensureWithBetterSqlite3(Database) {
  const db = new Database(DB_FILE);

  db.exec([
    "CREATE TABLE IF NOT EXISTS settings (",
    "  id INTEGER PRIMARY KEY CHECK (id = 1),",
    "  data TEXT NOT NULL",
    ");",
    "CREATE TABLE IF NOT EXISTS apiKeys (",
    "  id TEXT PRIMARY KEY,",
    "  key TEXT NOT NULL UNIQUE,",
    "  name TEXT,",
    "  machineId TEXT,",
    "  isActive INTEGER DEFAULT 1,",
    "  allowedModels TEXT DEFAULT '[]',",
    "  createdAt TEXT",
    ");"
  ].join("\\n"));

  try {
    const cols = db.prepare("PRAGMA table_info(apiKeys)").all();
    if (!cols.some((col) => col.name === "allowedModels")) {
      db.exec("ALTER TABLE apiKeys ADD COLUMN allowedModels TEXT DEFAULT '[]'");
    }
  } catch {}

  const settingsRow = db.prepare("SELECT data FROM settings WHERE id = 1").get();
  const settings = parseSettings(settingsRow?.data);

  // This is the dashboard "Require API key" setting used by the API gateway.
  // Keep compatibility aliases harmlessly present for older/forked builds.
  settings.requireApiKey = true;
  settings.apiKeyRequired = true;

  db.prepare([
    "INSERT INTO settings(id, data)",
    "VALUES(1, ?)",
    "ON CONFLICT(id) DO UPDATE SET data = excluded.data"
  ].join("\\n")).run(JSON.stringify(settings));

  const existingSystem = db.prepare("SELECT id, key FROM apiKeys WHERE name = ? LIMIT 1").get(SYSTEM_API_KEY_NAME);
  const machineId = loadMachineId();
  const now = new Date().toISOString();

  if (existingSystem) {
    db.prepare("UPDATE apiKeys SET isActive = 1, machineId = COALESCE(machineId, ?) WHERE id = ?")
      .run(machineId, existingSystem.id);
    db.close();
    process.stdout.write(existingSystem.key || "");
    return;
  }

  const existingKey = db.prepare("SELECT id, key FROM apiKeys WHERE key = ? LIMIT 1").get(SYSTEM_API_KEY_VALUE);

  if (existingKey) {
    db.prepare("UPDATE apiKeys SET name = ?, machineId = COALESCE(machineId, ?), isActive = 1 WHERE id = ?")
      .run(SYSTEM_API_KEY_NAME, machineId, existingKey.id);
    db.close();
    process.stdout.write(existingKey.key || SYSTEM_API_KEY_VALUE);
    return;
  }

  const id = crypto.randomUUID();
  db.prepare([
    "INSERT INTO apiKeys(id, key, name, machineId, isActive, allowedModels, createdAt)",
    "VALUES(?, ?, ?, ?, 1, '[]', ?)"
  ].join("\\n")).run(id, SYSTEM_API_KEY_VALUE, SYSTEM_API_KEY_NAME, machineId, now);

  db.close();
  process.stdout.write(SYSTEM_API_KEY_VALUE);
}

const Database = loadBetterSqlite3();

if (!Database) {
  throw new Error("better-sqlite3 is not available");
}

ensureWithBetterSqlite3(Database);
NODE
}

ENSURED_SYSTEM_API_KEY="$(ensure_system_api_key_and_settings || true)"
if [[ -n "\${ENSURED_SYSTEM_API_KEY}" ]]; then
  API_KEY="\${ENSURED_SYSTEM_API_KEY}"
  log "SYSTEM API key is ready and Require API key is enabled"
fi

payload="$(printf '{"apiKey":"%s","forceKillPort443":false}' "\${API_KEY}")"

log "Calling MITM autostart endpoint"
curl -sS -X POST \\
  -H "x-9r-cli-token: \${TOKEN}" \\
  -H "Content-Type: application/json" \\
  -d "\${payload}" \\
  "\${LOCAL_BASE_URL}/api/cli-tools/antigravity-mitm/autostart" || true

echo
`;

  fs.writeFileSync(scriptPath, script.replace(/\r\n/g, "\n"), { encoding: "utf8", mode: 0o755 });
  try {
    fs.chmodSync(scriptPath, 0o755);
  } catch {}

  return scriptPath;
}

function writeSystemdService({
  serviceName,
  port,
  host,
  dataDir,
  baseUrl,
  remoteMitmUiHosts,
  dashboardPassword,
  showLog,
  systemApiKey,
}) {
  const nodePath = process.execPath;
  const cliPath = __filename;
  const logFlag = showLog ? "--log" : "--log";
  const serviceFile = `/etc/systemd/system/${serviceName}.service`;

  ensureDir(dataDir);
  ensureDir(path.join(dataDir, "logs"));

  const mitmAutostartScript = writeMitmAutostartScript({
    serviceName,
    port,
    dataDir,
    baseUrl,
    systemApiKey,
  });

  const envLines = [
    ["NODE_ENV", "production"],
    ["PORT", String(port)],
    ["HOSTNAME", host],
    ["DATA_DIR", dataDir],
    ["BASE_URL", baseUrl],
    ["NEXT_PUBLIC_BASE_URL", baseUrl],
    ["ENABLE_REQUEST_LOGS", "true"],
    ["REQUEST_LOG_DIR", path.join(dataDir, "logs", "requests")],
    ["ALLOW_REMOTE_MITM_UI", "true"],
    ["REMOTE_MITM_UI_HOSTS", remoteMitmUiHosts],
    ["MITM_AUTO_API_KEY", systemApiKey],
    ["MITM_AUTO_DELAY_SECONDS", "8"],
  ];

  if (dashboardPassword) {
    envLines.push(["INITIAL_PASSWORD", dashboardPassword]);
  }

  const environmentBlock = envLines
    .map(([key, value]) => `Environment="${key}=${systemdEscapeEnv(value)}"`)
    .join("\n");

  const unit = `[Unit]
Description=Bluerouter
After=network.target

[Service]
Type=simple
WorkingDirectory=${systemdEscapePath(path.dirname(cliPath))}
${environmentBlock}
ExecStart=${systemdEscapePath(nodePath)} ${systemdEscapePath(cliPath)} --skip-update --no-browser ${logFlag} --port ${port} --host ${host}
ExecStartPost=/bin/bash ${systemdEscapePath(mitmAutostartScript)}
Restart=always
RestartSec=5
KillSignal=SIGTERM
TimeoutStopSec=20

[Install]
WantedBy=multi-user.target
`;

  fs.writeFileSync(serviceFile, unit, "utf8");

  return serviceFile;
}

function printServiceHelp() {
  console.log(`
Usage:
  bluerouter install-service [options]
  bluerouter uninstall-service [options]
  bluerouter service-status [options]

Linux/systemd service options:
  --service-name <name>       systemd service name (default: ${DEFAULT_SERVICE_NAME})
  -p, --port <port>           Port to run the server (default: ${DEFAULT_PORT})
  -H, --host <host>           Host to bind (default: ${DEFAULT_HOST})
  --data-dir <path>           Data directory (default: ${DEFAULT_DATA_DIR})
  --base-url <url>            Public base URL (default: http://<server-ip>:<port>)
  --remote-mitm-ui-hosts <v>  Allowed remote MITM UI hosts (default: detected server IP)
  --dashboard-password <v>    Initial dashboard password, optional
  --system-api-key <v>       SYSTEM API key value; auto-random if omitted
  --no-start                  Install service but do not start it
  -h, --help                  Show this help

Examples:
  sudo bluerouter install-service
  sudo bluerouter install-service --base-url http://172.16.6.8:20128
  sudo bluerouter uninstall-service
`);
}

function installService(argv) {
  if (hasArg(argv, ["--help", "-h"])) {
    printServiceHelp();
    return;
  }

  requireRootForService();

  const serverIp = detectServerIp();
  const serviceName = getArgValue(argv, ["--service-name"], DEFAULT_SERVICE_NAME);
  const portValue = getArgValue(argv, ["--port", "-p"], String(DEFAULT_PORT));
  const portNum = Number.parseInt(portValue, 10) || DEFAULT_PORT;
  const hostValue = getArgValue(argv, ["--host", "-H"], DEFAULT_HOST);
  const dataDir = getArgValue(argv, ["--data-dir"], DEFAULT_DATA_DIR);
  const baseUrl = getArgValue(argv, ["--base-url"], `http://${serverIp}:${portNum}`);
  const remoteMitmUiHosts = getArgValue(argv, ["--remote-mitm-ui-hosts"], serverIp || "127.0.0.1");
  const dashboardPassword =
    getArgValue(argv, ["--dashboard-password"], process.env.DASHBOARD_PASSWORD || "");
  const systemApiKey =
    getArgValue(argv, ["--system-api-key"], process.env.SYSTEM_API_KEY || generateSystemApiKeyValue());
  const noStart = hasArg(argv, ["--no-start"]);

  console.log(`== Installing ${serviceName}.service ==`);
  console.log(`Package:   ${pkg.name}@${pkg.version}`);
  console.log(`Command:   ${process.execPath} ${__filename}`);
  console.log(`Port:      ${portNum}`);
  console.log(`Host:      ${hostValue}`);
  console.log(`Base URL:  ${baseUrl}`);
  console.log(`Data dir:  ${dataDir}`);
  console.log(`SYSTEM API key: ${systemApiKey}`);
  console.log("");

  const serviceFile = writeSystemdService({
    serviceName,
    port: portNum,
    host: hostValue,
    dataDir,
    baseUrl,
    remoteMitmUiHosts,
    dashboardPassword,
    showLog: true,
    systemApiKey,
  });

  run("systemctl daemon-reload");
  run(`systemctl enable ${shellQuote(serviceName)}`);

  if (!noStart) {
    run(`systemctl restart ${shellQuote(serviceName)}`);
  }

  console.log("");
  console.log(`✅ Installed: ${serviceFile}`);
  console.log(`✅ MITM autostart hook: ${path.join(dataDir, "scripts", `${serviceName}-mitm-autostart.sh`)}`);
  console.log(`Dashboard: ${baseUrl}/dashboard`);
  console.log("");
  console.log("Useful commands:");
  console.log(`  systemctl status ${serviceName} --no-pager -l`);
  console.log(`  journalctl -u ${serviceName} -f`);
  console.log(`  systemctl restart ${serviceName}`);
}

function uninstallService(argv) {
  if (hasArg(argv, ["--help", "-h"])) {
    printServiceHelp();
    return;
  }

  requireRootForService();

  const serviceName = getArgValue(argv, ["--service-name"], DEFAULT_SERVICE_NAME);
  const serviceFile = `/etc/systemd/system/${serviceName}.service`;

  console.log(`== Removing ${serviceName}.service ==`);

  try { run(`systemctl stop ${shellQuote(serviceName)}`); } catch {}
  try { run(`systemctl disable ${shellQuote(serviceName)}`); } catch {}

  if (fs.existsSync(serviceFile)) {
    fs.unlinkSync(serviceFile);
  }

  run("systemctl daemon-reload");
  try { run("systemctl reset-failed"); } catch {}

  console.log(`✅ Removed ${serviceName}.service`);
}

function serviceStatus(argv) {
  requireRootForService();

  const serviceName = getArgValue(argv, ["--service-name"], DEFAULT_SERVICE_NAME);

  try {
    run(`systemctl status ${shellQuote(serviceName)} --no-pager -l`);
  } catch {
    process.exitCode = 1;
  }
}

const command = args[0] || "";

if (command === "install-service") {
  try {
    installService(args.slice(1));
    process.exit(0);
  } catch (error) {
    console.error(`❌ ${error.message || error}`);
    process.exit(1);
  }
}

if (command === "uninstall-service" || command === "remove-service") {
  try {
    uninstallService(args.slice(1));
    process.exit(0);
  } catch (error) {
    console.error(`❌ ${error.message || error}`);
    process.exit(1);
  }
}

if (command === "service-status" || command === "status-service") {
  serviceStatus(args.slice(1));
  process.exit(process.exitCode || 0);
}

// Parse arguments
let port = DEFAULT_PORT;
let host = DEFAULT_HOST;
let noBrowser = false;
let skipUpdate = false;
let showLog = false;
let trayMode = false;

for (let i = 0; i < args.length; i++) {
  if (args[i] === "--port" || args[i] === "-p") {
    port = parseInt(args[i + 1], 10) || DEFAULT_PORT;
    i++;
  } else if (args[i] === "--host" || args[i] === "-H") {
    host = args[i + 1] || DEFAULT_HOST;
    i++;
  } else if (args[i] === "--no-browser" || args[i] === "-n") {
    noBrowser = true;
  } else if (args[i] === "--log" || args[i] === "-l") {
    showLog = true;
  } else if (args[i] === "--skip-update") {
    skipUpdate = true;
  } else if (args[i] === "--tray" || args[i] === "-t") {
    trayMode = true;
    process.env.TRAY_MODE = "1";
  } else if (args[i] === "--help" || args[i] === "-h") {
    console.log(`
Usage: bluerouter [options]
       bluerouter install-service [options]
       bluerouter uninstall-service [options]
       bluerouter service-status [options]

Options:
  -p, --port <port>   Port to run the server (default: ${DEFAULT_PORT})
  -H, --host <host>   Host to bind (default: ${DEFAULT_HOST})
  -n, --no-browser    Don't open browser automatically
  -l, --log           Show server logs (default: hidden)
  -t, --tray          Run in system tray mode (background)
  --skip-update       Skip auto-update check
  -h, --help          Show this help message
  -v, --version       Show version

Service commands:
  install-service     Install and start Linux systemd service
  uninstall-service   Stop and remove Linux systemd service
  service-status      Show Linux systemd service status
`);
    process.exit(0);
  } else if (args[i] === "--version" || args[i] === "-v") {
    console.log(pkg.version);
    process.exit(0);
  }
}

// Auto-relaunch after update: detached process has no TTY → fallback to tray
if (skipUpdate && !trayMode && !process.stdin.isTTY) {
  trayMode = true;
  process.env.TRAY_MODE = "1";
}

// Always use Node.js runtime with absolute path
const RUNTIME = process.execPath;

// Compare semver versions: returns 1 if a > b, -1 if a < b, 0 if equal
function compareVersions(a, b) {
  const partsA = a.split(".").map(Number);
  const partsB = b.split(".").map(Number);
  for (let i = 0; i < 3; i++) {
    if (partsA[i] > partsB[i]) return 1;
    if (partsA[i] < partsB[i]) return -1;
  }
  return 0;
}

// Get app data dir (matches app/src/lib/dataDir.js convention)
function getAppDataDir() {
  return process.platform === "win32"
    ? path.join(process.env.APPDATA || "", "bluerouter")
    : path.join(os.homedir(), ".bluerouter");
}

// Kill PID from file (best-effort, removes file after)
function killByPidFile(pidFile) {
  try {
    if (!fs.existsSync(pidFile)) return;
    const pid = parseInt(fs.readFileSync(pidFile, "utf8").trim(), 10);
    if (!pid) return;
    try {
      if (process.platform === "win32") {
        execSync(`taskkill /F /T /PID ${pid}`, { stdio: "ignore", windowsHide: true, timeout: 3000 });
      } else {
        process.kill(pid, "SIGKILL");
      }
    } catch { }
    try { fs.unlinkSync(pidFile); } catch { }
  } catch { }
}

// Kill tunnel processes (cloudflared/tailscale) by their PID files
function killTunnelByPidFile() {
  const tunnelDir = path.join(getAppDataDir(), "tunnel");
  killByPidFile(path.join(tunnelDir, "cloudflared.pid"));
  killByPidFile(path.join(tunnelDir, "tailscale.pid"));
}

// Kill cloudflared whose --url targets this app's port (covers stale PID file case)
function killCloudflaredByAppPort(appPort) {
  if (!appPort) return [];
  const portMatchers = [`localhost:${appPort}`, `127.0.0.1:${appPort}`];
  const pids = [];
  try {
    if (process.platform === "win32") {
      const psCmd = `powershell -NonInteractive -WindowStyle Hidden -Command "Get-WmiObject Win32_Process -Filter 'Name=\\"cloudflared.exe\\"' | Select-Object ProcessId,CommandLine | ConvertTo-Csv -NoTypeInformation"`;
      const output = execSync(psCmd, { encoding: "utf8", windowsHide: true, timeout: 5000 });
      const lines = output.split("\n").slice(1).filter(l => l.trim());
      lines.forEach(line => {
        if (portMatchers.some(m => line.includes(m))) {
          const match = line.match(/^"(\d+)"/);
          if (match && match[1]) pids.push(match[1]);
        }
      });
    } else {
      const output = execSync("ps -eo pid,command 2>/dev/null", { encoding: "utf8", timeout: 5000 });
      output.split("\n").forEach(line => {
        if (line.includes("cloudflared") && portMatchers.some(m => line.includes(m))) {
          const parts = line.trim().split(/\s+/);
          const pid = parts[0];
          if (pid && !isNaN(pid)) pids.push(pid);
        }
      });
    }
  } catch { }
  return pids;
}

// Kill all bluerouter processes
function killAllAppProcesses(appPort) {
  return new Promise((resolve) => {
    try {
      // Kill MIT first (privileged process, needs special handling)
      killProxyByPidFile();
      // Kill cloudflared/tailscale by PID file (precise, only this app's tunnel)
      killTunnelByPidFile();

      const platform = process.platform;
      let pids = [];

      // Catch stale PID files: kill cloudflared bound to this app's port
      pids.push(...killCloudflaredByAppPort(appPort));

      if (platform === "win32") {
        // Windows: use WMI to get full CommandLine (tasklist /V doesn't include it)
        try {
          const psCmd = `powershell -NonInteractive -WindowStyle Hidden -Command "Get-WmiObject Win32_Process -Filter 'Name=\\"node.exe\\"' | Select-Object ProcessId,CommandLine | ConvertTo-Csv -NoTypeInformation"`;
          const output = execSync(psCmd, {
            encoding: "utf8",
            windowsHide: true,
            timeout: 5000
          });
          const lines = output.split("\n").slice(1).filter(l => l.trim());
          lines.forEach(line => {
            // Whitelist: real node process running bluerouter/cli.js, or next-server.
            // Avoids killing editors/grep/strace/cursor that just have "bluerouter" in cmdline.
            const cmd = line.toLowerCase();
            const isAppProcess =
              (cmd.includes("node") && cmd.includes("bluerouter") && (cmd.includes("cli.js") || cmd.includes("\\bluerouter") || cmd.includes("/bluerouter")))
              || cmd.includes("next-server");
            if (isAppProcess) {
              const match = line.match(/^"(\d+)"/);
              if (match && match[1] && match[1] !== process.pid.toString()) {
                pids.push(match[1]);
              }
            }
          });
        } catch (e) {
          // No processes found or error - continue
        }
      } else {
        // macOS/Linux: use ps to find all matching processes
        try {
          const output = execSync('ps aux 2>/dev/null', {
            encoding: 'utf8',
            timeout: 5000
          });
          const lines = output.split('\n');

          lines.forEach(line => {
            // Whitelist: real node process running bluerouter/cli.js, or next-server.
            // Avoids killing grep/strace/editors/cursor that incidentally match "bluerouter".
            const cmd = line.toLowerCase();
            const isAppProcess =
              (cmd.includes("node") && cmd.includes("bluerouter") && (cmd.includes("cli.js") || cmd.includes("/bluerouter")))
              || cmd.includes("next-server");
            if (isAppProcess) {
              const parts = line.trim().split(/\s+/);
              const pid = parts[1];
              if (pid && !isNaN(pid) && pid !== process.pid.toString()) {
                pids.push(pid);
              }
            }
          });
        } catch (e) {
          // No processes found or error - continue
        }
      }

      // Kill all found processes
      if (pids.length > 0) {
        pids.forEach(pid => {
          try {
            if (platform === "win32") {
              execSync(`taskkill /F /PID ${pid} 2>nul`, { stdio: 'ignore', shell: true, windowsHide: true, timeout: 3000 });
            } else {
              execSync(`kill -9 ${pid} 2>/dev/null`, { stdio: 'ignore', timeout: 3000 });
            }
          } catch (err) {
            // Process already dead or can't kill - continue
          }
        });

        // Wait for processes to fully terminate
        setTimeout(() => resolve(), 1000);
      } else {
        resolve();
      }
    } catch (err) {
      // Silent fail - continue anyway
      resolve();
    }
  });
}

// Sleep helper using SharedArrayBuffer wait (sync, no busy-loop)
function sleepSync(ms) {
  try { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); } catch { /* ignore */ }
}

// Wait until process dies or timeout reached
function waitForExit(pid, timeoutMs) {
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    try { process.kill(pid, 0); } catch { return true; }
    sleepSync(100);
  }
  return false;
}

// Kill MIT server by PID file (runs privileged, needs special handling)
// Sends SIGTERM first so MIT can clean up host entries before dying.
function killProxyByPidFile() {
  try {
    const pidFile = path.join(getAppDataDir(), "mitm", ".mitm.pid");
    if (!fs.existsSync(pidFile)) return;
    const pid = parseInt(fs.readFileSync(pidFile, "utf8").trim(), 10);
    if (!pid) return;

    if (process.platform === "win32") {
      // Graceful first (lets server cleanup hosts), then force
      try { execSync(`taskkill /T /PID ${pid}`, { stdio: "ignore", windowsHide: true, timeout: 2000 }); } catch { }
      if (!waitForExit(pid, 1500)) {
        try { execSync(`taskkill /F /T /PID ${pid}`, { stdio: "ignore", windowsHide: true, timeout: 3000 }); } catch { }
      }
      // Last-resort: PowerShell Stop-Process (sometimes succeeds where taskkill fails on admin processes)
      if (!waitForExit(pid, 500)) {
        try { execSync(`powershell -NonInteractive -WindowStyle Hidden -Command "Stop-Process -Id ${pid} -Force"`, { stdio: "ignore", windowsHide: true, timeout: 3000 }); } catch { }
      }
    } else {
      // SIGTERM via cached sudo token first
      try { execSync(`sudo -n kill -TERM ${pid} 2>/dev/null`, { stdio: "ignore", timeout: 2000 }); }
      catch { try { process.kill(pid, "SIGTERM"); } catch { } }
      if (!waitForExit(pid, 1500)) {
        try { execSync(`sudo -n kill -9 ${pid} 2>/dev/null`, { stdio: "ignore", timeout: 2000 }); }
        catch { try { process.kill(pid, "SIGKILL"); } catch { } }
      }
    }
    try { fs.unlinkSync(pidFile); } catch { }
  } catch { }
}

// Kill any process on specific port
function killProcessOnPort(port) {
  return new Promise((resolve) => {
    try {
      const platform = process.platform;
      let pid;

      if (platform === "win32") {
        try {
          const output = execSync(`netstat -ano | findstr :${port}`, {
            encoding: 'utf8',
            shell: true,
            windowsHide: true,
            timeout: 5000
          }).trim();
          const lines = output.split('\n').filter(l => l.includes('LISTENING'));
          if (lines.length > 0) {
            pid = lines[0].trim().split(/\s+/).pop();
            execSync(`taskkill /F /PID ${pid} 2>nul`, { stdio: 'ignore', shell: true, windowsHide: true, timeout: 3000 });
          }
        } catch (e) {
          // Port is free or error
        }
      } else {
        // macOS/Linux
        try {
          const pidOutput = execSync(`lsof -ti:${port}`, {
            encoding: 'utf8',
            stdio: ['pipe', 'pipe', 'ignore']
          }).trim();
          if (pidOutput) {
            pid = pidOutput.split('\n')[0];
            execSync(`kill -9 ${pid} 2>/dev/null`, { stdio: 'ignore', timeout: 3000 });
          }
        } catch (e) {
          // Port is free or error
        }
      }

      // Wait for port to be released
      setTimeout(() => resolve(), 500);
    } catch (err) {
      // Silent fail - continue anyway
      resolve();
    }
  });
}


// Detect if running in restricted environment (Codespaces, Docker)
function isRestrictedEnvironment() {
  // Check for Codespaces
  if (process.env.CODESPACES === "true" || process.env.GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN) {
    return "GitHub Codespaces";
  }

  // Check for Docker
  if (fs.existsSync("/.dockerenv") || (fs.existsSync("/proc/1/cgroup") && fs.readFileSync("/proc/1/cgroup", "utf8").includes("docker"))) {
    return "Docker";
  }

  return null;
}

// Check if new version available, return latest version or null
function checkForUpdate() {
  return new Promise((resolve) => {
    if (skipUpdate) {
      resolve(null);
      return;
    }

    const spinner = createSpinner("Checking for updates...").start();
    let resolved = false;

    const safetyTimeout = setTimeout(() => {
      if (!resolved) {
        resolved = true;
        spinner.stop();
        resolve(null);
      }
    }, 8000);

    const done = (version) => {
      if (resolved) return;
      resolved = true;
      clearTimeout(safetyTimeout);
      spinner.stop();
      resolve(version);
    };

    const req = https.get(`https://registry.npmjs.org/${pkg.name}/latest`, { timeout: 3000 }, (res) => {
      let data = "";
      res.on("data", chunk => data += chunk);
      res.on("end", () => {
        try {
          const latest = JSON.parse(data);
          if (latest.version && compareVersions(latest.version, pkg.version) > 0) {
            done(latest.version);
          } else {
            done(null);
          }
        } catch (e) {
          done(null);
        }
      });
    });

    req.on("error", () => done(null));
    req.on("timeout", () => { req.destroy(); done(null); });
  });
}

// Open browser
function openBrowser(url) {
  const platform = process.platform;
  let cmd;

  if (platform === "darwin") {
    cmd = `open "${url}"`;
  } else if (platform === "win32") {
    cmd = `start "" "${url}"`;
  } else {
    cmd = `xdg-open "${url}"`;
  }

  exec(cmd, { windowsHide: true }, (err) => {
    if (err) {
      console.log(`Open browser manually: ${url}`);
    }
  });
}

// Find standalone server (bundled in bin/app for published package)
const standaloneDir = path.join(__dirname, "app");
const serverPath = path.join(standaloneDir, "server.js");

if (!fs.existsSync(serverPath)) {
  console.error("Error: Standalone build not found.");
  console.error("Please run 'npm run build:cli' first.");
  process.exit(1);
}

// Check for updates FIRST, then start server
checkForUpdate().then((latestVersion) => {
  killAllAppProcesses(port).then(() => {
    return killProcessOnPort(port);
  }).then(() => {
    startServer(latestVersion);
  });
});

// Show interface selection menu
async function showInterfaceMenu(latestVersion) {
  const { selectMenu } = require("./src/cli/utils/input");
  const { clearScreen } = require("./src/cli/utils/display");
  const { getEndpoint } = require("./src/cli/utils/endpoint");

  clearScreen();

  const displayHost = host === DEFAULT_HOST ? "localhost" : host;

  // Detect tunnel/local mode for server URL display
  let serverUrl;
  try {
    const { endpoint, tunnelEnabled } = await getEndpoint(port);
    serverUrl = tunnelEnabled ? endpoint.replace(/\/v1$/, "") : `http://${displayHost}:${port}`;
  } catch (e) {
    serverUrl = `http://${displayHost}:${port}`;
  }

  const subtitle = `🚀 Server: \x1b[32m${serverUrl}\x1b[0m`;

  const menuItems = [];

  if (latestVersion) {
    menuItems.push({ label: `Update to v${latestVersion} (current: v${pkg.version})`, icon: "⬆" });
  }

  menuItems.push(
    { label: "Web UI (Open in Browser)", icon: "🌐" },
    { label: "Terminal UI (Interactive CLI)", icon: "💻" },
    { label: "Hide to Tray (Background)", icon: "🔔" },
    { label: "Exit", icon: "🚪" }
  );

  const selected = await selectMenu(`Choose Interface (v${pkg.version})`, menuItems, 0, subtitle);

  const offset = latestVersion ? 1 : 0;

  if (latestVersion && selected === 0) return "update";
  if (selected === offset) return "web";
  if (selected === offset + 1) return "terminal";
  if (selected === offset + 2) return "hide";
  return "exit";
}

const MAX_RESTARTS = 2;
const RESTART_RESET_MS = 30000; // Reset counter if alive > 30s

function startServer(latestVersion) {
  const displayHost = host === DEFAULT_HOST ? "localhost" : host;
  const url = `http://${displayHost}:${port}/dashboard`;

  let restartCount = 0;
  let serverStartTime = Date.now();

  const CRASH_LOG_LINES = 50;
  let crashLog = [];

  function spawnServer() {
    serverStartTime = Date.now();
    crashLog = [];
    const child = spawn(RUNTIME, ["--max-old-space-size=6144", serverPath], {
      cwd: standaloneDir,
      stdio: showLog ? "inherit" : ["ignore", "ignore", "pipe"],
      detached: true,
      windowsHide: true,
      env: {
        ...buildEnvWithRuntime(process.env),
        PORT: port.toString(),
        HOSTNAME: host
      }
    });
    if (!showLog && child.stderr) {
      child.stderr.on("data", (data) => {
        const lines = data.toString().split("\n").filter(Boolean);
        crashLog.push(...lines);
        if (crashLog.length > CRASH_LOG_LINES) crashLog = crashLog.slice(-CRASH_LOG_LINES);
      });
    }
    return child;
  }

  let server = spawnServer();

  // Cleanup function - force kill server process
  let isCleaningUp = false;
  function cleanup() {
    if (isCleaningUp) return;
    isCleaningUp = true;
    try {
      // Kill tray if running
      try {
        const { killTray } = require("./src/cli/tray/tray");
        killTray();
      } catch (e) { }
      // Kill MIT server (privileged process) via PID file
      killProxyByPidFile();
      // Kill cloudflared/tailscale via PID file (only this app's tunnel)
      killTunnelByPidFile();
      // Kill server process directly
      if (server.pid) {
        process.kill(server.pid, "SIGKILL");
      }
      // Also try to kill process group
      process.kill(-server.pid, "SIGKILL");
    } catch (e) { }
  }

  // Suppress all errors during shutdown (systray lib throws JSON parse errors)
  let isShuttingDown = false;
  process.on("uncaughtException", (err) => {
    if (isShuttingDown) return;
    console.error("Error:", err.message);
  });

  // Handle all exit scenarios
  process.on("SIGINT", () => {
    if (isShuttingDown) return;
    isShuttingDown = true;
    console.log("\nExiting...");
    cleanup();
    setTimeout(() => process.exit(0), 100);
  });
  process.on("SIGTERM", () => {
    if (isShuttingDown) return;
    isShuttingDown = true;
    cleanup();
    setTimeout(() => process.exit(0), 100);
  });
  process.on("SIGHUP", () => {
    if (isShuttingDown) return;
    isShuttingDown = true;
    cleanup();
    setTimeout(() => process.exit(0), 100);
  });

  // Initialize tray icon (runs alongside TUI)
  const initTrayIcon = () => {
    try {
      const { initTray } = require("./src/cli/tray/tray");
      initTray({
        port,
        onQuit: () => {
          isShuttingDown = true;
          console.log("\n👋 Shutting down from tray...");
          cleanup();
          setTimeout(() => process.exit(0), 100);
        },
        onOpenDashboard: () => openBrowser(url)
      });
    } catch (err) {
      // Tray not available - continue without it
    }
  };

  // Tray-only mode: no TUI, just tray icon
  if (trayMode) {
    // Ignore SIGHUP so macOS terminal close doesn't kill the background tray process
    process.removeAllListeners("SIGHUP");
    process.on("SIGHUP", () => {});

    console.log(`\n🚀 ${pkg.name} v${pkg.version}`);
    console.log(`Server: http://${displayHost}:${port}`);

    setTimeout(() => {
      initTrayIcon();
      console.log("\n💡 Router is now running in system tray. Close this terminal if you want.");
      console.log("   Right-click tray icon to open dashboard or quit.\n");
    }, 2000);

    return;
  }

  // Wait for server to be ready, then show interface menu loop + tray
  setTimeout(async () => {
    // Start tray icon alongside TUI
    initTrayIcon();

    try {
      while (true) {
        const choice = await showInterfaceMenu(latestVersion);

        if (choice === "update") {
          isShuttingDown = true;
          const { clearScreen } = require("./src/cli/utils/display");
          clearScreen();
          console.log(`\n⬆  Update v${pkg.version} → v${latestVersion}\n`);
          console.log(`Run this after exit:\n`);
          console.log(`   \x1b[33m${INSTALL_CMD_LATEST}\x1b[0m\n`);
          cleanup();
          await killAllAppProcesses(port);
          await killProcessOnPort(port);
          setTimeout(() => process.exit(0), 200);
          return;
        } else if (choice === "web") {
          openBrowser(url);
          // Wait for user to come back
          const { pause } = require("./src/cli/utils/input");
          await pause("\nPress Enter to go back to menu...");
        } else if (choice === "terminal") {
          // Start Terminal UI - it will return when user selects Back
          const { startTerminalUI } = require("./src/cli/terminalUI");
          await startTerminalUI(port);
          // Loop continues, show menu again
        } else if (choice === "hide") {
          const { clearScreen } = require("./src/cli/utils/display");
          clearScreen();

          // Enable auto startup on OS boot
          try {
            const { enableAutoStart } = require("./src/cli/tray/autostart");
            enableAutoStart(__filename);
          } catch (e) { }

          if (process.platform === "darwin") {
            // macOS: keep current process alive — spawning a detached child puts
            // it outside the login session so NSStatusItem silently fails.
            process.removeAllListeners("SIGHUP");
            process.on("SIGHUP", () => {});

            console.log(`\n⏳ Switching to tray mode... (icon already visible in menu bar)`);
            console.log(`🔔 Bluerouter is running in tray (PID: ${process.pid})`);
            console.log(`   Server: http://${displayHost}:${port}`);
            console.log(`\n💡 You can close this terminal. Right-click tray icon to quit.\n`);

            // Tray already init'd at startup — just keep event loop alive.
            return;
          }

          // Windows/Linux: spawn detached bgProcess (systray works fine in child)
          console.log(`\n⏳ Starting background process... (tray icon will appear in ~3s)`);

          const bgProcess = spawn(process.execPath, [__filename, "--tray", "--skip-update", "-p", port.toString()], {
            detached: true,
            stdio: "ignore",
            windowsHide: true,
            env: { ...process.env }
          });
          bgProcess.unref();

          console.log(`🔔 Bluerouter is now running in background (PID: ${bgProcess.pid})`);
          console.log(`   Server: http://${displayHost}:${port}`);
          console.log(`\n💡 You can close this terminal. Right-click tray icon to quit.\n`);

          // cleanup() kills server so bgProcess can claim the port fresh
          cleanup();
          process.exit(0);
        } else if (choice === "exit") {
          isShuttingDown = true;
          console.log("\nExiting...");
          cleanup();
          setTimeout(() => process.exit(0), 100);
        }
      }
    } catch (err) {
      console.error("Error:", err.message);
      cleanup();
      process.exit(1);
    }
  }, 3000);

  function attachServerEvents() {
    server.on("error", (err) => {
      console.error("Failed to start server:", err.message);
      if (!isShuttingDown) tryRestart();
      else { cleanup(); process.exit(1); }
    });

    server.on("close", (code) => {
      if (isShuttingDown || code === 0) {
        process.exit(code || 0);
        return;
      }
      tryRestart(code);
    });
  }

  function tryRestart(code) {
    const aliveMs = Date.now() - serverStartTime;
    // Reset counter if last run was stable
    if (aliveMs >= RESTART_RESET_MS) restartCount = 0;

    if (restartCount >= MAX_RESTARTS) {
      console.error(`\n⚠️  Server crashed ${MAX_RESTARTS} times. Disabling MIT and restarting...`);
      try {
        const dbPath = path.join(os.homedir(), process.platform === "win32" ? path.join("AppData", "Roaming", "bluerouter", "db.json") : path.join(".bluerouter", "db.json"));
        if (fs.existsSync(dbPath)) {
          const db = JSON.parse(fs.readFileSync(dbPath, "utf-8"));
          if (db.settings) db.settings.mitmEnabled = false;
          fs.writeFileSync(dbPath, JSON.stringify(db, null, 2));
        }
      } catch { /* best effort */ }
      restartCount = 0;
      server = spawnServer();
      attachServerEvents();
      return;
    }

    restartCount++;
    const delay = Math.min(1000 * restartCount, 10000);
    console.error(`\n⚠️  Server exited (code=${code ?? "unknown"}). Restarting in ${delay / 1000}s... (${restartCount}/${MAX_RESTARTS})`);
    if (crashLog.length) {
      console.error("\n--- Server crash log ---");
      crashLog.forEach(l => console.error(l));
      console.error("--- End crash log ---\n");
    }

    setTimeout(() => {
      server = spawnServer();
      attachServerEvents();
    }, delay);
  }

  attachServerEvents();
}
