#!/usr/bin/env node
import { copyFile, cp, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
import fssync from "node:fs";
import os from "node:os";
import path from "node:path";
import { spawn, spawnSync } from "node:child_process";
import { fileURLToPath } from "node:url";

const DEFAULT_GATEWAY = "https://ai-hub.nodedc.ru";
const PACKAGE_NAME = "@nodedc/ai-workspace-bridge";
const SERVICE_NAME = "NDC AI Workspace Bridge";
const WINDOWS_TASK_NAME = "NDCAIWorkspaceBridge";
const HEALTH_PATH = "/api/ai-workspace/bridge/v1/health";
const BRIDGE_PROTOCOL_VERSION = "ai-workspace-bridge.v1";
const BRIDGE_CAPABILITIES = [
  "hub-outbound",
  "local-health",
  "codex-exec",
  "codex-resume",
  "codex-stop",
  "dynamic-run-profile",
  "dynamic-mcp",
  "bundled-skills",
  "self-update",
];
const DEFAULT_CODEX_ARGS = [
  "exec",
  "--json",
  "--skip-git-repo-check",
  "-c",
  "model_reasoning_summary=detailed",
  "-c",
  "model_supports_reasoning_summaries=true",
  "-c",
  "use_experimental_reasoning_summary=true",
  "-c",
  "hide_agent_reasoning=false",
  "-c",
  "show_raw_agent_reasoning=false",
  "-",
];

const CLI_FILE = fileURLToPath(import.meta.url);
const PACKAGE_ROOT = path.resolve(path.dirname(CLI_FILE), "..");
const ASSETS_DIR = path.join(PACKAGE_ROOT, "assets");
const PACKAGE_VERSION = readPackageVersion();

class UsageError extends Error {}

async function main() {
  const args = parseArgs(process.argv.slice(2));
  if (args.help) {
    printHelp(args.command);
    return;
  }

  if (args.command === "setup") return runSetup(args);
  if (args.command === "status") return runStatus(args, { doctor: false });
  if (args.command === "doctor") return runStatus(args, { doctor: true });
  if (args.command === "update") return runUpdate(args);
  if (args.command === "start") return runStart(args);
  if (args.command === "stop") return runStop(args);
  if (args.command === "logs") return runLogs(args);

  throw new UsageError(`Unknown command: ${args.command}`);
}

async function runSetup(args) {
  if (!args.setupCode) {
    throw new UsageError(`Missing setup code. Use: ai-workspace-bridge setup <setup-code>`);
  }
  ensureNodeRuntime();

  const gateway = cleanHttpEndpoint(args.gateway || process.env.NODEDC_AI_WORKSPACE_GATEWAY || DEFAULT_GATEWAY);
  if (!gateway) throw new UsageError("--gateway must be an http(s) URL.");

  console.log(`${SERVICE_NAME} setup`);
  console.log(`Gateway: ${gateway}`);
  const redeemPayload = await redeemSetupCode(gateway, args.setupCode);
  const setup = normalizeSetupPayload(redeemPayload);
  const bridge = setup.bridge;

  const installRoot = path.resolve(expandHome(args.installRoot || defaultInstallRoot()));
  const workspace = path.resolve(expandHome(args.workspace || bridge.workspace || process.cwd()));
  const port = sanitizePort(args.port || bridge.port || 8787);
  const codexHome = path.join(installRoot, "codex-home");
  const paths = buildInstallPaths(installRoot);
  const pathDirs = resolvePathDirs();

  await mkdir(paths.logsDir, { recursive: true });
  await mkdir(workspace, { recursive: true });
  await copyFile(path.join(ASSETS_DIR, "worker.mjs"), paths.workerPath);
  await copyFile(path.join(ASSETS_DIR, "ndcAgentMcpServer.mjs"), paths.ndcAgentMcpServerPath);
  await writeStartScript(paths.startScriptPath);
  await writeConfig(paths.configPath, {
    service: SERVICE_NAME,
    packageName: PACKAGE_NAME,
    packageVersion: PACKAGE_VERSION,
    agentVersion: PACKAGE_VERSION,
    protocolVersion: BRIDGE_PROTOCOL_VERSION,
    capabilities: BRIDGE_CAPABILITIES,
    installedAt: new Date().toISOString(),
    gateway,
    installRoot,
    workspace,
    port,
    codexHome,
    worker: paths.workerPath,
    ndcAgentMcpServer: paths.ndcAgentMcpServerPath,
    startScript: paths.startScriptPath,
    pidPath: paths.pidPath,
    logPath: paths.logPath,
    healthPath: HEALTH_PATH,
    nodePath: process.execPath,
    pathDirs,
    hubUrl: bridge.hubUrl,
    hubUrls: bridge.hubUrls,
    pairingCode: bridge.pairingCode,
    machineName: bridge.machineName || os.hostname(),
    appMcpServers: bridge.appMcpServers,
  });

  if (!args.noCodexInstall) {
    await ensureCodexCli({ pathDirs });
  }
  await setupCodexHome(codexHome, bridge.appMcpServers);
  if (!args.noCodexLogin) {
    await ensureCodexLogin(codexHome, { pathDirs });
  }

  if (!args.noAutostart) {
    await installAutostart(paths.startScriptPath, installRoot).catch((error) => {
      console.warn(`Autostart was not registered: ${error.message}`);
    });
  }

  await stopBridge(installRoot).catch(() => {});
  await startBridge(installRoot);
  const health = await waitForHealth(port, 15000);

  console.log(`${SERVICE_NAME} setup complete.`);
  console.log(`Install root: ${installRoot}`);
  console.log(`Workspace: ${workspace}`);
  console.log(`Health: ${health.ok ? "online" : "not ready"}`);
  console.log(`Run: ai-workspace-bridge doctor`);
}

async function runStatus(args, { doctor }) {
  const installRoot = path.resolve(expandHome(args.installRoot || defaultInstallRoot()));
  const config = await readLocalConfig(installRoot);
  if (!config) {
    console.log(`${SERVICE_NAME} is not installed at ${installRoot}`);
    process.exitCode = 1;
    return;
  }

  const health = await fetchHealth(config.port);
  const codex = doctor ? inspectCodex(config.codexHome, config.pathDirs || []) : null;
  console.log(`${SERVICE_NAME} status`);
  console.log(`Install root: ${config.installRoot || installRoot}`);
  console.log(`Workspace: ${config.workspace || ""}`);
  console.log(`Port: ${config.port || ""}`);
  console.log(`Agent: ${config.agentVersion || config.packageVersion || "unknown"}`);
  console.log(`Protocol: ${config.protocolVersion || "unknown"}`);
  if (Array.isArray(config.capabilities) && config.capabilities.length) {
    console.log(`Capabilities: ${config.capabilities.join(", ")}`);
  }
  console.log(`Hub: ${health.ok ? "online" : "offline"}`);
  if (doctor) {
    console.log(`Node: ${process.version}`);
    console.log(`Codex CLI: ${codex.codexOk ? codex.codexVersion || "ok" : "missing"}`);
    console.log(`Codex auth: ${codex.authOk ? "present" : "missing"}`);
    if (codex.error) console.log(`Codex detail: ${codex.error}`);
  }
  process.exitCode = health.ok && (!doctor || (codex.codexOk && codex.authOk)) ? 0 : 1;
}

async function runStart(args) {
  const installRoot = path.resolve(expandHome(args.installRoot || defaultInstallRoot()));
  await startBridge(installRoot);
  console.log(`${SERVICE_NAME} started.`);
}

async function runStop(args) {
  const installRoot = path.resolve(expandHome(args.installRoot || defaultInstallRoot()));
  await stopBridge(installRoot);
  console.log(`${SERVICE_NAME} stopped.`);
}

async function runLogs(args) {
  const installRoot = path.resolve(expandHome(args.installRoot || defaultInstallRoot()));
  const config = await readLocalConfig(installRoot);
  const logPath = config?.logPath || path.join(installRoot, "logs", "bridge.log");
  const text = await readFile(logPath, "utf8").catch(() => "");
  console.log(tailLines(text, Number(args.lines || 120)));
}

async function runUpdate(args) {
  ensureNodeRuntime();
  const installRoot = path.resolve(expandHome(args.installRoot || defaultInstallRoot()));
  const config = await readLocalConfig(installRoot);
  if (!config) {
    console.log(`${SERVICE_NAME} is not installed at ${installRoot}`);
    process.exitCode = 1;
    return;
  }

  const paths = buildInstallPaths(installRoot);
  const pathDirs = resolvePathDirs();
  await mkdir(paths.logsDir, { recursive: true });
  await copyFile(path.join(ASSETS_DIR, "worker.mjs"), paths.workerPath);
  await copyFile(path.join(ASSETS_DIR, "ndcAgentMcpServer.mjs"), paths.ndcAgentMcpServerPath);
  await writeStartScript(paths.startScriptPath);
  await setupCodexHome(config.codexHome || path.join(installRoot, "codex-home"), config.appMcpServers);

  await writeConfig(paths.configPath, {
    ...config,
    service: SERVICE_NAME,
    packageName: PACKAGE_NAME,
    packageVersion: PACKAGE_VERSION,
    agentVersion: PACKAGE_VERSION,
    protocolVersion: BRIDGE_PROTOCOL_VERSION,
    capabilities: BRIDGE_CAPABILITIES,
    updatedAt: new Date().toISOString(),
    worker: paths.workerPath,
    ndcAgentMcpServer: paths.ndcAgentMcpServerPath,
    startScript: paths.startScriptPath,
    nodePath: process.execPath,
    pathDirs,
  });

  if (!args.noRestart) {
    await stopBridge(installRoot).catch(() => {});
    await startBridge(installRoot);
    const health = await waitForHealth(config.port || 8787, 15000);
    console.log(`${SERVICE_NAME} updated to ${PACKAGE_VERSION}.`);
    console.log(`Health: ${health.ok ? "online" : "not ready"}`);
    return;
  }

  console.log(`${SERVICE_NAME} updated to ${PACKAGE_VERSION}. Restart skipped.`);
}

function parseArgs(rawArgs) {
  let args = rawArgs[0] === "--" ? rawArgs.slice(1) : rawArgs;
  let command = "setup";
  if (["setup", "status", "doctor", "update", "start", "stop", "logs"].includes(args[0])) {
    command = args[0];
    args = args.slice(1);
  } else if (args[0] === "help") {
    return { command: "help", help: true };
  }

  const parsed = {
    command,
    gateway: "",
    setupCode: "",
    installRoot: "",
    workspace: "",
    port: "",
    lines: "",
    help: false,
    noAutostart: false,
    noCodexInstall: false,
    noCodexLogin: false,
    noRestart: false,
  };
  const positional = [];

  for (let i = 0; i < args.length; i += 1) {
    const arg = args[i];
    if (arg === "-h" || arg === "--help") {
      parsed.help = true;
      continue;
    }
    if (arg === "--gateway") {
      parsed.gateway = requireValue(args, ++i, arg);
      continue;
    }
    if (arg.startsWith("--gateway=")) {
      parsed.gateway = arg.slice("--gateway=".length);
      continue;
    }
    if (arg === "--install-root") {
      parsed.installRoot = requireValue(args, ++i, arg);
      continue;
    }
    if (arg.startsWith("--install-root=")) {
      parsed.installRoot = arg.slice("--install-root=".length);
      continue;
    }
    if (arg === "--workspace") {
      parsed.workspace = requireValue(args, ++i, arg);
      continue;
    }
    if (arg.startsWith("--workspace=")) {
      parsed.workspace = arg.slice("--workspace=".length);
      continue;
    }
    if (arg === "--port") {
      parsed.port = requireValue(args, ++i, arg);
      continue;
    }
    if (arg.startsWith("--port=")) {
      parsed.port = arg.slice("--port=".length);
      continue;
    }
    if (arg === "--lines") {
      parsed.lines = requireValue(args, ++i, arg);
      continue;
    }
    if (arg.startsWith("--lines=")) {
      parsed.lines = arg.slice("--lines=".length);
      continue;
    }
    if (arg === "--no-autostart") {
      parsed.noAutostart = true;
      continue;
    }
    if (arg === "--no-codex-install") {
      parsed.noCodexInstall = true;
      continue;
    }
    if (arg === "--no-codex-login") {
      parsed.noCodexLogin = true;
      continue;
    }
    if (arg === "--no-restart") {
      parsed.noRestart = true;
      continue;
    }
    if (arg === "--setup-code" || arg === "--code") {
      parsed.setupCode = requireValue(args, ++i, arg);
      continue;
    }
    if (arg.startsWith("--setup-code=")) {
      parsed.setupCode = arg.slice("--setup-code=".length);
      continue;
    }
    if (arg.startsWith("--code=")) {
      parsed.setupCode = arg.slice("--code=".length);
      continue;
    }
    if (arg.startsWith("-")) throw new UsageError(`Unknown argument: ${arg}`);
    positional.push(arg);
  }

  if (command === "setup") {
    parsed.setupCode ||= positional.shift() || "";
  }
  if (positional.length) throw new UsageError(`Unexpected argument: ${positional[0]}`);
  return parsed;
}

function requireValue(args, index, flag) {
  const value = args[index];
  if (!value || value.startsWith("--")) throw new UsageError(`Missing value for ${flag}.`);
  return value;
}

function printHelp() {
  console.log(`Install ${SERVICE_NAME}.

Usage:
  ai-workspace-bridge setup <setup-code> [--gateway <url>] [--install-root <path>] [--workspace <path>] [--port <port>]
  ai-workspace-bridge status [--install-root <path>]
  ai-workspace-bridge doctor [--install-root <path>]
  ai-workspace-bridge update [--install-root <path>] [--no-restart]
  ai-workspace-bridge start [--install-root <path>]
  ai-workspace-bridge stop [--install-root <path>]
  ai-workspace-bridge logs [--install-root <path>]

AI Hub tarball form:
  npx --yes --package ${DEFAULT_GATEWAY}/api/ai-workspace/bridge-package.tgz ai-workspace-bridge setup <setup-code> --gateway ${DEFAULT_GATEWAY}
`);
}

async function redeemSetupCode(gateway, setupCode) {
  let response;
  try {
    response = await fetch(`${gateway}/api/ai-workspace/setup-codes/redeem`, {
      method: "POST",
      headers: {
        Accept: "application/json",
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ setupCode, setup_code: setupCode }),
    });
  } catch (error) {
    throw new Error(`Setup code redeem failed: ${error.message}`);
  }

  const bodyText = await response.text();
  let data = {};
  try {
    data = bodyText ? JSON.parse(bodyText) : {};
  } catch {
    throw new Error(`Setup code redeem failed: HTTP ${response.status} ${bodyText}`);
  }
  if (!response.ok || data.ok === false) {
    throw new Error(`Setup code redeem failed: ${data.error || `HTTP ${response.status}`}`);
  }
  return data;
}

