import { spawnSync } from "node:child_process";
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { dirname, join } from "node:path";
import { getConfigDir, getProfile } from "../config/index.js";
import { logger } from "../logger.js";
import { getCurrentVersion } from "../utils/version-check.js";
import { checkAccessibilityPermission, checkScreenRecordingPermission, checkInputMonitoringPermission, getCliclickBinaryPath, getNodeBinaryPath, } from "../onboarding/macos-permissions.js";
import { installLaunchAgent, printLaunchAgentStatus, uninstallLaunchAgent } from "./launchd.js";
const APP_NAME = "visionclaw";
const VISIONCLAW_DIR = join(homedir(), ".visionclaw");
const LAUNCHER_PATH = join(VISIONCLAW_DIR, "launcher.cjs");
const ECOSYSTEM_PATH = join(VISIONCLAW_DIR, "pm2-ecosystem.config.cjs");
// PM2 reads the displayed `version` column by walking upward from the
// process script (pm_exec_path) looking for the first `package.json`.
// Placing a small marker package.json next to launcher.cjs is the least
// invasive way to feed PM2 our version without polluting profile dirs.
const PM2_PACKAGE_JSON_PATH = join(VISIONCLAW_DIR, "package.json");
const TCC_PATHS_FILE = join(VISIONCLAW_DIR, "tcc-binary-paths.json");
const GREEN = "\x1b[32m";
const RED = "\x1b[31m";
const YELLOW = "\x1b[33m";
const DIM = "\x1b[2m";
const RESET = "\x1b[0m";
const BOLD = "\x1b[1m";
function ok(msg) {
    console.log(`  ${GREEN}✓${RESET} ${msg}`);
}
function warn(msg) {
    console.log(`  ${YELLOW}!${RESET} ${msg}`);
}
function fail(msg) {
    console.log(`  ${RED}✗${RESET} ${msg}`);
}
function ensureMac() {
    if (process.platform === "darwin")
        return true;
    console.log("VisionClaw daemon management is only supported on macOS.");
    return false;
}
function resolveBinary(name, installHint) {
    const result = spawnSync("which", [name], { encoding: "utf-8" });
    const resolved = result.stdout.trim();
    if (result.status === 0 && resolved.length > 0)
        return resolved;
    throw new Error(`Could not find ${name}.${installHint ? ` ${installHint}` : ""}`);
}
function run(command, args, inherit = true) {
    const result = spawnSync(command, args, {
        encoding: "utf-8",
        stdio: inherit ? "inherit" : ["ignore", "pipe", "pipe"],
    });
    return {
        status: result.status,
        stdout: typeof result.stdout === "string" ? result.stdout : "",
        stderr: typeof result.stderr === "string" ? result.stderr : "",
    };
}
function uniquePathEntries(entries) {
    const seen = new Set();
    const result = [];
    for (const entry of entries.flatMap((value) => value?.split(":") ?? [])) {
        if (!entry || seen.has(entry))
            continue;
        seen.add(entry);
        result.push(entry);
    }
    return result.join(":");
}
function buildDaemonPath() {
    const pnpmPath = spawnSync("which", ["pnpm"], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).stdout.trim();
    return uniquePathEntries([
        join(homedir(), "Library", "pnpm"),
        join(homedir(), "Library", "pnpm", "bin"),
        pnpmPath ? dirname(pnpmPath) : undefined,
        "/opt/homebrew/bin",
        "/opt/homebrew/sbin",
        "/usr/local/bin",
        process.env.PATH,
    ]);
}
function writeLauncher() {
    mkdirSync(VISIONCLAW_DIR, { recursive: true });
    writeFileSync(LAUNCHER_PATH, generateLauncherScript(), "utf-8");
}
/**
 * Write a small marker package.json next to the PM2 launcher script so PM2's
 * `findPackageVersion()` (which walks upward from `pm_exec_path`) can populate
 * the `version` column in `pm2 list` / `pm2 status`. The file lives at
 * `~/.visionclaw/package.json` and contains only `name` + `version`.
 *
 * This is intentionally NOT placed inside the profile dir (`getConfigDir()`)
 * because Claude Code CLI runs with `cwd = getConfigDir()` and walks parents
 * to locate project metadata; a synthetic package.json there could confuse it.
 */
