#!/usr/bin/env node
"use strict";

process.title = "annovibe";

// eslint-disable-next-line @typescript-eslint/no-require-imports
const { spawn, spawnSync } = require("child_process");
// eslint-disable-next-line @typescript-eslint/no-require-imports
const path = require("path");
// eslint-disable-next-line @typescript-eslint/no-require-imports
const fs = require("fs");
// eslint-disable-next-line @typescript-eslint/no-require-imports
const os = require("os");
// eslint-disable-next-line @typescript-eslint/no-require-imports
const http = require("http");
// eslint-disable-next-line @typescript-eslint/no-require-imports
const net = require("net");
// eslint-disable-next-line @typescript-eslint/no-require-imports
const readline = require("readline");
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { parseArgs } = require("util");
// eslint-disable-next-line @typescript-eslint/no-require-imports
const bcrypt = require("bcryptjs");
// eslint-disable-next-line @typescript-eslint/no-require-imports
const https = require("https");
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { randomBytes } = require("crypto");

const pkgDir = path.join(__dirname, "..");
const nextDir = path.join(pkgDir, ".next");
const pkg = JSON.parse(fs.readFileSync(path.join(pkgDir, "package.json"), "utf8"));
const VERSION = pkg.version;
const PKG_NAME = pkg.name;
const agentDir = process.env.PI_CODING_AGENT_DIR ?? path.join(os.homedir(), ".pi", "agent");
const authFile = path.join(agentDir, "web-auth.json");
const stateFile = path.join(agentDir, "annovibe.json");
const legacyStateFile = path.join(agentDir, "pidex.json");
const logFile = path.join(agentDir, "annovibe.log");
const legacyLogFile = path.join(agentDir, "pidex.log");
const STOP_TERM_TIMEOUT_MS = 6000;
const STOP_KILL_TIMEOUT_MS = 2000;
const PROCESS_TITLE = "annovibe";
const NEXT_PROCESS_TITLE = "annovibe-next";
const AUTO_RESTART_ENABLED = !/^(0|false|no)$/i.test(process.env.ANNOVIBE_AUTO_RESTART ?? "1");
const AUTO_RESTART_POLL_MS = parsePositiveInt(process.env.ANNOVIBE_AUTO_RESTART_POLL_MS, 60_000);
const AUTO_RESTART_IDLE_GRACE_MS = parsePositiveInt(process.env.ANNOVIBE_AUTO_RESTART_IDLE_GRACE_MS, 10_000);
const AUTO_RESTART_MAX_WAIT_MS = parsePositiveInt(process.env.ANNOVIBE_AUTO_RESTART_MAX_WAIT_MS, 30 * 60_000);
const AUTO_RESTART_WAIT_FOR_PORT_MS = parsePositiveInt(process.env.ANNOVIBE_AUTO_RESTART_WAIT_FOR_PORT_MS, 30_000);
const INTERNAL_RUNTIME_TIMEOUT_MS = 3000;

// Resolve next's CLI entry
let nextBin;
try {
  nextBin = require.resolve("next/dist/bin/next", { paths: [pkgDir] });
} catch {
  try {
    const nextPkg = require.resolve("next/package.json", { paths: [pkgDir] });
    nextBin = path.join(path.dirname(nextPkg), "dist", "bin", "next");
  } catch {
    nextBin = path.join(pkgDir, "node_modules", "next", "dist", "bin", "next");
  }
}

const { values: cliArgs, positionals } = parseArgs({
  options: {
    port:     { type: "string", short: "p" },
    hostname: { type: "string", short: "H" },
    json:     { type: "boolean" },
    follow:   { type: "boolean", short: "f" },
    version:  { type: "boolean", short: "v" },
    help:     { type: "boolean", short: "h" },
  },
  strict: false,
  allowPositionals: true,
});

const port     = cliArgs.port     ?? process.env.PORT     ?? "30141";
const hostname = cliArgs.hostname ?? process.env.HOSTNAME ?? null;

// ── CLI commands ──

function ensureAgentDir() {
  if (!fs.existsSync(agentDir)) fs.mkdirSync(agentDir, { recursive: true });
}

function getCommandPath() {
  return path.resolve(process.argv[1] || __filename);
}

function getUrl(host, listenPort) {
  return `http://${host ?? "localhost"}:${listenPort}`;
}

function getServerArgs() {
  return [getCommandPath(), "__server", "--port", String(port), ...(hostname ? ["--hostname", hostname] : [])];
}

function getServerCommand() {
  return `${getCommandPath()} __server --port ${port}${hostname ? ` --hostname ${hostname}` : ""}`;
}

function getServerCommandFor(listenPort, listenHost) {
  return `${getCommandPath()} __server --port ${listenPort}${listenHost ? ` --hostname ${listenHost}` : ""}`;
}

function nowIso() {
  return new Date().toISOString();
}

function parsePositiveInt(value, fallback) {
  const n = Number(value);
  return Number.isFinite(n) && n > 0 ? Math.floor(n) : fallback;
}

function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