function normalizeSetupPayload(payload) {
  const setup = isPlainObject(payload?.setup) ? payload.setup : {};
  const bridge = isPlainObject(setup.bridge) ? setup.bridge : {};
  const pairingCode = cleanString(bridge.pairingCode || bridge.pairing_code, 80);
  const hubUrl = cleanWsEndpoint(bridge.hubUrl || bridge.hub_url);
  const hubUrls = uniqueStrings([
    hubUrl,
    ...(Array.isArray(bridge.hubUrls) ? bridge.hubUrls : []),
    ...(Array.isArray(bridge.hub_urls) ? bridge.hub_urls : []),
  ].map(cleanWsEndpoint));
  if (!pairingCode) throw new Error("Setup payload is missing pairing code.");
  if (!hubUrls.length) throw new Error("Setup payload is missing Hub URL.");
  return {
    bridge: {
      hubUrl: hubUrls[0],
      hubUrls,
      pairingCode,
      machineName: cleanString(bridge.machineName || bridge.machine_name, 120) || os.hostname(),
      workspace: cleanString(bridge.workspace || bridge.workspacePath || bridge.workspace_path, 1000),
      port: sanitizePort(bridge.port || 8787),
      appMcpServers: sanitizeMcpServers(bridge.appMcpServers || bridge.app_mcp_servers),
    },
  };
}

