#!/usr/bin/env bun
/**
 * `jun` CLI — entry point of the published junhost package.
 *
 * The daemon requires the Bun runtime (Bun.serve/Bun.spawn); the shebang
 * targets bun directly, and the guard below catches the npm/Node case with a
 * pointer instead of a cryptic crash.
 */

if (typeof Bun === "undefined") {
  console.error(
    "Jun runs on Bun, not Node. Install Bun (https://bun.sh) and reinstall:\n" +
      "  bun install -g junhost",
  );
  process.exit(1);
}

import path from "node:path";
import { existsSync } from "node:fs";

const root = path.resolve(import.meta.dir, "..");
const command = process.argv[2] ?? "start";

function version() {
  const pkg = require(path.join(root, "package.json"));
  return pkg.version;
}

function help() {
  console.log(`jun ${version()} — self-hosted Codex agent dashboard

Usage: jun <command>

Commands:
  start         Start the daemon + dashboard (default; http://127.0.0.1:8787)
  stop          Stop the daemon (systemd service when present)
  open          Open the dashboard in your browser
  doctor        Check prerequisites + live health (codex, browser, engine)
  upgrade       Update junhost to the latest published version
  reset --yes   Delete all Jun data (JUN_HOME) and re-bootstrap on next start
  mcp-server    Run the self-management MCP server (spawned by Codex, stdio)
  version       Print the version

Environment:
  JUN_HOME      Data directory (default ~/.jun)
  JUN_HOST      Bind address (default 127.0.0.1)
  JUN_PORT      Port (default 8787)`);
}

async function doctor() {
  const os = await import("node:os");
  const fs = await import("node:fs");
  let failed = false;
  let warned = false;
  // ok | warn | fail, with an optional hint and an indented detail line.
  const report = (state, label, hint, detail) => {
    const tag = state === "ok" ? "  ok  " : state === "warn" ? " warn " : " FAIL ";
    console.log(`${tag} ${label}${hint && state !== "ok" ? ` — ${hint}` : ""}`);
    if (detail) console.log(`       ${detail}`);
    if (state === "fail") failed = true;
    if (state === "warn") warned = true;
  };
  const probe = async (argv) => {
    try {
      const p = Bun.spawn(argv, { stdout: "pipe", stderr: "ignore" });
      const out = await new Response(p.stdout).text();
      await p.exited;
      return { code: p.exitCode, out: out.trim() };
    } catch {
      return { code: -1, out: "" };
    }
  };

  console.log("Runtime & prerequisites");
  report("ok", `bun ${Bun.version}`);

  // Codex's npm bin is a Node launcher (#!/usr/bin/env node) — without node on
  // PATH, `codex app-server` dies with "env: node: No such file" (hit on a fresh
  // VPS). So node is a hard requirement even though Jun itself runs on Bun.
  const node = Bun.which("node");
  if (node) report("ok", `node (${(await probe([node, "--version"])).out || node})`);
  else report("fail", "node not found", "Codex's launcher needs it: apt install nodejs (or nvm)");

  const codex = Bun.which("codex");
  if (codex) {
    const v = await probe([codex, "--version"]);
    report("ok", `codex CLI — ${v.out || codex}`);
  } else {
    report("fail", "codex CLI not found", "install with: npm i -g @openai/codex");
  }

  const cloakRoot = path.join(os.homedir(), ".cloakbrowser");
  let cloakChrome = null;
  if (existsSync(cloakRoot)) {
    const dir = fs.readdirSync(cloakRoot).filter((d) => d.startsWith("chromium-")).sort().pop();
    if (dir) {
      const exe = path.join(cloakRoot, dir, process.platform === "win32" ? "chrome.exe" : "chrome");
      if (existsSync(exe)) cloakChrome = exe;
    }
  }
  if (cloakChrome) report("ok", `stealth browser engine (cloakbrowser ${path.basename(path.dirname(cloakChrome))})`);
  else report("warn", "stealth browser engine not downloaded", "fetched on first start (~535MB), or: bun x cloakbrowser install");

  // The silent killer on Linux: the cloak binary downloads fine but won't launch
  // because Chromium's system libs are missing (libatk/libnss/libgbm…), and
  // agent-browser silently falls back to a blockable engine. Catch it via ldd.
  if (process.platform === "linux" && cloakChrome) {
    const ldd = await probe(["ldd", cloakChrome]);
    const missing = ldd.out
      .split("\n")
      .filter((l) => l.includes("not found"))
      .map((l) => l.trim().split(" ")[0]);
    if (missing.length) {
      report(
        "fail",
        `chromium system libs missing (${missing.length})`,
        "apt install libnss3 libatk1.0-0t64 libatk-bridge2.0-0t64 libcups2t64 libgbm1 libasound2t64 libxkbcommon0 libpango-1.0-0 libxcomposite1 libxdamage1 libxrandr2 libatspi2.0-0",
        missing.slice(0, 6).join(", ") + (missing.length > 6 ? " …" : ""),
      );
    } else {
      report("ok", "chromium system libraries present");
    }
  }

  // Live daemon checks — only if it's actually up on JUN_HOST:JUN_PORT.
  const host = process.env.JUN_HOST ?? process.env.AGENTHOST_HOST ?? "127.0.0.1";
  const port = Number(process.env.JUN_PORT ?? process.env.AGENTHOST_PORT ?? 8787);
  const base = `http://${host === "0.0.0.0" ? "127.0.0.1" : host}:${port}`;
  const getJson = async (p) => {
    try {
      const res = await fetch(`${base}${p}`, { signal: AbortSignal.timeout(4000) });
      return res.ok ? await res.json() : null;
    } catch {
      return null;
    }
  };

  console.log(`\nDaemon (${base})`);
  const health = await getJson("/api/health");
  if (!health) {
    report("warn", "daemon not running (or unreachable)", `start it with: jun start  —  checked ${base}`);
  } else {
    const status = await getJson("/api/status");
    report("ok", `daemon up — junhost ${status?.version ?? "?"}`);
    if (status?.updateAvailable) {
      report("warn", `update available: ${status.latestVersion}`, "run: jun upgrade");
    }
    if (status?.engine?.crashLooping) {
      report(
        "fail",
        `Codex engine crash-looping (backoff ${Math.ceil((status.engine.backoffMs || 0) / 1000)}s)`,
        `last exit code ${status.engine.lastExitCode} — check auth/memory, journalctl -u jun`,
      );
    }

    const account = await getJson("/api/account");
    if (account?.account) {
      const a = account.account;
      report("ok", `Codex signed in — ${a.email ?? a.type}${a.planType ? ` (${a.planType})` : ""}`);
    } else if (account?.requiresOpenaiAuth) {
      report("fail", "Codex NOT signed in", "open Settings and sign in (device-code), else runs fail with 401");
    } else {
      report("warn", "Codex auth status unknown");
    }

    const browser = await getJson("/api/browser/status");
    const b = browser?.status;
    if (b?.running) {
      report("ok", `browser session running — ${b.url || "about:blank"}${b.streaming ? ", streaming" : ""}`);
    } else {
      report("warn", "browser session not running", "starts on demand from the Browser page");
    }
  }

  console.log(
    `\n${failed ? "✗ Problems found — fix the FAIL items above." : warned ? "✓ Healthy (some warnings)." : "✓ All good."}`,
  );
  process.exit(failed ? 1 : 0);
}