function readPackageVersion(packageDir) {
  try {
    const installedPkg = JSON.parse(fs.readFileSync(path.join(packageDir, "package.json"), "utf8"));
    return typeof installedPkg.version === "string" ? installedPkg.version : null;
  } catch {
    return null;
  }
}

function getInstalledPackageDir() {
  try {
    return path.resolve(path.dirname(fs.realpathSync(getCommandPath())), "..");
  } catch {
    return pkgDir;
  }
}

function readInstalledVersion() {
  const dirs = [getInstalledPackageDir(), pkgDir];
  for (const dir of dirs) {
    const version = readPackageVersion(dir);
    if (version) return version;
  }
  return null;
}

function getRuntimeConnectHost(host) {
  if (!host || host === "0.0.0.0") return "127.0.0.1";
  if (host === "::" || host === "[::]") return "::1";
  return String(host).replace(/^\[(.*)\]$/, "$1");
}

function canBindPort(listenPort, host) {
  return new Promise((resolve) => {
    const server = net.createServer();
    let settled = false;
    const done = (available) => {
      if (settled) return;
      settled = true;
      resolve(available);
    };
    server.unref();
    server.once("error", () => done(false));
    server.once("listening", () => {
      server.close(() => done(true));
    });
    server.listen(Number(listenPort), host ? String(host) : undefined);
  });
}

async function waitForPortAvailable(listenPort, host, timeoutMs) {
  const startedAt = Date.now();
  while (Date.now() - startedAt < timeoutMs) {
    if (await canBindPort(listenPort, host)) return true;
    await sleep(250);
  }
  return canBindPort(listenPort, host);
}

function fetchRuntimeStatus(internalToken) {
  return new Promise((resolve) => {
    let settled = false;
    const finish = (status) => {
      if (settled) return;
      settled = true;
      resolve(status);
    };
    const req = http.request({
      hostname: getRuntimeConnectHost(hostname),
      port: Number(port),
      path: "/api/internal/runtime",
      method: "GET",
      timeout: INTERNAL_RUNTIME_TIMEOUT_MS,
      headers: {
        "x-annovibe-internal-token": internalToken,
      },
    }, (res) => {
      if (res.statusCode !== 200) {
        res.resume();
        finish(null);
        return;
      }
      let data = "";
      res.setEncoding("utf8");
      res.on("data", (chunk) => {
        data += chunk;
        if (data.length > 1024 * 1024) req.destroy();
      });
      res.on("end", () => {
        try {
          finish(JSON.parse(data));
        } catch {
          finish(null);
        }
      });
    });
    req.on("error", () => finish(null));
    req.on("timeout", () => {
      req.destroy();
      finish(null);
    });
    req.end();
  });
}

function setStableProcessTitle(title) {
  process.title = title;
  try {
    Object.defineProperty(process, "title", {
      configurable: true,
      enumerable: true,
      get: () => title,
      set: () => {},
    });
  } catch {
    const timer = setInterval(() => {
      try { process.title = title; } catch { /* ignore */ }
    }, 500);
    timer.unref();
  }
}

function readState() {
  try {
    const target = fs.existsSync(stateFile) ? stateFile : legacyStateFile;
    if (!fs.existsSync(target)) return null;
    return JSON.parse(fs.readFileSync(target, "utf8"));
  } catch {
    return null;
  }
}

function staleStateRemoved() {
  const state = readState();
  if (state && !isPidAlive(state.supervisorPid) && !isPidAlive(state.nextPid)) {
    removeState();
    return true;
  }
  return false;
}

function writeState(state) {
  ensureAgentDir();
  fs.writeFileSync(stateFile, JSON.stringify({ ...state, updatedAt: nowIso() }, null, 2));
}

function findListeningPids(listenPort) {
  if (process.platform === "win32") return [];
  const pids = new Set();
  try {
    const result = spawnSync("lsof", [`-tiTCP:${listenPort}`, "-sTCP:LISTEN"], { encoding: "utf8" });
    result.stdout
      .split(/\s+/)
      .map((value) => Number(value))
      .filter((pid) => Number.isInteger(pid) && pid > 0 && pid !== process.pid)
      .forEach((pid) => pids.add(pid));
  } catch {
    // Fall through to ss.
  }
  if (pids.size === 0 && process.platform === "linux") {
    try {
      const result = spawnSync("ss", ["-H", "-ltnp", `sport = :${listenPort}`], { encoding: "utf8" });
      const matches = result.stdout.matchAll(/pid=(\d+)/g);
      for (const match of matches) {
        const pid = Number(match[1]);
        if (Number.isInteger(pid) && pid > 0 && pid !== process.pid) pids.add(pid);
      }
    } catch {
      // Ignore lookup failures.
    }
  }
  return [...pids];
}

function safeRealpath(targetPath) {
  try {
    return fs.realpathSync(targetPath);
  } catch {
    return targetPath;
  }
}

function isPathInside(childPath, parentPath) {
  const relative = path.relative(parentPath, childPath);
  return relative === "" || (!!relative && !relative.startsWith("..") && !path.isAbsolute(relative));
}