async function writeStartScript(startScriptPath) {
  const source = `#!/usr/bin/env node
import { mkdir, open, readFile, writeFile } from "node:fs/promises";
import fssync from "node:fs";
import path from "node:path";
import { spawn } from "node:child_process";
import { fileURLToPath } from "node:url";

const root = path.dirname(fileURLToPath(import.meta.url));
const configPath = path.join(root, "config.json");
const config = JSON.parse(await readFile(configPath, "utf8"));
await mkdir(path.dirname(config.logPath), { recursive: true });
await writeFile(config.pidPath, String(process.pid), "utf8").catch(() => {});

let stopping = false;
let child = null;
process.on("SIGTERM", () => stop("SIGTERM"));
process.on("SIGINT", () => stop("SIGINT"));

function stop(signal) {
  stopping = true;
  if (child && !child.killed) {
    try { child.kill(signal); } catch {}
  }
  setTimeout(() => process.exit(0), 1500).unref();
}

function buildEnv() {
  const delimiter = process.platform === "win32" ? ";" : ":";
  const pathDirs = Array.isArray(config.pathDirs) ? config.pathDirs.filter(Boolean) : [];
  return {
    ...process.env,
    CODEX_HOME: config.codexHome,
    AI_BRIDGE_HOST: "0.0.0.0",
    AI_BRIDGE_PORT: String(config.port || 8787),
    AI_BRIDGE_CODEX_CWD: config.workspace,
    AI_BRIDGE_CODEX_BIN: "codex",
    AI_BRIDGE_CODEX_ARGS: ${JSON.stringify(JSON.stringify(DEFAULT_CODEX_ARGS))},
    AI_BRIDGE_ALLOW_ORIGIN: "*",
    AI_BRIDGE_HUB_URL: config.hubUrl || "",
    AI_BRIDGE_HUB_URLS: Array.isArray(config.hubUrls) ? config.hubUrls.join(",") : "",
    AI_BRIDGE_PAIRING_CODE: config.pairingCode || "",
    AI_BRIDGE_MACHINE_NAME: config.machineName || "",
    AI_BRIDGE_AGENT_VERSION: config.agentVersion || config.packageVersion || "",
    AI_BRIDGE_PROTOCOL_VERSION: config.protocolVersion || "",
    AI_BRIDGE_CAPABILITIES: Array.isArray(config.capabilities) ? config.capabilities.join(",") : "",
    AI_BRIDGE_ACTIVE_MIRROR_DIR: path.join(config.workspace || root, ".nodedc", "ai-workspace"),
    AI_BRIDGE_ACTIVE_MIRROR_OPEN_IDE: "1",
    PATH: [...pathDirs, process.env.PATH || ""].filter(Boolean).join(delimiter),
    Path: [...pathDirs, process.env.Path || process.env.PATH || ""].filter(Boolean).join(delimiter),
  };
}

async function runOnce() {
  const log = await open(config.logPath, "a");
  try {
    child = spawn(config.nodePath || process.execPath, [config.worker], {
      cwd: config.installRoot || root,
      env: buildEnv(),
      stdio: ["ignore", log.fd, log.fd],
      windowsHide: true,
    });
    const code = await new Promise((resolve) => {
      child.on("exit", (exitCode, signal) => resolve(signal || exitCode || 0));
      child.on("error", (error) => resolve(error.message));
    });
    child = null;
    return code;
  } finally {
    await log.close().catch(() => {});
  }
}

while (!stopping) {
  const code = await runOnce();
  if (stopping) break;
  await new Promise((resolve) => setTimeout(resolve, 5000));
}
`;
  await writeFile(startScriptPath, source, "utf8");
  await chmodIfPossible(startScriptPath, 0o755);
}