switch (command) {
  case "start":
    await import(path.join(root, "dist", "index.js"));
    break;
  case "mcp-server":
    await import(path.join(root, "dist", "mcp-server.js"));
    break;
  case "stop": {
    // The daemon normally runs under systemd; stop the service if it's there.
    const systemctl = Bun.which("systemctl");
    if (systemctl) {
      const r = Bun.spawnSync([systemctl, "stop", "jun"], { stdout: "inherit", stderr: "inherit" });
      process.exit(r.exitCode ?? 0);
    }
    console.log(
      "No systemd service found. If you ran `jun start` in a terminal, stop it with Ctrl-C.",
    );
    break;
  }
  case "open": {
    const host = process.env.JUN_HOST ?? process.env.AGENTHOST_HOST ?? "127.0.0.1";
    const port = process.env.JUN_PORT ?? process.env.AGENTHOST_PORT ?? "8787";
    const dashUrl = `http://${host === "0.0.0.0" ? "127.0.0.1" : host}:${port}`;
    const opener =
      process.platform === "win32"
        ? ["cmd", "/c", "start", "", dashUrl]
        : process.platform === "darwin"
          ? ["open", dashUrl]
          : ["xdg-open", dashUrl];
    try {
      Bun.spawn(opener, { stdout: "ignore", stderr: "ignore" });
    } catch {
      /* headless / no opener — the URL print below is the fallback */
    }
    console.log(`Dashboard: ${dashUrl}`);
    break;
  }
  case "reset": {
    const os = await import("node:os");
    const fs = await import("node:fs");
    const dataDir =
      process.env.JUN_HOME ?? process.env.AGENTHOST_HOME ?? path.join(os.homedir(), ".jun");
    if (process.argv[3] !== "--yes") {
      console.error(
        `This will DELETE all Jun data (runs, projects, schedules, browser profile) at:\n` +
          `  ${dataDir}\n\nRe-run to confirm:  jun reset --yes`,
      );
      process.exit(1);
    }
    if (existsSync(dataDir)) {
      fs.rmSync(dataDir, { recursive: true, force: true });
      console.log(`Removed ${dataDir}.`);
    } else {
      console.log(`Nothing to remove (${dataDir} does not exist).`);
    }
    console.log("Run `jun start` to re-bootstrap a fresh data directory.");
    break;
  }
  case "doctor":
    await doctor();
    break;
  case "upgrade": {
    console.log(`Current version: ${version()}`);
    const proc = Bun.spawnSync([process.execPath, "install", "-g", "junhost@latest"], {
      stdout: "inherit",
      stderr: "inherit",
    });
    if (proc.exitCode !== 0) {
      console.error(
        "\nUpgrade failed. If you installed with npm instead of bun, run:\n" +
          "  npm install -g junhost@latest",
      );
      process.exit(proc.exitCode ?? 1);
    }
    console.log("\nUpgrade complete. Restart the daemon (jun start) to run the new version.");
    break;
  }
  case "version":
  case "--version":
  case "-v":
    console.log(version());
    break;
  case "help":
  case "--help":
  case "-h":
    help();
    break;
  default:
    console.error(`Unknown command: ${command}\n`);
    help();
    process.exit(1);
}