function readProcessCmdline(pid) {
  if (!Number.isInteger(pid) || pid <= 0) return "";
  if (process.platform === "linux") {
    try {
      return fs.readFileSync(`/proc/${pid}/cmdline`, "utf8").replace(/\0/g, " ").trim();
    } catch {
      // Fall through to ps.
    }
  }
  try {
    const result = spawnSync("ps", ["-p", String(pid), "-o", "command="], { encoding: "utf8" });
    return result.status === 0 ? result.stdout.trim() : "";
  } catch {
    return "";
  }
}

function readProcessCwd(pid) {
  if (!Number.isInteger(pid) || pid <= 0) return "";
  if (process.platform === "linux") {
    try {
      return fs.realpathSync(`/proc/${pid}/cwd`);
    } catch {
      return "";
    }
  }
  if (process.platform === "darwin") {
    try {
      const result = spawnSync("lsof", ["-a", "-p", String(pid), "-d", "cwd", "-Fn"], { encoding: "utf8" });
      if (result.status !== 0) return "";
      const line = result.stdout.split(/\r?\n/).find((value) => value.startsWith("n"));
      return line ? safeRealpath(line.slice(1)) : "";
    } catch {
      return "";
    }
  }
  return "";
}

function getProcessSummary(pid) {
  return {
    pid,
    cmdline: readProcessCmdline(pid),
    cwd: readProcessCwd(pid),
  };
}

function logSkippedProcess(info, listenPort = null) {
  const label = info.cmdline || "unknown command";
  const portText = listenPort ? `port ${listenPort} is used by ` : "";
  console.error(`annovibe stop: ${portText}pid ${info.pid}, but it does not look like annovibe; not stopping it.`);
  console.error(`command: ${label}`);
  if (info.cwd) console.error(`cwd: ${info.cwd}`);
}

function looksLikeAnnoVibeProcess(info) {
  const cmdline = info.cmdline || "";
  const cwd = info.cwd || "";
  const realPkgDir = safeRealpath(pkgDir);
  const realCwd = cwd ? safeRealpath(cwd) : "";
  const inAnnoVibeDir = !!realCwd && isPathInside(realCwd, realPkgDir);
  const commandMentionsAnnoVibe =
    cmdline.includes("pi-web.js") ||
    cmdline.includes("@seqyuan/annovibe") ||
    cmdline.includes("@seqyuan/pidex") ||
    cmdline.includes("/annovibe/") ||
    cmdline.includes("/pidex/") ||
    cmdline.includes("\\annovibe\\") ||
    cmdline.includes("\\pidex\\") ||
    cmdline.includes(PROCESS_TITLE);
  const commandMentionsNext =
    cmdline.includes("next-server") ||
    cmdline.includes("next/dist/bin/next") ||
    cmdline.includes("next start");

  return commandMentionsAnnoVibe || (inAnnoVibeDir && commandMentionsNext);
}

async function stopVerifiedPortListeners(listenPort, quiet = false) {
  const listeningPids = findListeningPids(listenPort);
  if (listeningPids.length === 0) return { found: false, stopped: true, skipped: [] };

  const verified = [];
  const skipped = [];
  for (const pid of listeningPids) {
    const info = getProcessSummary(pid);
    if (looksLikeAnnoVibeProcess(info)) {
      verified.push(pid);
    } else {
      skipped.push(info);
    }
  }

  if (skipped.length > 0) {
    for (const info of skipped) {
      logSkippedProcess(info, listenPort);
    }
  }

  if (verified.length === 0) {
    return { found: true, stopped: skipped.length === 0, skipped };
  }

  let stopped = true;
  for (const pid of verified) {
    stopped = (await terminatePid(pid)) && stopped;
  }
  if (!quiet) {
    console.log(stopped
      ? `annovibe stop: stopped listener pid${verified.length === 1 ? "" : "s"} ${verified.join(", ")}`
      : `annovibe stop: failed to stop listener pid${verified.length === 1 ? "" : "s"} ${verified.join(", ")}`);
  }
  return { found: true, stopped: stopped && skipped.length === 0, skipped };
}

function removeState() {
  try { fs.rmSync(stateFile, { force: true }); } catch { /* ignore */ }
  try { fs.rmSync(legacyStateFile, { force: true }); } catch { /* ignore */ }
}

function isPidAlive(pid) {
  if (!Number.isInteger(pid) || pid <= 0) return false;
  try {
    process.kill(pid, 0);
    return true;
  } catch (error) {
    return error && error.code === "EPERM";
  }
}

function waitForPidExit(pid, timeoutMs) {
  const startedAt = Date.now();
  return new Promise((resolve) => {
    const tick = () => {
      if (!isPidAlive(pid)) {
        resolve(true);
        return;
      }
      if (Date.now() - startedAt >= timeoutMs) {
        resolve(false);
        return;
      }
      setTimeout(tick, 100);
    };
    tick();
  });
}