async function writeConfig(configPath, config) {
  await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
}

async function setupCodexHome(codexHome, appMcpServers) {
  await mkdir(codexHome, { recursive: true });
  await copyIfExists(path.join(os.homedir(), ".codex", "auth.json"), path.join(codexHome, "auth.json"));
  await mergeCodexMcpConfig(path.join(codexHome, "config.toml"), appMcpServers);
  await installBundledSkills(codexHome);
}

async function installBundledSkills(codexHome) {
  const sourceRoot = path.join(ASSETS_DIR, "skills");
  const entries = await readdir(sourceRoot, { withFileTypes: true }).catch(() => []);
  if (!entries.length) return;
  const targetRoot = path.join(codexHome, "skills");
  await mkdir(targetRoot, { recursive: true });
  for (const entry of entries) {
    if (!entry.isDirectory()) continue;
    await cp(path.join(sourceRoot, entry.name), path.join(targetRoot, entry.name), {
      recursive: true,
      force: true,
    });
  }
}

function readPackageVersion() {
  try {
    const parsed = JSON.parse(fssync.readFileSync(path.join(PACKAGE_ROOT, "package.json"), "utf8"));
    return cleanString(parsed.version, 80) || "0.0.0";
  } catch {
    return "0.0.0";
  }
}

async function mergeCodexMcpConfig(configPath, appMcpServers) {
  const servers = sanitizeMcpServers(appMcpServers);
  if (!servers.length) {
    await touchFile(configPath);
    return;
  }
  let raw = await readFile(configPath, "utf8").catch(() => "");
  for (const server of servers) {
    raw = removeTomlSection(raw, `[mcp_servers.${server.serverName}]`);
    raw = removeTomlSection(raw, `[mcp_servers.${server.serverName}.headers]`);
    raw = removeTomlSection(raw, `[mcp_servers.${server.serverName}.http_headers]`);
    const lines = [
      `[mcp_servers.${server.serverName}]`,
      `url = ${tomlString(server.url)}`,
      `startup_timeout_sec = ${server.startupTimeoutSec}`,
      `tool_timeout_sec = ${server.toolTimeoutSec}`,
      `required = ${server.required ? "true" : "false"}`,
    ];
    if (Object.keys(server.httpHeaders).length) {
      lines.push("", `[mcp_servers.${server.serverName}.http_headers]`);
      for (const [key, value] of Object.entries(server.httpHeaders)) {
        lines.push(`${tomlBareKey(key)} = ${tomlString(value)}`);
      }
    }
    raw = `${raw.trim()}\n\n${lines.join("\n")}\n`;
  }
  await mkdir(path.dirname(configPath), { recursive: true });
  await writeFile(configPath, raw.trimStart(), "utf8");
}

