#!/usr/bin/env node
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";

const DEFAULT_GATEWAY = "https://ops-agents.nodedc.ru";
const SERVER_NAME = "nodedc-ops-agent";
const ONTOLOGY_SERVER_NAME = "nodedc_ontology";
const SKILL_NAME = "ops-context";
const MCP_PROTOCOL_VERSION = "2025-06-18";

async function main() {
  const args = parseArgs(process.argv.slice(2));

  if (args.help) {
    printHelp(args.command);
    return;
  }

  if (args.command === "setup") {
    await runSetup(args);
    return;
  }

  if (args.command === "status") {
    const report = await inspectLocalInstalls(args.codexHome, { smoke: false });
    printInstallReport(report);
    process.exitCode = report.ok ? 0 : 1;
    return;
  }

  if (args.command === "doctor") {
    const report = await inspectLocalInstalls(args.codexHome, { smoke: true });
    printInstallReport(report);
    process.exitCode = report.ok ? 0 : 1;
    return;
  }

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

async function runSetup(args) {
  if (!args.setupCode) {
    throw new UsageError("Missing setup code. Use: ops-codex setup <ndcsetup_...>");
  }

  const gateway = String(args.gateway || process.env.NODEDC_OPS_AGENT_GATEWAY || DEFAULT_GATEWAY).replace(/\/+$/, "");
  const redeemPayload = await redeemSetupCode(gateway, args.setupCode);
  const setup = redeemPayload.setup || {};
  const mcpServer = setup.mcp_server || {};
  const ops = redeemPayload.ops || {
    serverName: SERVER_NAME,
    mcpUrl: String(mcpServer.url || `${gateway}/mcp`),
    token: redeemPayload.token,
  };
  const ontology = redeemPayload.ontology;
  if (!ops.token || !ontology?.token) {
    throw new Error("Setup code redeem did not return independent Ops and Ontology credentials.");
  }
  const opsEndpoint = String(ops.mcpUrl || `${gateway}/mcp`);
  const ontologyEndpoint = String(ontology.mcpUrl || `${gateway}/ontology-mcp`);
  const agentsMd = setup.agents_md || defaultSkillBody(opsEndpoint);

  const targets = await resolveCodexTargets(args.codexHome);

  for (const target of targets) {
    const configPath = path.join(target.codexHome, "config.toml");
    const skillPath = path.join(target.codexHome, "skills", SKILL_NAME, "SKILL.md");
    await writeCodexConfig(configPath, [
      { serverName: String(ops.serverName || SERVER_NAME), endpoint: opsEndpoint, token: ops.token },
      {
        serverName: String(ontology.serverName || ONTOLOGY_SERVER_NAME),
        endpoint: ontologyEndpoint,
        token: ontology.token,
      },
    ]);
    await writeSkill(skillPath, agentsMd, opsEndpoint, ontologyEndpoint);
  }
  const opsToolCount = await smokeToolsList(opsEndpoint, ops.token);
  const ontologyToolCount = await smokeToolsList(ontologyEndpoint, ontology.token);

  console.log("NODE.DC Ops Codex setup complete.");
  for (const target of targets) {
    console.log("Config:", path.join(target.codexHome, "config.toml"));
    console.log("Skill:", path.join(target.codexHome, "skills", SKILL_NAME, "SKILL.md"));
    console.log("Target:", target.reason);
  }
  console.log("Ops MCP smoke tools:", opsToolCount);
  console.log("Ontology MCP smoke tools:", ontologyToolCount);
  console.log("Run: ops-codex doctor");
  console.log("Restart Codex Desktop completely before testing. A new chat is not enough if the Desktop process already loaded MCP config.");
  console.log("In the next session, tool discovery must expose tasker_list_projects. If it does not, Codex has not loaded the NODE.DC Ops MCP server.");
}

function parseArgs(rawArgs) {
  let args = rawArgs[0] === "--" ? rawArgs.slice(1) : rawArgs;
  let command = "setup";

  if (args[0] === "setup" || args[0] === "status" || args[0] === "doctor") {
    command = args[0];
    args = args.slice(1);
  } else if (args[0] === "help") {
    return { codexHome: "", command: "help", gateway: "", help: true, setupCode: "" };
  }

  const parsed = {
    codexHome: "",
    command,
    gateway: "",
    help: false,
    setupCode: "",
  };
  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, "--gateway");
      continue;
    }
    if (arg.startsWith("--gateway=")) {
      parsed.gateway = arg.slice("--gateway=".length);
      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 === "--codex-home") {
      parsed.codexHome = requireValue(args, ++i, "--codex-home");
      continue;
    }
    if (arg.startsWith("--codex-home=")) {
      parsed.codexHome = arg.slice("--codex-home=".length);
      continue;
    }
    if (arg.startsWith("-")) {
      throw new UsageError(`Unknown argument: ${arg}`);
    }
    positional.push(arg);
  }

  if (parsed.command === "setup") {
    if (!parsed.setupCode && positional[0]) {
      parsed.setupCode = positional[0];
    }
    if (positional.length > 1) {
      throw new UsageError(`Unexpected extra argument: ${positional[1]}`);
    }
  } else if (positional.length) {
    throw new UsageError(`${parsed.command} does not accept positional arguments.`);
  }

  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(command = "") {
  if (command === "status") {
    console.log(`Show local NODE.DC Ops Codex install status.

Usage:
  ops-codex status [--codex-home <path>]
`);
    return;
  }

  if (command === "doctor") {
    console.log(`Check local NODE.DC Ops Codex install and MCP connectivity.

Usage:
  ops-codex doctor [--codex-home <path>]
`);
    return;
  }

  console.log(`Install NODE.DC Ops MCP access into local Codex.

Usage:
  ops-codex setup <setup-code> [--gateway <url>] [--codex-home <path>]
  ops-codex setup --code <setup-code> [--gateway <url>] [--codex-home <path>]
  ops-codex status [--codex-home <path>]
  ops-codex doctor [--codex-home <path>]

Registry form after publish:
  npx --yes @nodedc/ops-codex setup <setup-code>

Self-host fallback:
  npm exec --yes --package=<gateway>/install/nodedc-ops-codex-setup.tgz -- ops-codex setup <setup-code>

Options:
  --code, --setup-code <code>  One-time setup code generated by NODE.DC Ops.
  --gateway <url>             Ops Agent Gateway URL. Defaults to ${DEFAULT_GATEWAY}.
  --codex-home <path>         Codex home directory. Overrides auto-discovery.
  -h, --help                  Show this help.

Codex home discovery:
  By default, the installer writes every existing Codex-like home it can detect:
  $CODEX_HOME, ~/.codex, common OS app-data Codex folders, and portable-style
  .codex folders near the current directory. If none exist, it creates ~/.codex.

Legacy compatibility:
  nodedc-ops-codex --setup-code <setup-code>
`);
}