async function terminatePid(pid, termTimeoutMs = STOP_TERM_TIMEOUT_MS, killTimeoutMs = STOP_KILL_TIMEOUT_MS) {
  if (!isPidAlive(pid)) return true;
  try {
    process.kill(pid, "SIGTERM");
  } catch {
    return !isPidAlive(pid);
  }
  if (await waitForPidExit(pid, termTimeoutMs)) return true;
  try {
    process.kill(pid, "SIGKILL");
  } catch {
    return !isPidAlive(pid);
  }
  return waitForPidExit(pid, killTimeoutMs);
}

function renderState(state, json = false) {
  const supervisorAlive = !!state && isPidAlive(state.supervisorPid);
  const nextAlive = !!state && isPidAlive(state.nextPid);
  const status = supervisorAlive ? "running" : nextAlive ? "orphaned" : "stopped";
  const payload = {
    running: supervisorAlive || nextAlive,
    status,
    version: state?.version ?? VERSION,
    url: state?.url ?? getUrl(hostname, port),
    port: state?.port ?? port,
    hostname: state?.hostname ?? hostname,
    supervisorPid: supervisorAlive ? state.supervisorPid : null,
    nextPid: nextAlive ? state.nextPid : null,
    startedAt: supervisorAlive ? state.startedAt ?? null : null,
    restartPending: !!state?.restartPending,
    installedVersion: state?.installedVersion ?? null,
    restartDetectedAt: state?.restartDetectedAt ?? null,
    restartStartedAt: state?.restartStartedAt ?? null,
    logFile,
    stateFile,
  };
  if (json) return JSON.stringify(payload, null, 2);
  if (payload.status === "stopped") {
    return [
      "annovibe status: stopped",
      `log: ${payload.logFile}`,
      `state: ${payload.stateFile}`,
    ].join("\n");
  }
  return [
    "annovibe status: running",
    `url: ${payload.url}`,
    `version: ${payload.version}`,
    `state: ${payload.status}`,
    `supervisor pid: ${payload.supervisorPid ?? "not running"}`,
    `next pid: ${payload.nextPid ?? "unknown"}`,
    `started: ${payload.startedAt ?? "unknown"}`,
    ...(payload.restartPending ? [
      `restart: pending${payload.installedVersion ? ` for v${payload.installedVersion}` : ""}`,
    ] : []),
    `log: ${payload.logFile}`,
  ].join("\n");
}

function printRecentLogs(follow = false) {
  ensureAgentDir();
  const targetLogFile = fs.existsSync(logFile) ? logFile : legacyLogFile;
  if (!fs.existsSync(targetLogFile)) {
    console.log(`annovibe logs: no log file yet (${logFile})`);
    return;
  }
  const content = fs.readFileSync(targetLogFile, "utf8");
  const lines = content.split(/\r?\n/);
  console.log(lines.slice(Math.max(0, lines.length - 200)).join("\n"));
  if (!follow) return;
  const tail = spawn(process.platform === "win32" ? "powershell.exe" : "tail", process.platform === "win32"
    ? ["-NoProfile", "-Command", `Get-Content -Path ${JSON.stringify(targetLogFile)} -Wait -Tail 0`]
    : ["-f", targetLogFile], { stdio: "inherit" });
  tail.on("exit", (code) => process.exit(code ?? 0));
}

async function stopManagedServer(quiet = false) {
  const state = readState();
  if (!state) {
    const portStop = await stopVerifiedPortListeners(port, quiet);
    if (portStop.found) return portStop.stopped ? 0 : 1;
    if (!quiet) console.log("annovibe stop: already stopped");
    return 0;
  }

  const supervisorPid = Number(state.supervisorPid);
  const nextPid = Number(state.nextPid);
  const statePort = state.port ?? port;
  const candidatePids = [...new Set([supervisorPid, nextPid])]
    .filter((pid) => Number.isInteger(pid) && pid > 0 && isPidAlive(pid));

  let stopped = true;
  let verifiedCount = 0;
  const stoppedPids = [];
  for (const pid of candidatePids) {
    const info = getProcessSummary(pid);
    if (!looksLikeAnnoVibeProcess(info)) {
      logSkippedProcess(info);
      continue;
    }
    verifiedCount += 1;
    const candidateStopped = await terminatePid(pid);
    if (candidateStopped) stoppedPids.push(pid);
    stopped = candidateStopped && stopped;
  }

  const portStop = await stopVerifiedPortListeners(statePort, quiet);
  if (portStop.found) {
    stopped = portStop.stopped && stopped;
  }

  if (stopped) {
    removeState();
    if (!quiet) {
      if (verifiedCount > 0) {
        console.log(`annovibe stop: stopped pid${stoppedPids.length === 1 ? "" : "s"} ${stoppedPids.join(", ")}`);
      } else if (!portStop.found) {
        console.log("annovibe stop: state was stale; cleaned up");
      }
    }
    return 0;
  }
  if (!quiet) console.error(`annovibe stop: failed to stop annovibe process${verifiedCount === 1 ? "" : "es"}`);
  return 1;
}