function removeTomlSection(raw, header) {
  const lines = String(raw || "").split(/\r?\n/);
  const out = [];
  let skipping = false;
  const normalizedHeader = normalizeTomlHeader(header);
  for (const line of lines) {
    const current = normalizeTomlHeader(line);
    if (current) {
      skipping = current === normalizedHeader;
      if (skipping) continue;
    }
    if (!skipping) out.push(line);
  }
  return out.join("\n").trimEnd();
}

function normalizeTomlHeader(line) {
  const text = String(line || "").trim();
  if (!text.startsWith("[") || !text.endsWith("]")) return "";
  return text.replace(/\s+/g, "").replace(/["']/g, "");
}

async function ensureCodexCli({ pathDirs }) {
  const before = runCommandQuiet("codex", ["--version"], { pathDirs });
  if (before.ok) return;
  const npm = process.platform === "win32" ? "npm.cmd" : "npm";
  console.log("Installing Codex CLI with npm...");
  await runCommand(npm, ["install", "-g", "@openai/codex", "--no-audit", "--no-fund"], { pathDirs });
  const after = runCommandQuiet("codex", ["--version"], { pathDirs: resolvePathDirs() });
  if (!after.ok) throw new Error("Codex CLI was installed, but codex is not available in PATH.");
}

async function ensureCodexLogin(codexHome, { pathDirs }) {
  if (fssync.existsSync(path.join(codexHome, "auth.json"))) return;
  console.log("Codex auth is missing. Opening codex login...");
  await runCommand("codex", ["login"], {
    pathDirs,
    env: { CODEX_HOME: codexHome },
    stdio: "inherit",
  }).catch((error) => {
    console.warn(`Codex login did not complete: ${error.message}`);
  });
}

function inspectCodex(codexHome, pathDirs) {
  const version = runCommandQuiet("codex", ["--version"], { pathDirs });
  return {
    codexOk: version.ok,
    codexVersion: version.stdout.trim(),
    authOk: fssync.existsSync(path.join(codexHome || "", "auth.json")),
    error: version.ok ? "" : version.error,
  };
}

async function installAutostart(startScriptPath, installRoot) {
  if (process.platform === "win32") return installWindowsTask(startScriptPath);
  if (process.platform === "darwin") return installLaunchAgent(startScriptPath);
  return installSystemdUserService(startScriptPath, installRoot);
}

async function installWindowsTask(startScriptPath) {
  const taskRun = `"${process.execPath}" "${startScriptPath}"`;
  runCommandQuiet("schtasks", ["/Delete", "/TN", WINDOWS_TASK_NAME, "/F"], { shell: false });
  await runCommand("schtasks", [
    "/Create",
    "/TN",
    WINDOWS_TASK_NAME,
    "/SC",
    "ONLOGON",
    "/TR",
    taskRun,
    "/RL",
    "HIGHEST",
    "/F",
  ], { shell: false });
}

async function installLaunchAgent(startScriptPath) {
  const label = "ru.nodedc.ai-workspace-bridge";
  const plistDir = path.join(os.homedir(), "Library", "LaunchAgents");
  const plistPath = path.join(plistDir, `${label}.plist`);
  await mkdir(plistDir, { recursive: true });
  await writeFile(plistPath, `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
  <key>Label</key><string>${label}</string>
  <key>ProgramArguments</key><array><string>${xmlEscape(process.execPath)}</string><string>${xmlEscape(startScriptPath)}</string></array>
  <key>RunAtLoad</key><true/>
  <key>KeepAlive</key><true/>
  <key>StandardOutPath</key><string>${xmlEscape(path.join(path.dirname(startScriptPath), "logs", "launchd.out.log"))}</string>
  <key>StandardErrorPath</key><string>${xmlEscape(path.join(path.dirname(startScriptPath), "logs", "launchd.err.log"))}</string>
</dict></plist>
`, "utf8");
  runCommandQuiet("launchctl", ["bootout", `gui/${process.getuid?.()}`, plistPath], {});
  await runCommand("launchctl", ["bootstrap", `gui/${process.getuid?.()}`, plistPath], {});
  await runCommand("launchctl", ["enable", `gui/${process.getuid?.()}/${label}`], {});
}

async function installSystemdUserService(startScriptPath, installRoot) {
  const serviceDir = path.join(os.homedir(), ".config", "systemd", "user");
  const servicePath = path.join(serviceDir, "nodedc-ai-workspace-bridge.service");
  await mkdir(serviceDir, { recursive: true });
  await writeFile(servicePath, `[Unit]
Description=NDC AI Workspace Bridge

[Service]
Type=simple
ExecStart=${process.execPath} ${startScriptPath}
Restart=always
RestartSec=5
WorkingDirectory=${installRoot}

[Install]
WantedBy=default.target
`, "utf8");
  await runCommand("systemctl", ["--user", "daemon-reload"], {});
  await runCommand("systemctl", ["--user", "enable", "--now", "nodedc-ai-workspace-bridge.service"], {});
}

async function startBridge(installRoot) {
  const config = await readLocalConfig(installRoot);
  if (!config?.startScript) throw new Error(`${SERVICE_NAME} is not installed at ${installRoot}`);
  const child = spawn(config.nodePath || process.execPath, [config.startScript], {
    cwd: installRoot,
    detached: true,
    stdio: "ignore",
    windowsHide: true,
  });
  child.unref();
}

async function stopBridge(installRoot) {
  const config = await readLocalConfig(installRoot);
  const pidPath = config?.pidPath || path.join(installRoot, "bridge.pid");
  const pidText = await readFile(pidPath, "utf8").catch(() => "");
  const pid = Number(pidText.trim());
  if (!Number.isInteger(pid) || pid <= 0) return;
  if (process.platform === "win32") {
    runCommandQuiet("taskkill", ["/PID", String(pid), "/T", "/F"], {});
  } else {
    try {
      process.kill(pid, "SIGTERM");
    } catch {}
  }
  await rm(pidPath, { force: true }).catch(() => {});
}

async function waitForHealth(port, timeoutMs) {
  const started = Date.now();
  let latest = { ok: false, error: "not_checked" };
  while (Date.now() - started < timeoutMs) {
    latest = await fetchHealth(port);
    if (latest.ok) return latest;
    await sleep(700);
  }
  return latest;
}

async function fetchHealth(port) {
  try {
    const response = await fetch(`http://127.0.0.1:${sanitizePort(port)}${HEALTH_PATH}`);
    const data = await response.json().catch(() => ({}));
    return { ok: response.ok && data?.ok !== false, data };
  } catch (error) {
    return { ok: false, error: String(error?.message || error) };
  }
}

async function readLocalConfig(installRoot) {
  try {
    return JSON.parse(await readFile(path.join(installRoot, "config.json"), "utf8"));
  } catch {
    return null;
  }
}

function buildInstallPaths(installRoot) {
  return {
    logsDir: path.join(installRoot, "logs"),
    logPath: path.join(installRoot, "logs", "bridge.log"),
    pidPath: path.join(installRoot, "bridge.pid"),
    configPath: path.join(installRoot, "config.json"),
    workerPath: path.join(installRoot, "worker.mjs"),
    ndcAgentMcpServerPath: path.join(installRoot, "ndcAgentMcpServer.mjs"),
    startScriptPath: path.join(installRoot, "start-bridge.mjs"),
  };
}

function defaultInstallRoot() {
  if (process.platform === "win32") {
    return path.join(process.env.LOCALAPPDATA || os.homedir(), "NDC", "AIWorkspaceBridge");
  }
  if (process.platform === "darwin") {
    return path.join(os.homedir(), "Library", "Application Support", "NDC", "AIWorkspaceBridge");
  }
  return path.join(os.homedir(), ".local", "share", "nodedc", "ai-workspace-bridge");
}

function resolvePathDirs() {
  const dirs = [path.dirname(process.execPath)];
  const prefix = runCommandQuiet(process.platform === "win32" ? "npm.cmd" : "npm", ["prefix", "-g"], {}).stdout.trim();
  if (prefix) {
    dirs.push(process.platform === "win32" ? prefix : path.join(prefix, "bin"));
  }
  return uniqueStrings(dirs);
}

function runCommandQuiet(command, args, options = {}) {
  try {
    const result = spawnSync(command, args, {
      env: commandEnv(options),
      encoding: "utf8",
      shell: options.shell ?? process.platform === "win32",
      windowsHide: true,
    });
    return {
      ok: result.status === 0,
      stdout: result.stdout || "",
      stderr: result.stderr || "",
      error: result.error ? result.error.message : result.stderr || "",
    };
  } catch (error) {
    return { ok: false, stdout: "", stderr: "", error: String(error?.message || error) };
  }
}

function runCommand(command, args, options = {}) {
  return new Promise((resolve, reject) => {
    const child = spawn(command, args, {
      env: commandEnv(options),
      stdio: options.stdio || "inherit",
      shell: options.shell ?? process.platform === "win32",
      windowsHide: true,
    });
    child.on("error", reject);
    child.on("exit", (code) => {
      if (code === 0) resolve();
      else reject(new Error(`${command} exited with code ${code}`));
    });
  });
}

function commandEnv(options = {}) {
  const delimiter = process.platform === "win32" ? ";" : ":";
  const pathDirs = Array.isArray(options.pathDirs) ? options.pathDirs.filter(Boolean) : [];
  const basePath = process.env.PATH || process.env.Path || "";
  return {
    ...process.env,
    ...(options.env || {}),
    PATH: [...pathDirs, basePath].filter(Boolean).join(delimiter),
    Path: [...pathDirs, process.env.Path || basePath].filter(Boolean).join(delimiter),
  };
}

function ensureNodeRuntime() {
  const major = Number(process.versions.node.split(".")[0]);
  if (!Number.isInteger(major) || major < 22) {
    throw new Error(`${SERVICE_NAME} requires Node.js 22+ for WebSocket bridge mode. Current: ${process.version}`);
  }
}

function sanitizeMcpServers(value) {
  const items = Array.isArray(value) ? value : [];
  return items.map((item) => {
    if (!isPlainObject(item)) return null;
    const serverName = safeMcpServerName(item.serverName || item.server_name || item.name);
    const url = cleanHttpEndpoint(item.url);
    if (!serverName || !url) return null;
    return {
      serverName,
      url,
      required: item.required === true,
      startupTimeoutSec: sanitizeInteger(item.startupTimeoutSec || item.startup_timeout_sec, 20, 1, 300),
      toolTimeoutSec: sanitizeInteger(item.toolTimeoutSec || item.tool_timeout_sec, 60, 1, 3600),
      httpHeaders: sanitizeHeaders(item.httpHeaders || item.http_headers || item.headers),
    };
  }).filter(Boolean);
}

function sanitizeHeaders(value) {
  if (!isPlainObject(value)) return {};
  const headers = {};
  for (const [key, raw] of Object.entries(value)) {
    const name = cleanString(key, 120);
    const text = cleanString(raw, 2000);
    if (!name || !text || /[\r\n\0]/.test(name) || /[\r\n\0]/.test(text)) continue;
    headers[name] = text;
  }
  return headers;
}

function safeMcpServerName(value) {
  const text = String(value || "").trim().replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 80);
  return text || "";
}

function cleanHttpEndpoint(value) {
  const text = cleanString(value, 1000);
  if (!text) return "";
  try {
    const url = new URL(text);
    if (url.protocol !== "http:" && url.protocol !== "https:") return "";
    url.username = "";
    url.password = "";
    return url.toString().replace(/\/+$/, "");
  } catch {
    return "";
  }
}

function cleanWsEndpoint(value) {
  const text = cleanString(value, 1000);
  if (!text) return "";
  try {
    const url = new URL(text);
    if (url.protocol !== "ws:" && url.protocol !== "wss:") return "";
    url.username = "";
    url.password = "";
    return url.toString().replace(/\/+$/, "");
  } catch {
    return "";
  }
}

function sanitizePort(value) {
  const port = Number(value || 8787);
  if (!Number.isInteger(port) || port < 1 || port > 65535) throw new UsageError("port_invalid");
  return port;
}

function sanitizeInteger(value, fallback, min, max) {
  const number = Number(value || fallback);
  if (!Number.isFinite(number)) return fallback;
  return Math.min(Math.max(Math.trunc(number), min), max);
}

function cleanString(value, max) {
  return String(value || "").trim().slice(0, max);
}

function uniqueStrings(values) {
  return Array.from(new Set((values || []).map((item) => String(item || "").trim()).filter(Boolean)));
}

function expandHome(value) {
  const text = String(value || "");
  if (text === "~") return os.homedir();
  if (text.startsWith("~/") || text.startsWith("~\\")) return path.join(os.homedir(), text.slice(2));
  return text;
}

function tomlString(value) {
  return JSON.stringify(String(value || ""));
}

function tomlBareKey(value) {
  const key = String(value || "");
  return /^[A-Za-z0-9_-]+$/.test(key) ? key : JSON.stringify(key);
}

function xmlEscape(value) {
  return String(value || "")
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;");
}

async function copyIfExists(from, to) {
  try {
    await stat(from);
    await mkdir(path.dirname(to), { recursive: true });
    await copyFile(from, to);
  } catch {}
}

async function touchFile(filePath) {
  await mkdir(path.dirname(filePath), { recursive: true });
  if (!fssync.existsSync(filePath)) await writeFile(filePath, "", "utf8");
}

async function chmodIfPossible(filePath, mode) {
  try {
    await fssync.promises.chmod(filePath, mode);
  } catch {}
}

function tailLines(text, count) {
  return String(text || "").split(/\r?\n/).slice(-Math.max(1, count || 120)).join("\n");
}

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

function isPlainObject(value) {
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
}

main().catch((error) => {
  console.error(error instanceof UsageError ? error.message : `Error: ${error.message || error}`);
  process.exitCode = 1;
});