function writePm2PackageJson(version) {
    mkdirSync(VISIONCLAW_DIR, { recursive: true });
    writeFileSync(PM2_PACKAGE_JSON_PATH, `${JSON.stringify({ name: APP_NAME, version, private: true }, null, 2)}\n`, "utf-8");
}
function writePm2Ecosystem(nodePath, options) {
    const version = getCurrentVersion();
    writePm2PackageJson(version);
    const ecosystem = {
        apps: [{
                name: APP_NAME,
                script: LAUNCHER_PATH,
                interpreter: nodePath,
                cwd: getConfigDir(),
                args: buildAgentArgs(options),
                version,
                env: {
                    VISIONCLAW_VERSION: version,
                    PATH: buildDaemonPath(),
                },
            }],
    };
    writeFileSync(ECOSYSTEM_PATH, `module.exports = ${JSON.stringify(ecosystem, null, 2)};\n`, "utf-8");
}
export function generateLauncherScript() {
    return `#!/usr/bin/env node
const { execFileSync } = require("node:child_process");
const { existsSync, readFileSync, readdirSync } = require("node:fs");
const { homedir } = require("node:os");
const { dirname, isAbsolute, join } = require("node:path");

function output(cmd, args) {
  try { return execFileSync(cmd, args, { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim(); }
  catch { return ""; }
}

function unique(values) {
  return [...new Set(values.filter(Boolean))];
}

function shimCandidates() {
  return unique([
    output("which", ["visionclaw"]),
    join(homedir(), "Library", "pnpm", "visionclaw"),
    process.env.PNPM_HOME ? join(process.env.PNPM_HOME, "visionclaw") : "",
    "/opt/homebrew/bin/visionclaw",
    "/usr/local/bin/visionclaw",
  ]);
}

function expandShimPath(candidate, shim) {
  const basedir = dirname(shim);
  const expanded = candidate
    .replaceAll("\${basedir}", basedir)
    .replaceAll("$basedir", basedir);
  return isAbsolute(expanded) ? expanded : join(basedir, expanded);
}

function candidatesFromShim() {
  const candidates = [];
  for (const shim of shimCandidates()) {
    if (!existsSync(shim)) continue;
    try {
      const content = readFileSync(shim, "utf-8");
      const doubleQuoted = new RegExp('"([^"]*visionclaw[^"]*dist-agent/bundle[.]cjs)"', 'g');
      const singleQuoted = new RegExp("'([^']*visionclaw[^']*dist-agent/bundle[.]cjs)'", 'g');
      const matches = [
        ...[...content.matchAll(doubleQuoted)].map((m) => m[1]),
        ...[...content.matchAll(singleQuoted)].map((m) => m[1]),
      ];
      for (const match of matches) {
        candidates.push(match, expandShimPath(match, shim));
      }
    } catch { /* ignore */ }
  }
  return unique(candidates);
}

function defaultGlobalRoots() {
  return unique([
    join(homedir(), "Library", "pnpm", "global", "5", "node_modules"),
    join(homedir(), ".local", "share", "pnpm", "global", "5", "node_modules"),
    process.env.PNPM_HOME ? join(process.env.PNPM_HOME, "global", "5", "node_modules") : "",
  ]);
}

function pnpmStoreCandidates(root) {
  const candidates = [];
  const store = join(dirname(root), ".pnpm");
  try {
    for (const entry of readdirSync(store, { withFileTypes: true })) {
      if (entry.isDirectory() && entry.name.startsWith("visionclaw@")) {
        candidates.push(join(store, entry.name, "node_modules", "visionclaw", "dist-agent", "bundle.cjs"));
      }
    }
  } catch { /* ignore */ }
  return candidates;
}

function globalRootCandidates(root) {
  if (!root) return [];
  const direct = [
    join(root, "visionclaw", "dist-agent", "bundle.cjs"),
    join(root, "node_modules", "visionclaw", "dist-agent", "bundle.cjs"),
  ];
  try {
    for (const entry of readdirSync(root, { withFileTypes: true })) {
      if (entry.isDirectory()) {
        direct.push(join(root, entry.name, "node_modules", "visionclaw", "dist-agent", "bundle.cjs"));
      }
    }
  } catch { /* ignore */ }
  return unique([...direct, ...pnpmStoreCandidates(root)]);
}

function resolverDiagnostics(roots, candidates) {
  const lines = [
    "Could not resolve visionclaw/dist-agent/bundle.cjs from the current global install.",
    "which visionclaw: " + (output("which", ["visionclaw"]) || "not found"),
    "pnpm root -g: " + (output("pnpm", ["root", "-g"]) || "failed"),
    "npm root -g: " + (output("npm", ["root", "-g"]) || "failed"),
    "candidate count: " + candidates.length,
  ];
  const packageDirs = roots.map((root) => join(root, "visionclaw")).filter(existsSync);
  if (packageDirs.length > 0) lines.push("existing package dirs: " + packageDirs.join(", "));
  if (candidates.length > 0) lines.push("first candidates: " + candidates.slice(0, 5).join(", "));
  return lines.join("\\n");
}

function resolveBundle() {
  const commandRoots = [output("pnpm", ["root", "-g"]), output("npm", ["root", "-g"])].filter(Boolean);
  const roots = unique([...commandRoots, ...defaultGlobalRoots()]);
  const candidates = [...candidatesFromShim(), ...roots.flatMap(globalRootCandidates)];
  for (const candidate of candidates) {
    if (existsSync(candidate)) return candidate;
  }
  for (const root of roots) {
    try { return require.resolve("visionclaw/dist-agent/bundle.cjs", { paths: [root] }); }
    catch { /* try next */ }
  }
  throw new Error(resolverDiagnostics(roots, candidates));
}

try {
  const bundle = resolveBundle();
  require(bundle);
} catch (err) {
  console.error("[visionclaw launcher] " + (err instanceof Error ? err.message : String(err)));
  console.error("Try reinstalling VisionClaw: pnpm add -g visionclaw@latest");
  process.exit(1);
}
`;
}
function hardFailIfRoot() {
    if (typeof process.getuid === "function" && process.getuid() === 0) {
        fail("Do not run VisionClaw daemon commands with sudo/root.");
        console.log("  macOS TCC permissions must belong to the login user, not root.");
        return true;
    }
    if (process.env.SUDO_USER) {
        fail("Do not run VisionClaw daemon commands through sudo.");
        console.log("  Re-run as the normal login user.");
        return true;
    }
    return false;
}
function warnIfNoGuiSession() {
    const uid = typeof process.getuid === "function" ? process.getuid() : null;
    if (uid === null)
        return;
    const result = spawnSync("launchctl", ["print", `gui/${uid}`], { stdio: "ignore" });
    if (result.status !== 0) {
        warn(`No active launchd GUI session detected for gui/${uid}. Autostart may wait until the user logs in.`);
    }
}
function readSavedTccPaths() {
    try {
        if (!existsSync(TCC_PATHS_FILE))
            return null;
        return JSON.parse(readFileSync(TCC_PATHS_FILE, "utf-8"));
    }
    catch {
        return null;
    }
}
async function warnIfPermissionsLookWrong() {
    const saved = readSavedTccPaths();
    if (saved) {
        const nodePath = getNodeBinaryPath();
        const cliclickPath = getCliclickBinaryPath();
        if (saved.nodePath && nodePath && saved.nodePath !== nodePath) {
            warn(`Node path changed since prepare-mac: ${saved.nodePath} → ${nodePath}`);
            console.log(`    Run ${DIM}visionclaw prepare-mac${RESET} to refresh macOS permissions.`);
        }
        if (saved.cliclickPath && cliclickPath && saved.cliclickPath !== cliclickPath) {
            warn(`cliclick path changed since prepare-mac: ${saved.cliclickPath} → ${cliclickPath}`);
            console.log(`    Run ${DIM}visionclaw prepare-mac${RESET} to refresh macOS permissions.`);
        }
    }
    if (!saved)
        return;
    const checks = [
        ["Screen Recording", await checkScreenRecordingPermission()],
        ["Accessibility", await checkAccessibilityPermission()],
        ["Input Monitoring", await checkInputMonitoringPermission()],
    ];
    const missing = checks.filter(([, granted]) => !granted).map(([label]) => label);
    if (missing.length > 0) {
        warn(`Some macOS permissions appear missing: ${missing.join(", ")}`);
        console.log(`    Run ${DIM}visionclaw prepare-mac${RESET} if desktop automation does not work.`);
    }
}
async function preflight() {
    if (!ensureMac())
        return false;
    if (hardFailIfRoot())
        return false;
    warnIfNoGuiSession();
    await warnIfPermissionsLookWrong();
    return true;
}
function buildAgentArgs(options) {
    const args = [];
    if (options.debug)
        args.push("--debug");
    if (options.profile && options.profile !== "default")
        args.push("--profile", options.profile);
    return args;
}
/**
 * Regenerate the PM2 ecosystem file with the current VisionClaw version,
 * preserving args/cwd/interpreter from the existing file. Intended for use
 * after a self-upgrade so PM2's `pm2 list` reflects the freshly installed
 * version.
 *
 * Returns true on success, false if no existing ecosystem file is found
 * (caller should fall back to a normal pm2 restart).
 */