async function startManagedServerWithPort(overridePort, overrideHost) {
  return startManagedServer({ effectivePort: overridePort, effectiveHost: overrideHost });
}

async function startManagedServer({ quiet = false, effectivePort: optPort, effectiveHost: optHost } = {}) {
  seedDefaults();
  await maybeSetupAuth();
  if (!fs.existsSync(nextDir)) {
    console.error("Build artifacts not found. Please report this issue.");
    return 1;
  }
  staleStateRemoved();
  const existing = readState();
  const existingSupervisorAlive = existing && isPidAlive(existing.supervisorPid);
  const existingNextAlive = existing && isPidAlive(existing.nextPid);
  const effectivePort = optPort ?? cliArgs.port ?? existing?.port ?? process.env.PORT ?? "30141";
  const effectiveHost = optHost ?? cliArgs.hostname ?? existing?.hostname ?? process.env.HOSTNAME ?? null;
  if (existingSupervisorAlive) {
    if (!quiet) {
      console.log(`annovibe start: already running pid ${existing.supervisorPid}`);
      console.log(`url: ${existing.url ?? getUrl(effectiveHost, effectivePort)}`);
      console.log(`log: ${logFile}`);
    }
    return 0;
  }
  if (existingNextAlive) {
    if (!quiet) console.log(`annovibe start: recovering orphaned server pid ${existing.nextPid}`);
    if (!(await terminatePid(existing.nextPid))) {
      if (!quiet) console.error(`annovibe start: failed to stop orphaned server pid ${existing.nextPid}`);
      return 1;
    }
    removeState();
  }
  ensureAgentDir();
  const child = spawnDetachedServer({}, effectivePort, effectiveHost);
  if (!child.pid) {
    throw new Error("failed to start annovibe server");
  }
  child.unref();
  const state = {
    schemaVersion: 1,
    version: VERSION,
    command: getServerCommandFor(effectivePort, effectiveHost),
    supervisorPid: child.pid,
    nextPid: null,
    port: String(effectivePort),
    hostname: effectiveHost,
    url: getUrl(effectiveHost, effectivePort),
    logFile,
    stateFile,
    startedAt: nowIso(),
  };
  writeState(state);
  if (!quiet) {
    console.log(`annovibe start: pid ${child.pid}`);
    console.log(`url: ${state.url}`);
    console.log(`log: ${logFile}`);
  }
  return 0;
}

function spawnDetachedServer(extraEnv = {}, overridePort, overrideHost) {
  ensureAgentDir();
  const fd = fs.openSync(logFile, "a");
  const serverArgs = overridePort != null
    ? [getCommandPath(), "__server", "--port", String(overridePort), ...(overrideHost ? ["--hostname", overrideHost] : [])]
    : getServerArgs();
  const child = spawn(process.execPath, serverArgs, {
    cwd: fs.existsSync(getInstalledPackageDir()) ? getInstalledPackageDir() : pkgDir,
    detached: true,
    stdio: ["ignore", fd, fd],
    env: {
      ...process.env,
      ANNOVIBE_MANAGED: "1",
      ...extraEnv,
    },
  });
  fs.closeSync(fd);
  return child;
}

if (cliArgs.version) {
  console.log(VERSION);
  process.exit(0);
}

if (cliArgs.help) {
  console.log(`annovibe v${VERSION} — Web UI for the pi coding agent

Usage:
  annovibe [options]
  annovibe start          Start annovibe in background
  annovibe restart        Stop then start annovibe in background
  annovibe stop           Stop background annovibe
  annovibe status [--json] Show background server status
  annovibe logs [-f]      Show background server logs
  annovibe passwd          Change password (or set if none)
  annovibe passwd --reset   Remove password (disable auth)
  annovibe update          Update to the latest version

Options:
  -p, --port <port>     Port to listen on (default: 30141, env: PORT)
  -H, --hostname <host> Hostname to bind to (default: localhost)
      --json            Print machine-readable status JSON
  -f, --follow          Follow logs
  -v, --version         Print version
  -h, --help            Show this help

Environment:
  PORT                  Server port
  ANNOVIBE_PASSWORD     Set password on first start
  ANNOVIBE_AUTO_RESTART Set 0/false/no to disable idle auto-restart after update
  PI_CODING_AGENT_DIR   Custom pi agent data directory
`);
  process.exit(0);
}

const firstPos = positionals[0];
if (firstPos === "status") {
  const json = cliArgs.json;
  staleStateRemoved();
  const state = readState();
  console.log(renderState(state, json));
  process.exit(state && (isPidAlive(state.supervisorPid) || isPidAlive(state.nextPid)) ? 0 : 1);
}

if (firstPos === "logs") {
  printRecentLogs(cliArgs.follow);
  if (!cliArgs.follow) process.exit(0);
  return;
}

if (firstPos === "stop") {
  stopManagedServer()
    .then((code) => process.exit(code))
    .catch((error) => {
      console.error(`annovibe stop: ${error instanceof Error ? error.message : String(error)}`);
      process.exit(1);
    });
  return;
}