async function redeemSetupCode(gateway, setupCode) {
  let response;
  try {
    response = await fetch(`${gateway}/api/v1/setup-codes/redeem`, {
      method: "POST",
      headers: {
        Accept: "application/json",
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ 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) {
    throw new Error(`Setup code redeem failed: HTTP ${response.status} ${bodyText}`);
  }

  if (!data.ok || !data.token || !data.ontology?.token) {
    throw new Error(`Setup code redeem failed: ${JSON.stringify(data)}`);
  }

  return data;
}

async function resolveCodexTargets(codexHomeArg) {
  if (codexHomeArg) {
    return [{ codexHome: expandHome(codexHomeArg), reason: "--codex-home" }];
  }

  if (process.env.CODEX_HOME) {
    return [{ codexHome: expandHome(process.env.CODEX_HOME), reason: "CODEX_HOME" }];
  }

  const candidates = buildCodexHomeCandidates();
  const existing = [];
  for (const candidate of candidates) {
    if (await looksLikeCodexHome(candidate.codexHome)) {
      existing.push(candidate);
    }
  }

  const dedupedExisting = dedupeTargets(existing);
  if (dedupedExisting.length > 0) {
    return dedupedExisting;
  }

  return [{ codexHome: defaultCodexHome(), reason: "default ~/.codex" }];
}

function buildCodexHomeCandidates() {
  const home = os.homedir();
  const candidates = [{ codexHome: defaultCodexHome(), reason: "default ~/.codex" }];

  if (process.platform === "darwin") {
    candidates.push(
      {
        codexHome: path.join(home, "Library", "Application Support", "Codex"),
        reason: "macOS Application Support Codex",
      },
      {
        codexHome: path.join(home, "Library", "Application Support", "OpenAI", "Codex"),
        reason: "macOS Application Support OpenAI Codex",
      }
    );
  }

  if (process.platform === "win32") {
    const appData = process.env.APPDATA || path.join(home, "AppData", "Roaming");
    const localAppData = process.env.LOCALAPPDATA || path.join(home, "AppData", "Local");
    candidates.push(
      { codexHome: path.join(appData, "Codex"), reason: "Windows APPDATA Codex" },
      { codexHome: path.join(appData, "OpenAI", "Codex"), reason: "Windows APPDATA OpenAI Codex" },
      { codexHome: path.join(localAppData, "Codex"), reason: "Windows LOCALAPPDATA Codex" },
      { codexHome: path.join(localAppData, "OpenAI", "Codex"), reason: "Windows LOCALAPPDATA OpenAI Codex" }
    );
  }

  for (const portableHome of portableCodexHomeCandidates(process.cwd())) {
    candidates.push(portableHome);
  }

  return dedupeTargets(candidates);
}

function portableCodexHomeCandidates(startDir) {
  const candidates = [];
  let current = path.resolve(startDir);
  const stopAt = path.parse(current).root;

  for (let depth = 0; depth < 4; depth += 1) {
    candidates.push(
      { codexHome: path.join(current, ".codex"), reason: "portable .codex near current directory" },
      { codexHome: path.join(current, "data", ".codex"), reason: "portable data/.codex near current directory" },
      { codexHome: path.join(current, "profile", ".codex"), reason: "portable profile/.codex near current directory" },
      { codexHome: path.join(current, "portable", ".codex"), reason: "portable portable/.codex near current directory" }
    );

    if (current === stopAt) {
      break;
    }
    current = path.dirname(current);
  }

  return candidates;
}

function defaultCodexHome() {
  return path.join(os.homedir(), ".codex");
}

async function looksLikeCodexHome(codexHome) {
  if (await pathExists(path.join(codexHome, "config.toml"))) {
    return true;
  }
  if (await pathExists(path.join(codexHome, "skills"))) {
    return true;
  }
  const baseName = path.basename(codexHome).toLowerCase();
  return baseName === ".codex" && (await pathExists(codexHome));
}

async function pathExists(targetPath) {
  try {
    await stat(targetPath);
    return true;
  } catch (error) {
    if (error && error.code === "ENOENT") {
      return false;
    }
    throw error;
  }
}

function dedupeTargets(targets) {
  const seen = new Set();
  const result = [];
  for (const target of targets) {
    const key = normalizePathKey(target.codexHome);
    if (seen.has(key)) {
      continue;
    }
    seen.add(key);
    result.push({ codexHome: path.resolve(target.codexHome), reason: target.reason });
  }
  return result;
}

function normalizePathKey(value) {
  const resolved = path.resolve(value);
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
}

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

async function writeCodexConfig(configPath, servers) {
  await mkdir(path.dirname(configPath), { recursive: true });
  const original = await readTextIfExists(configPath);
  let backupPath = "";
  if (original !== null) {
    backupPath = `${configPath}.nodedc-bak`;
    await writeFile(backupPath, original, "utf8");
  }

  let nextText = stripServerSections(
    original || "",
    servers.map((server) => server.serverName)
  ).trimEnd();
  if (nextText) {
    nextText += "\n\n";
  }
  nextText += servers
    .map((server) => buildMcpConfigBlock(server.serverName, server.endpoint, server.token))
    .join("\n\n");
  nextText += "\n";
  await writeFile(configPath, nextText, "utf8");

  if (backupPath) {
    console.log("Backup:", backupPath);
  }
}

async function readTextIfExists(filePath) {
  try {
    return await readFile(filePath, "utf8");
  } catch (error) {
    if (error && error.code === "ENOENT") {
      return null;
    }
    throw error;
  }
}

function stripServerSections(text, serverNames) {
  const targets = new Set(serverNames.map((serverName) => `mcp_servers.${serverName}`));
  const kept = [];
  let skip = false;

  for (const line of text.split(/\r?\n/)) {
    const match = line.match(/^\s*\[([^\]]+)\]\s*(?:#.*)?$/);
    if (match) {
      const section = match[1].trim();
      skip = [...targets].some((target) => section === target || section.startsWith(`${target}.`));
    }
    if (!skip) {
      kept.push(line);
    }
  }

  return kept.join("\n");
}

function buildMcpConfigBlock(serverName, endpoint, token) {
  return [
    `[mcp_servers.${serverName}]`,
    `url = ${tomlString(endpoint)}`,
    "enabled = true",
    "required = false",
    "startup_timeout_sec = 20",
    "tool_timeout_sec = 60",
    "",
    `[mcp_servers.${serverName}.http_headers]`,
    `Authorization = ${tomlString(`Bearer ${token}`)}`,
    `Accept = ${tomlString("application/json")}`,
    `${tomlString("MCP-Protocol-Version")} = ${tomlString(MCP_PROTOCOL_VERSION)}`,
  ].join("\n");
}

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

async function writeSkill(skillPath, agentsMd, endpoint, ontologyEndpoint) {
  await mkdir(path.dirname(skillPath), { recursive: true });
  await writeFile(skillPath, buildSkillBody(agentsMd, endpoint, ontologyEndpoint), "utf8");
}

function buildSkillBody(agentsMd, endpoint, ontologyEndpoint) {
  return [
    "---",
    "name: ops-context",
    "description: Use when working with NODE.DC Ops/Tasker cards, project cards, checkers, labels, comments, or when the user asks for ops-context. Always use the direct nodedc-ops-agent tasker_* MCP tools and never the old codex_apps OPS widgets.",
    "---",
    "",
    "# ops-context",
    "",
    "Use this skill when the user asks to work with NODE.DC Ops, Tasker cards, project cards, checkers, labels, comments, or asks for ops-context.",
    "",
    "## Hard Rules",
    "",
    "- Treat Ops MCP as the source of truth for cards and project context.",
    "- Treat `nodedc_ontology` as a separate read-only semantic authority. Use it before inventing entity ids, semantic types, aliases, relations, or platform routing assumptions.",
    "- Ontology tools and credentials are never embedded or multiplexed into Ops MCP.",
    "- Use only the direct nodedc-ops-agent MCP tools for Ops cards. The expected tool names are tasker_get_agent_instructions, tasker_list_projects, tasker_get_project_context, tasker_search_issues, and the other tasker_* tools listed below.",
    "- Do not use Codex Apps workspace widgets for Ops: never call tools from codex_apps servers named nodedc_ops_readonly, nodedc_ops_gateway_readonly, ops-readonly, or ops_gateway_readonly. They are known-broken for Tasker cards and waste context.",
    "- Do not open current workspace widgets, CodexPro cards, .ai-bridge handoff files, local thread lists, or broad filesystem searches to answer Ops card requests.",
    "- MCP tools can be lazy-loaded. If mcp__nodedc_ops_agent.tasker_list_projects is not already visible in the active tools, do not answer yet.",
    "- First call the available tool discovery/search tool, normally tool_search.tool_search_tool, with query exactly: tasker_list_projects nodedc-ops-agent NODE.DC Ops MCP",
    "- After discovery returns NODE.DC Ops tools, immediately call mcp__nodedc_ops_agent.tasker_get_agent_instructions, then mcp__nodedc_ops_agent.tasker_list_projects, then mcp__nodedc_ops_agent.tasker_get_project_context for the target project.",
    "- Resolve workspace/project from tasker_list_projects and granted project context. Do not guess a workspace slug from user wording.",
    "- Stop only if no discovery/search tool is available, or discovery returns no mcp__nodedc_ops_agent tasker_* tools. Then tell the user: 'NODE.DC Ops MCP is installed in config.toml, but this Codex session did not expose the nodedc-ops-agent tools after tool discovery.' Do not fall back to filesystem search, config.toml parsing, curl, or Codex Apps widgets unless the user explicitly asks to debug installation.",
    "- Never print Authorization headers, setup codes, or agent tokens.",
    "- Every write must use the official Tasker MCP write tool and a fresh idempotency_key.",
    "- Read a card with `tasker_get_issue` before claiming its comments or attachments are unavailable.",
    "- Create projects only through `tasker_create_project` when the effective workspace grant includes `project:create`.",
    "- Attach local files only through `tasker_attach_file`; never pass arbitrary server paths or remote URLs.",
    "- Do not delete or archive cards, comments, labels, projects, states, members, or workspaces.",
    "",
    "## Installed Endpoint",
    "",
    endpoint,
    "",
    "## Separate read-only Ontology endpoint",
    "",
    ontologyEndpoint,
    "",
    "## Gateway Rules Snapshot",
    "",
    agentsMd.trim(),
    "",
  ].join("\n");
}

function defaultSkillBody(endpoint) {
  return [
    "# NODE.DC Ops Agent Rules",
    "",
    `MCP endpoint: ${endpoint}`,
    "",
    "Call tasker_get_agent_instructions, tasker_list_projects, and tasker_get_project_context before writing Tasker cards.",
  ].join("\n");
}

async function inspectLocalInstall(codexHome, options) {
  const configPath = path.join(codexHome, "config.toml");
  const skillPath = path.join(codexHome, "skills", SKILL_NAME, "SKILL.md");
  const checks = [];
  const configText = await readTextIfExists(configPath);
  const skillText = await readTextIfExists(skillPath);
  const opsConfig = configText ? parseCodexMcpConfig(configText, SERVER_NAME) : null;
  const ontologyConfig = configText ? parseCodexMcpConfig(configText, ONTOLOGY_SERVER_NAME) : null;
  const endpoint = opsConfig?.server.url || "";
  const authorization = opsConfig?.headers.Authorization || "";
  const ontologyEndpoint = ontologyConfig?.server.url || "";
  const ontologyAuthorization = ontologyConfig?.headers.Authorization || "";

  checks.push({ ok: Boolean(configText), name: "config", detail: configPath });
  checks.push({
    ok: Boolean(opsConfig?.found),
    name: "Ops MCP server",
    detail: opsConfig?.found ? SERVER_NAME : "missing [mcp_servers.nodedc-ops-agent]",
  });
  checks.push({
    ok: Boolean(endpoint),
    name: "endpoint",
    detail: endpoint || "missing url",
  });
  checks.push({
    ok: Boolean(parsedConfig?.headers && authorization),
    name: "authorization",
    detail: authorization ? "present" : "missing http_headers.Authorization",
  });
  checks.push({
    ok: Boolean(ontologyConfig?.found),
    name: "Ontology MCP server",
    detail: ontologyConfig?.found ? ONTOLOGY_SERVER_NAME : "missing [mcp_servers.nodedc_ontology]",
  });
  checks.push({
    ok: Boolean(ontologyEndpoint),
    name: "Ontology endpoint",
    detail: ontologyEndpoint || "missing url",
  });
  checks.push({
    ok: Boolean(ontologyAuthorization),
    name: "Ontology authorization",
    detail: ontologyAuthorization ? "present" : "missing http_headers.Authorization",
  });
  checks.push({
    ok: Boolean(skillText),
    name: "skill",
    detail: skillPath,
  });
  checks.push({
    ok: Boolean(skillText?.startsWith("---\nname: ops-context")),
    name: "skill frontmatter",
    detail: skillText ? "present" : "not checked",
  });
  checks.push({
    ok: Boolean(skillText?.includes("tool_search.tool_search_tool")),
    name: "lazy discovery rule",
    detail: skillText ? "present" : "not checked",
  });

  let smoke = null;
  if (options.smoke) {
    if (endpoint && authorization && ontologyEndpoint && ontologyAuthorization) {
      try {
        const opsToolCount = await smokeToolsListWithAuthorization(endpoint, authorization);
        const ontologyToolCount = await smokeToolsListWithAuthorization(ontologyEndpoint, ontologyAuthorization);
        smoke = { ok: true, detail: `${opsToolCount} Ops tools; ${ontologyToolCount} Ontology tools` };
      } catch (error) {
        smoke = { ok: false, detail: error.message || String(error) };
      }
    } else {
      smoke = { ok: false, detail: "Ops/Ontology endpoint or Authorization header is missing" };
    }
    checks.push({ ok: smoke.ok, name: "mcp smoke", detail: smoke.detail });
  }

  return {
    codexHome,
    configPath,
    endpoint,
    ok: checks.every((check) => check.ok),
    skillPath,
    checks,
  };
}

async function inspectLocalInstalls(codexHomeArg, options) {
  const targets = await resolveCodexTargets(codexHomeArg);
  const reports = [];
  for (const target of targets) {
    reports.push({
      target,
      report: await inspectLocalInstall(target.codexHome, options),
    });
  }

  return {
    ok: reports.some((entry) => entry.report.ok),
    reports,
  };
}

function parseCodexMcpConfig(text, serverName) {
  const target = `mcp_servers.${serverName}`;
  const headers = `${target}.http_headers`;
  let current = "";
  const result = { found: false, headers: {}, server: {} };

  for (const rawLine of text.split(/\r?\n/)) {
    const line = rawLine.trim();
    if (!line || line.startsWith("#")) {
      continue;
    }
    const sectionMatch = line.match(/^\[([^\]]+)\]$/);
    if (sectionMatch) {
      current = sectionMatch[1].trim();
      if (current === target || current === headers) {
        result.found = true;
      }
      continue;
    }
    if (current !== target && current !== headers) {
      continue;
    }
    const keyValue = line.match(/^("[^"]+"|[A-Za-z0-9_.-]+)\s*=\s*(.+)$/);
    if (!keyValue) {
      continue;
    }
    const key = parseTomlKey(keyValue[1]);
    const value = parseTomlValue(keyValue[2]);
    if (current === target) {
      result.server[key] = value;
    } else {
      result.headers[key] = value;
    }
  }

  return result;
}

function parseTomlKey(value) {
  const trimmed = value.trim();
  if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
    return JSON.parse(trimmed);
  }
  return trimmed;
}

function parseTomlValue(value) {
  const trimmed = stripTomlInlineComment(value.trim());
  if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
    try {
      return JSON.parse(trimmed);
    } catch {
      return trimmed.slice(1, -1);
    }
  }
  if (trimmed === "true") return true;
  if (trimmed === "false") return false;
  return trimmed;
}

function stripTomlInlineComment(value) {
  let inString = false;
  let escaped = false;
  for (let i = 0; i < value.length; i += 1) {
    const char = value[i];
    if (escaped) {
      escaped = false;
      continue;
    }
    if (char === "\\") {
      escaped = true;
      continue;
    }
    if (char === '"') {
      inString = !inString;
      continue;
    }
    if (char === "#" && !inString) {
      return value.slice(0, i).trimEnd();
    }
  }
  return value;
}

function printInstallReport(result) {
  console.log("NODE.DC Ops Codex status");
  for (const entry of result.reports) {
    const report = entry.report;
    console.log("Codex home:", report.codexHome);
    console.log("Target:", entry.target.reason);
    for (const check of report.checks) {
      console.log(`${check.ok ? "OK" : "FAIL"} ${check.name}: ${check.detail}`);
    }
  }
  if (result.ok) {
    console.log("Result: ready");
  } else {
    console.log("Result: needs attention");
  }
}

async function smokeToolsList(endpoint, token) {
  return smokeToolsListWithAuthorization(endpoint, `Bearer ${token}`);
}

async function smokeToolsListWithAuthorization(endpoint, authorization) {
  let response;
  try {
    response = await fetch(endpoint, {
      method: "POST",
      headers: {
        Authorization: authorization,
        Accept: "application/json",
        "Content-Type": "application/json",
        "MCP-Protocol-Version": MCP_PROTOCOL_VERSION,
      },
      body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list" }),
    });
  } catch (error) {
    throw new Error(`MCP smoke check failed: ${error.message}`);
  }

  const bodyText = await response.text();
  let data;
  try {
    data = bodyText ? JSON.parse(bodyText) : {};
  } catch {
    throw new Error(`MCP smoke check failed: HTTP ${response.status} ${bodyText}`);
  }

  if (!response.ok) {
    throw new Error(`MCP smoke check failed: HTTP ${response.status} ${bodyText}`);
  }
  if (data.error) {
    throw new Error(`MCP smoke check failed: ${JSON.stringify(data.error)}`);
  }

  const tools = data.result?.tools || [];
  if (!Array.isArray(tools) || tools.length === 0) {
    throw new Error("MCP smoke check failed: tools/list returned no tools.");
  }
  return tools.length;
}

class UsageError extends Error {}

main().catch((error) => {
  if (error instanceof UsageError) {
    console.error(error.message);
    console.error("Run ops-codex --help for usage.");
    process.exit(2);
  }
  console.error(error.message || String(error));
  process.exit(1);
});