export function refreshPm2EcosystemVersion(version = getCurrentVersion()) {
    if (!existsSync(ECOSYSTEM_PATH))
        return false;
    try {
        const raw = readFileSync(ECOSYSTEM_PATH, "utf-8");
        // The file is `module.exports = { ... };\n` — extract the JSON-ish object.
        const match = /module\.exports\s*=\s*(\{[\s\S]*\});?\s*$/.exec(raw.trim());
        if (!match) {
            logger.warn(`PM2 ecosystem file ${ECOSYSTEM_PATH} did not match expected shape; skipping version refresh.`);
            return false;
        }
        const parsed = JSON.parse(match[1]);
        if (!parsed.apps?.[0]) {
            logger.warn(`PM2 ecosystem file ${ECOSYSTEM_PATH} has no apps[0]; skipping version refresh.`);
            return false;
        }
        writePm2PackageJson(version);
        parsed.apps[0].version = version;
        parsed.apps[0].env = {
            ...(parsed.apps[0].env ?? {}),
            VISIONCLAW_VERSION: version,
            PATH: buildDaemonPath(),
        };
        writeFileSync(ECOSYSTEM_PATH, `module.exports = ${JSON.stringify(parsed, null, 2)};\n`, "utf-8");
        return true;
    }
    catch (err) {
        logger.warn(`Failed to refresh PM2 ecosystem version: ${err instanceof Error ? err.message : String(err)}`);
        return false;
    }
}
export function getEcosystemPath() {
    return ECOSYSTEM_PATH;
}
function findPm2Process() {
    const result = spawnSync("pm2", ["jlist"], {
        encoding: "utf-8",
        stdio: ["ignore", "pipe", "pipe"],
    });
    if (result.status !== 0 || !result.stdout.trim())
        return null;
    try {
        const processes = JSON.parse(result.stdout);
        return processes.find((proc) => proc.name === APP_NAME) ?? null;
    }
    catch {
        return null;
    }
}
function formatBytes(bytes) {
    if (!bytes || bytes <= 0)
        return "0b";
    if (bytes < 1024 * 1024)
        return `${Math.round(bytes / 1024)}kb`;
    return `${(bytes / 1024 / 1024).toFixed(1)}mb`;
}
function formatUptime(startMs) {
    if (!startMs)
        return "unknown";
    const seconds = Math.max(0, Math.floor((Date.now() - startMs) / 1000));
    if (seconds < 60)
        return `${seconds}s`;
    const minutes = Math.floor(seconds / 60);
    if (minutes < 60)
        return `${minutes}m`;
    const hours = Math.floor(minutes / 60);
    return `${hours}h ${minutes % 60}m`;
}
export async function startDaemon(options = {}) {
    if (!(await preflight()))
        return;
    writeLauncher();
    const nodePath = resolveBinary("node", "Install Node.js >= 24.12.0.");
    resolveBinary("pm2", "Install it globally: npm install -g pm2");
    mkdirSync(getConfigDir(), { recursive: true });
    run("pm2", ["delete", APP_NAME], false);
    writePm2Ecosystem(nodePath, options);
    const result = run("pm2", ["start", ECOSYSTEM_PATH], !options.installed);
    if (result.status === 0) {
        if (!options.installed)
            ok("VisionClaw daemon started");
    }
    else {
        fail("Failed to start VisionClaw daemon via pm2");
    }
}
export function stopDaemon() {
    if (!ensureMac())
        return;
    resolveBinary("pm2", "Install it globally: npm install -g pm2");
    const result = run("pm2", ["stop", APP_NAME]);
    if (result.status === 0)
        ok("VisionClaw daemon stopped");
}
export async function restartDaemon(options = {}) {
    await startDaemon(options);
}
export function printDaemonStatus() {
    if (!ensureMac())
        return;
    console.log(`\n${BOLD}VisionClaw Daemon Status${RESET}\n`);
    printLaunchAgentStatus();
    const proc = findPm2Process();
    if (!proc) {
        warn("pm2 process: not found");
        console.log(`    Start it with ${DIM}visionclaw daemon start${RESET}\n`);
        return;
    }
    const status = proc.pm2_env?.status ?? "unknown";
    const script = proc.pm2_env?.pm_exec_path ?? "unknown";
    const statusColor = status === "online" ? GREEN : YELLOW;
    ok(`pm2 process: ${statusColor}${status}${RESET}`);
    console.log(`    id:       ${proc.pm_id ?? "unknown"}`);
    console.log(`    pid:      ${proc.pid ?? "unknown"}`);
    console.log(`    uptime:   ${formatUptime(proc.pm2_env?.pm_uptime)}`);
    console.log(`    restarts: ${proc.pm2_env?.restart_time ?? 0}`);
    console.log(`    cpu:      ${proc.monit?.cpu ?? 0}%`);
    console.log(`    memory:   ${formatBytes(proc.monit?.memory)}`);
    console.log(`    script:   ${script}`);
    if (script !== LAUNCHER_PATH) {
        warn(`pm2 is not using the stable launcher (${LAUNCHER_PATH})`);
        console.log(`    Fix with ${DIM}visionclaw daemon start${RESET}`);
    }
    console.log("");
}
export function showDaemonLogs() {
    if (!ensureMac())
        return;
    resolveBinary("pm2", "Install it globally: npm install -g pm2");
    run("pm2", ["logs", APP_NAME, "--lines", "200"]);
}
export async function installDaemon(options = {}) {
    if (!(await preflight()))
        return;
    writeLauncher();
    installLaunchAgent({ profile: options.profile ?? getProfile(), debug: options.debug ?? false });
    await new Promise((resolve) => setTimeout(resolve, 3000));
    const proc = findPm2Process();
    if (proc?.pm2_env?.status === "online") {
        ok("pm2 reports VisionClaw online");
        run("pm2", ["save", "--force"], false);
        ok("pm2 process list saved");
    }
    else {
        warn("VisionClaw is not online in pm2 after install");
        console.log(`    Check ${DIM}visionclaw daemon logs${RESET}`);
        console.log(`    Check ${DIM}~/.visionclaw/logs/launchd-stderr.log${RESET}`);
    }
}
export function uninstallDaemon() {
    if (!ensureMac())
        return;
    uninstallLaunchAgent();
    run("pm2", ["delete", APP_NAME], false);
    run("pm2", ["save", "--force"], false);
    ok("pm2 process cleaned up and process list saved");
}
function printUsage() {
    console.log("Usage: visionclaw daemon <start|stop|restart|status|logs|install|uninstall> [--profile <name>] [--debug]");
}
export async function runDaemonCli(subAction, args, options) {
    const installed = args.includes("--installed");
    const daemonOptions = { ...options, installed };
    switch (subAction) {
        case "start":
            await startDaemon(daemonOptions);
            break;
        case "stop":
            stopDaemon();
            break;
        case "restart":
            await restartDaemon(daemonOptions);
            break;
        case "status":
            printDaemonStatus();
            break;
        case "logs":
            showDaemonLogs();
            break;
        case "install":
            await installDaemon(daemonOptions);
            break;
        case "uninstall":
            uninstallDaemon();
            break;
        default:
            printUsage();
            break;
    }
}
//# sourceMappingURL=daemon.js.map