if (firstPos === "start") {
  startManagedServer()
    .then((code) => process.exit(code))
    .catch((error) => {
      console.error(`annovibe start: ${error instanceof Error ? error.message : String(error)}`);
      process.exit(1);
    });
  return;
}

if (firstPos === "restart") {
  const existingState = readState();
  const restartPort = cliArgs.port ?? existingState?.port ?? port;
  const restartHost = cliArgs.hostname ?? existingState?.hostname ?? hostname;
  stopManagedServer(true).then(async (code) => {
    if (code !== 0) process.exit(code);
    process.exit(await startManagedServerWithPort(restartPort, restartHost));
  }).catch((error) => {
    console.error(`annovibe restart: ${error instanceof Error ? error.message : String(error)}`);
    process.exit(1);
  });
  return;
}

if (firstPos === "update") {
  console.log(`\n  Updating ${PKG_NAME} to latest version...\n`);
  const npmExecPath = process.env.npm_execpath;
  const hasNpmExecPath = npmExecPath && fs.existsSync(npmExecPath);
  const command = hasNpmExecPath ? process.execPath : (process.platform === "win32" ? "npm.cmd" : "npm");
  const args = [
    ...(hasNpmExecPath ? [npmExecPath] : []),
    "install", "-g", `${PKG_NAME}@latest`,
  ];
  const child = spawn(command, args, { stdio: "inherit" });
  child.on("exit", (code) => process.exit(code ?? 0));
  return;
}

if (firstPos === "passwd") {
  const reset = positionals[1] === "--reset";

  if (reset) {
    if (fs.existsSync(authFile)) {
      fs.unlinkSync(authFile);
      console.log("   🔓 Password removed. Auth disabled.\n");
    } else {
      console.log("   No password configured.\n");
    }
    process.exit(0);
  }

  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
  const ask = (prompt) => new Promise((res) => rl.question(prompt, res));
  (async () => {
    const exists = fs.existsSync(authFile);
    if (exists) {
      console.log("\n   Changing password...\n");
      const oldPw = await ask("Current password: ");
      try {
        const cfg = JSON.parse(fs.readFileSync(authFile, "utf8"));
        if (!bcrypt.compareSync(oldPw, cfg.passwordHash)) {
          console.log("   ❌ Incorrect password.\n");
          rl.close();
          process.exit(1);
        }
      } catch { console.log("   ❌ Could not read auth file.\n"); rl.close(); process.exit(1); }
    }
    const pw1 = await ask("New password (empty to disable): ");
    if (!pw1.trim()) {
      if (fs.existsSync(authFile)) fs.unlinkSync(authFile);
      console.log("   🔓 Auth disabled.\n");
      rl.close();
      process.exit(0);
    }
    const pw2 = await ask("Confirm new password: ");
    if (pw1 !== pw2) {
      console.log("   ❌ Passwords don't match.\n");
      rl.close();
      process.exit(1);
    }
    const hash = bcrypt.hashSync(pw1, 10);
    const dir = path.dirname(authFile);
    if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
    fs.writeFileSync(authFile, JSON.stringify({ passwordHash: hash, enabled: true }, null, 2));
    console.log(exists ? "   ✅ Password changed.\n" : "   ✅ Password set.\n");
    rl.close();
    process.exit(0);
  })();
  return;
}

// ── Version check (async, non-blocking) ──

function checkVersion() {
  const req = https.get(`https://registry.npmjs.org/${encodeURIComponent(PKG_NAME)}/latest`, { timeout: 5000 }, (res) => {
    let data = "";
    res.on("data", (chunk) => data += chunk);
    res.on("end", () => {
      try {
        const json = JSON.parse(data);
        const latest = json.version;
        if (latest && latest !== VERSION) {
          console.log(`\n   ⚡ Update available: ${VERSION} → ${latest}`);
          console.log(`   Run: annovibe update\n`);
        }
      } catch { /* ignore */ }
    });
  });
  req.on("error", () => {});
  req.on("timeout", () => req.destroy());
}

// ── Normal startup ──

function promptPassword() {
  return new Promise((resolve) => {
    const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
    const ask = (prompt) => new Promise((res) => rl.question(prompt, res));
    (async () => {
      console.log("\n🔐 No password set. You can protect annovibe with a password.");
      console.log("   Leave empty and press Enter to skip.\n");
      const pw1 = await ask("Enter password: ");
      if (!pw1.trim()) {
        console.log("   Skipped — no password required.\n");
        rl.close();
        resolve(false);
        return;
      }
      const pw2 = await ask("Confirm password: ");
      if (pw1 !== pw2) {
        console.log("   Passwords don't match. Skipping.\n");
        rl.close();
        resolve(false);
        return;
      }
      const hash = bcrypt.hashSync(pw1, 10);
      const dir = path.dirname(authFile);
      if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
      fs.writeFileSync(authFile, JSON.stringify({ passwordHash: hash, enabled: true }, null, 2));
      console.log("   ✅ Password set. Restart won't prompt.\n");
      rl.close();
      resolve(true);
    })();
  });
}

async function maybeSetupAuth() {
  if (fs.existsSync(authFile)) return;
  const password = process.env.ANNOVIBE_PASSWORD ?? process.env.PIDEX_PASSWORD;
  if (password) {
    const hash = bcrypt.hashSync(password, 10);
    const dir = path.dirname(authFile);
    if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
    fs.writeFileSync(authFile, JSON.stringify({ passwordHash: hash, enabled: true }, null, 2));
    console.log(`   🔐 Password set from ${process.env.ANNOVIBE_PASSWORD ? "ANNOVIBE_PASSWORD" : "PIDEX_PASSWORD"} env var.`);
    return;
  }
  if (!process.stdin.isTTY) return;
  await promptPassword();
}

function seedDefaults() {
  const defaultSoulSrc = path.join(pkgDir, "lib", "default-soul.md");
  const soulDest = path.join(agentDir, "SOUL.md");
  if (!fs.existsSync(soulDest) && fs.existsSync(defaultSoulSrc)) {
    const dir = path.dirname(soulDest);
    if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
    fs.copyFileSync(defaultSoulSrc, soulDest);
    console.log("   🖖 Default SOUL.md seeded to ~/.pi/agent/SOUL.md");
  }

  const defaultHarnessSrc = path.join(pkgDir, "lib", "visualization-harness.md");
  const harnessDest = path.join(agentDir, "HARNESS.md");
  if (!fs.existsSync(harnessDest) && fs.existsSync(defaultHarnessSrc)) {
    const dir = path.dirname(harnessDest);
    if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
    fs.copyFileSync(defaultHarnessSrc, harnessDest);
    console.log("   📊 Default HARNESS.md seeded to ~/.pi/agent/HARNESS.md");
  }
}

// ── Start server ──
function openBrowser(url) {
  const isWindows = process.platform === "win32";
  const isMac = process.platform === "darwin";
  const openCmd = isWindows ? "start" : isMac ? "open" : "xdg-open";
  const openChild = spawn(openCmd, [url], { shell: isWindows, stdio: "ignore", detached: true });
  openChild.on("error", () => {});
  openChild.unref();
}

function startAutoRestartWatcher({ child, internalToken, writeCurrentState, markRestarting }) {
  if (!AUTO_RESTART_ENABLED) return () => {};

  let pending = null;
  let idleSince = null;
  let restarting = false;
  let lastWaitLogAt = 0;

  const poll = async () => {
    if (restarting) return;
    const installedVersion = readInstalledVersion();
    if (!installedVersion || installedVersion === VERSION) {
      if (pending) {
        pending = null;
        idleSince = null;
        writeCurrentState({ restartPending: false, installedVersion: null, restartDetectedAt: null, restartStartedAt: null });
      }
      return;
    }

    if (!pending || pending.installedVersion !== installedVersion) {
      pending = {
        installedVersion,
        detectedAt: nowIso(),
        detectedAtMs: Date.now(),
      };
      idleSince = null;
      lastWaitLogAt = 0;
      console.log(`annovibe auto-restart: detected installed version ${installedVersion}; current runtime is ${VERSION}`);
      writeCurrentState({
        restartPending: true,
        installedVersion,
        restartDetectedAt: pending.detectedAt,
      });
    }

    const status = await fetchRuntimeStatus(internalToken);
    if (!status || typeof status.busy !== "boolean") {
      idleSince = null;
      if (Date.now() - lastWaitLogAt >= 5 * 60_000) {
        lastWaitLogAt = Date.now();
        console.log("annovibe auto-restart: waiting for runtime status before restarting");
      }
      return;
    }

    if (status.busy) {
      idleSince = null;
      const waitedMs = Date.now() - pending.detectedAtMs;
      if (Date.now() - lastWaitLogAt >= 5 * 60_000) {
        lastWaitLogAt = Date.now();
        const busyCount = Array.isArray(status.busySessions) ? status.busySessions.length : 0;
        const suffix = waitedMs >= AUTO_RESTART_MAX_WAIT_MS ? "; max wait reached, still waiting for graceful idle" : "";
        console.log(`annovibe auto-restart: update pending, waiting for ${busyCount} busy session${busyCount === 1 ? "" : "s"}${suffix}`);
      }
      return;
    }

    idleSince ??= Date.now();
    if (Date.now() - idleSince < AUTO_RESTART_IDLE_GRACE_MS) return;

    restarting = true;
    let replacement;
    try {
      replacement = spawnDetachedServer({
        ANNOVIBE_NO_OPEN: "1",
        ANNOVIBE_WAIT_FOR_PORT: "1",
      });
    } catch (error) {
      console.error(`annovibe auto-restart: failed to start replacement supervisor: ${error instanceof Error ? error.message : String(error)}`);
      restarting = false;
      return;
    }
    if (!replacement.pid) {
      console.error("annovibe auto-restart: failed to start replacement supervisor");
      restarting = false;
      return;
    }
    markRestarting();
    writeCurrentState({
      restartPending: true,
      installedVersion: pending.installedVersion,
      restartDetectedAt: pending.detectedAt,
      restartStartedAt: nowIso(),
    });
    console.log(`annovibe auto-restart: installed ${VERSION} -> ${pending.installedVersion}; restarting now`);
    replacement.unref();
    console.log(`annovibe auto-restart: replacement supervisor pid ${replacement.pid}`);

    if (!(await terminatePid(child.pid))) {
      console.error(`annovibe auto-restart: failed to stop old next pid ${child.pid}`);
      restarting = false;
      markRestarting(false);
      return;
    }
    process.exit(0);
  };

  const timer = setInterval(() => {
    poll().catch((error) => {
      console.error(`annovibe auto-restart: ${error instanceof Error ? error.message : String(error)}`);
    });
  }, AUTO_RESTART_POLL_MS);
  timer.unref();

  poll().catch(() => {});
  return () => clearInterval(timer);
}

async function runServerProcess() {
  setStableProcessTitle(PROCESS_TITLE);
  await maybeSetupAuth();
  if (!fs.existsSync(nextDir)) {
    console.error("Build artifacts not found. Please report this issue.");
    process.exit(1);
  }

  checkVersion();

  if (process.env.ANNOVIBE_WAIT_FOR_PORT === "1") {
    const available = await waitForPortAvailable(port, hostname, AUTO_RESTART_WAIT_FOR_PORT_MS);
    if (!available) {
      console.error(`annovibe server: port ${port} was not released within ${AUTO_RESTART_WAIT_FOR_PORT_MS}ms`);
      process.exit(1);
    }
  }

  const url = getUrl(hostname, port);
  const nextArgs = [getCommandPath(), "__next", "--port", String(port)];
  if (hostname) nextArgs.push("--hostname", hostname);
  const internalToken = process.env.ANNOVIBE_INTERNAL_TOKEN || randomBytes(24).toString("hex");

  const child = spawn(process.execPath, nextArgs, {
    cwd: pkgDir,
    stdio: ["ignore", "pipe", "pipe"],
    env: {
      ...process.env,
      ANNOVIBE_INTERNAL_TOKEN: internalToken,
      ANNOVIBE_SUPERVISOR_PID: String(process.pid),
      ANNOVIBE_NEXT_WRAPPER: "1",
    },
  });
  let restarting = false;

  const writeCurrentState = (patch = {}) => {
    const current = readState() ?? {};
    writeState({
      schemaVersion: 1,
      version: VERSION,
      command: getServerCommand(),
      supervisorPid: process.pid,
      nextPid: child.pid ?? null,
      port: String(port),
      hostname,
      url,
      logFile,
      stateFile,
      startedAt: current.startedAt ?? nowIso(),
      ...patch,
    });
  };

  writeCurrentState();
  const stopAutoRestartWatcher = startAutoRestartWatcher({
    child,
    internalToken,
    writeCurrentState,
    markRestarting: (value = true) => { restarting = value; },
  });

  let browserOpened = false;
  child.stdout.on("data", (chunk) => {
    const text = chunk.toString();
    process.stdout.write(text);
    if (!browserOpened && text.includes("Ready")) {
      browserOpened = true;
      if (process.env.ANNOVIBE_NO_OPEN !== "1") openBrowser(url);
      console.log(`\n   annovibe running at ${url}\n`);
      writeCurrentState({ readyAt: nowIso() });
    }
  });
  child.stderr.on("data", (chunk) => process.stderr.write(chunk));

  const shutdown = async () => {
    stopAutoRestartWatcher();
    await terminatePid(child.pid);
    if (!restarting) removeState();
    process.exit(0);
  };
  process.on("SIGTERM", () => { void shutdown(); });
  process.on("SIGINT", () => { void shutdown(); });

  child.on("exit", (code) => {
    stopAutoRestartWatcher();
    if (!restarting) removeState();
    process.exit(code ?? 0);
  });
}

function runNextProcess() {
  setStableProcessTitle(NEXT_PROCESS_TITLE);
  if (!fs.existsSync(nextDir)) {
    console.error("Build artifacts not found. Please report this issue.");
    process.exit(1);
  }

  const nextArgs = ["start", "-p", String(port)];
  if (hostname) nextArgs.push("-H", String(hostname));
  process.argv = [process.execPath, nextBin, ...nextArgs];
  // eslint-disable-next-line @typescript-eslint/no-require-imports
  require(nextBin);
}

if (firstPos === "__server") {
  runServerProcess().catch((error) => {
    console.error(`annovibe server: ${error instanceof Error ? error.message : String(error)}`);
    process.exit(1);
  });
} else if (firstPos === "__next") {
  try {
    runNextProcess();
  } catch (error) {
    console.error(`annovibe next: ${error instanceof Error ? error.message : String(error)}`);
    process.exit(1);
  }
} else {
  startManagedServer()
    .then((code) => process.exit(code))
    .catch((error) => {
      console.error(`annovibe start: ${error instanceof Error ? error.message : String(error)}`);
      process.exit(1);
    });
}
