/**
 * `agent-inspector onboard` subcommand.
 *
 * Generates Agent Inspector onboarding skills into the user's home dir.
 * Claude Code gets a skill + slash command. Codex gets a skill under
 * ~/.codex/skills that guides the user through connecting /api/mcp.
 *
 * Idempotent by default: re-runs without `--force` write missing files and
 * refresh older generated files. `--force` overwrites, `--dry-run` writes
 * nothing. Designed as an explicit setup step that can fail softly without
 * blocking normal CLI use.
 */

import {
  mkdirSync,
  writeFileSync,
  existsSync,
  readFileSync,
  unlinkSync,
  readdirSync,
  rmdirSync,
} from "node:fs";
import { homedir } from "node:os";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";

import { detectAll, detectFirst } from "./detect-tools.js";
import { renderCommandOnboard } from "./templates/command-onboard.js";
import {
  renderCodexSkillOnboard,
  REQUIRED_CODEX_PHASE_HEADINGS,
} from "./templates/codex-skill-onboard.js";
import { renderSkillOnboard, REQUIRED_PHASE_HEADINGS } from "./templates/skill-onboard.js";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

const DEFAULT_PORT = 25947;
const DEFAULT_MCP_URL = `http://localhost:${DEFAULT_PORT}/api/mcp`;
const SKILL_DIR_NAME = "agent-inspector-onboard";
const SKILL_FILE_NAME = "SKILL.md";
// Windows reserves `:` for NTFS alternate data streams, so a file named
// `agent-inspector:onboard.md` becomes an empty `agent-inspector` on disk.
// Use `-` on Windows and `:` on Unix to match the slash-command convention
// (`/agent-inspector:onboard` vs `/agent-inspector-onboard`) within the
// constraints of each platform.
const COMMAND_FILE_NAME =
  process.platform === "win32" ? "agent-inspector-onboard.md" : "agent-inspector:onboard.md";

const GENERATED_VERSION_PATTERN = /^\s*version:\s*([0-9]+\.[0-9]+\.[0-9]+(?:[-+][^\s]+)?)\s*$/m;
const FRONTMATTER_PATTERN = /^---\r?\n([\s\S]*?)\r?\n---/;
const SEMVER_PATTERN = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/;

export type OnboardFlags = {
  force: boolean;
  dryRun: boolean;
  skipProvider: boolean;
  skipToolWire: boolean;
  codexOnly: boolean;
  skipCodexSkill: boolean;
  opencode: boolean;
  opencodeOnly: boolean;
  opencodeConfig: string | null;
  opencodeTransport: AgentMcpTransport;
  opencodeMcpUrl: string;
  mimo: boolean;
  mimoOnly: boolean;
  mimoConfig: string | null;
  mimoTransport: AgentMcpTransport;
  mimoMcpUrl: string;
  uninstall: boolean;
  status: boolean;
  json: boolean;
  skillDir: string | null;
  codexSkillDir: string | null;
};

type AgentMcpTransport = "local" | "remote";
type AgentMcpStatusTransport = AgentMcpTransport | "unknown";
type AgentMcpConfigState = "missing" | "configured" | "custom" | "disabled" | "invalid";

type AgentMcpConfigTarget = {
  label: "OpenCode" | "MiMo Code";
  configLabel: "OpenCode MCP config" | "MiMo Code MCP config";
  configPath: string;
  transport: AgentMcpTransport;
  endpoint: string;
  setupAction: string;
  forceAction: string;
  verifyCommand: string;
  invalidAction: string;
  configObjectName: string;
  defaultSchema: string;
};

type AgentMcpConfigStatus = {
  label: "OpenCode MCP config" | "MiMo Code MCP config";
  path: string;
  state: AgentMcpConfigState;
  transport: AgentMcpStatusTransport;
  endpoint: string;
  action: string;
  forceAction: string;
  verifyCommand: string;
  detail: string;
};

type AgentMcpConfigPlan = {
  status: AgentMcpConfigStatus;
  body: string | null;
  shouldWrite: boolean;
  shouldRemove: boolean;
  blockedMessage: string | null;
};

type AgentConfigReadResult =
  | {
      ok: true;
      exists: boolean;
      config: Record<string, unknown>;
    }
  | {
      ok: false;
      exists: boolean;
      message: string;
    };

type OnboardTargets = {
  claudeSkillFile: string;
  claudeCommandFile: string;
  codexSkillFile: string;
};

type PlannedFile = {
  label: string;
  path: string;
  body: string;
  enabled: boolean;
};

type ParsedVersion = {
  major: number;
  minor: number;
  patch: number;
};

type GeneratedFileState = "missing" | "current" | "outdated" | "newer" | "custom";

type GeneratedFileStatus = {
  label: string;
  path: string;
  state: GeneratedFileState;
  version: string | null;
  action: string;
};

function actionForStatus(label: string, state: GeneratedFileState): string {
  switch (state) {
    case "missing":
      switch (label) {
        case "Codex skill":
          return "agent-inspector onboard --codex-only";
        case "Claude skill":
        case "Claude command":
          return "agent-inspector onboard --skip-codex-skill";
        default:
          return "agent-inspector onboard";
      }
    case "outdated":
      return "agent-inspector onboard --force";
    case "custom":
      return "preserved; use agent-inspector onboard --force to replace";
    case "newer":
      return "no action; installed file is newer than this package";
    case "current":
      return "no action";
    default:
      return "agent-inspector onboard";
  }
}

function parseFlags(argv: readonly string[]): OnboardFlags {
  const flags: OnboardFlags = {
    force: false,
    dryRun: false,
    skipProvider: false,
    skipToolWire: false,
    codexOnly: false,
    skipCodexSkill: false,
    opencode: false,
    opencodeOnly: false,
    opencodeConfig: null,
    opencodeTransport: "local",
    opencodeMcpUrl: DEFAULT_MCP_URL,
    mimo: false,
    mimoOnly: false,
    mimoConfig: null,
    mimoTransport: "local",
    mimoMcpUrl: DEFAULT_MCP_URL,
    uninstall: false,
    status: false,
    json: false,
    skillDir: null,
    codexSkillDir: null,
  };
  for (let i = 0; i < argv.length; i++) {
    const arg = argv[i];
    switch (arg) {
      case undefined:
        continue;
      case "--force":
        flags.force = true;
        break;
      case "--dry-run":
        flags.dryRun = true;
        break;
      case "--skip-provider":
        flags.skipProvider = true;
        break;
      case "--skip-tool-wire":
        flags.skipToolWire = true;
        break;
      case "--codex-only":
        flags.codexOnly = true;
        break;
      case "--skip-codex-skill":
        flags.skipCodexSkill = true;
        break;
      case "--opencode":
        flags.opencode = true;
        break;
      case "--opencode-only":
        flags.opencode = true;
        flags.opencodeOnly = true;
        break;
      case "--opencode-config": {
        const next = argv[i + 1];
        if (next === undefined) {
          process.stderr.write(
            "agent-inspector onboard: --opencode-config requires a path argument\n",
          );
          process.exit(2);
        }
        flags.opencodeConfig = next;
        i++;
        break;
      }
      case "--opencode-transport": {
        const next = argv[i + 1];
        if (next === undefined) {
          process.stderr.write(
            "agent-inspector onboard: --opencode-transport requires local or remote\n",
          );
          process.exit(2);
        }
        switch (next) {
          case "local":
          case "remote":
            flags.opencodeTransport = next;
            break;
          default:
            process.stderr.write(
              `agent-inspector onboard: invalid --opencode-transport: ${next}\n`,
            );
            process.exit(2);
        }
        i++;
        break;
      }
      case "--opencode-mcp-url": {
        const next = argv[i + 1];
        if (next === undefined) {
          process.stderr.write("agent-inspector onboard: --opencode-mcp-url requires a URL\n");
          process.exit(2);
        }
        flags.opencodeMcpUrl = next;
        i++;
        break;
      }
      case "--mimo":
        flags.mimo = true;
        break;
      case "--mimo-only":
        flags.mimo = true;
        flags.mimoOnly = true;
        break;
      case "--mimo-config": {
        const next = argv[i + 1];
        if (next === undefined) {
          process.stderr.write("agent-inspector onboard: --mimo-config requires a path argument\n");
          process.exit(2);
        }
        flags.mimoConfig = next;
        i++;
        break;
      }
      case "--mimo-transport": {
        const next = argv[i + 1];
        if (next === undefined) {
          process.stderr.write(
            "agent-inspector onboard: --mimo-transport requires local or remote\n",
          );
          process.exit(2);
        }
        switch (next) {
          case "local":
          case "remote":
            flags.mimoTransport = next;
            break;
          default:
            process.stderr.write(`agent-inspector onboard: invalid --mimo-transport: ${next}\n`);
            process.exit(2);
        }
        i++;
        break;
      }
      case "--mimo-mcp-url": {
        const next = argv[i + 1];
        if (next === undefined) {
          process.stderr.write("agent-inspector onboard: --mimo-mcp-url requires a URL\n");
          process.exit(2);
        }
        flags.mimoMcpUrl = next;
        i++;
        break;
      }
      case "--uninstall":
        flags.uninstall = true;
        break;
      case "--status":
        flags.status = true;
        break;
      case "--json":
        flags.json = true;
        break;
      case "--skill-dir": {
        const next = argv[i + 1];
        if (next === undefined) {
          process.stderr.write("agent-inspector onboard: --skill-dir requires a path argument\n");
          process.exit(2);
        }
        flags.skillDir = next;
        i++;
        break;
      }
      case "--codex-skill-dir": {
        const next = argv[i + 1];
        if (next === undefined) {
          process.stderr.write(
            "agent-inspector onboard: --codex-skill-dir requires a path argument\n",
          );
          process.exit(2);
        }
        flags.codexSkillDir = next;
        i++;
        break;
      }
      case "--help":
      case "-h":
        printHelp();
        process.exit(0);
        break;
      default:
        process.stderr.write(`agent-inspector onboard: unknown flag: ${arg}\n`);
        process.exit(2);
    }
  }
  return flags;
}

function printHelp(): void {
  process.stdout.write(`agent-inspector onboard - install Agent Inspector onboarding skills

Usage:
  agent-inspector onboard [options]

Options:
  --force                  Overwrite existing generated skills and slash command
  --dry-run                Print target paths and template previews, write nothing
  --skip-provider          Skip the provider-setup phase in the Claude skill body
  --skip-tool-wire         Skip the wire-tool phase in the Claude skill body
  --skip-codex-skill       Install only the Claude Code skill and slash command
  --codex-only             Install only the Codex skill
  --opencode               Also configure OpenCode MCP in opencode.json/jsonc
  --opencode-only          Configure only OpenCode MCP
  --opencode-config <path> Override OpenCode config path (default: ~/.config/opencode/opencode.json)
  --opencode-transport <local|remote>
                           OpenCode MCP transport to write (default: local)
  --opencode-mcp-url <url> Agent Inspector MCP endpoint for OpenCode
  --mimo                   Also configure MiMo Code MCP in mimocode.json/jsonc
  --mimo-only              Configure only MiMo Code MCP
  --mimo-config <path>     Override MiMo config path (default: ~/.config/mimocode/mimocode.jsonc)
  --mimo-transport <local|remote>
                           MiMo Code MCP transport to write (default: local)
  --mimo-mcp-url <url>     Agent Inspector MCP endpoint for MiMo Code
  --uninstall              Remove generated files whose version matches this package
  --status                 Show installed onboarding file versions and suggested action
  --json                   Emit machine-readable JSON with --status
  --skill-dir <path>       Override the target Claude root directory (default: ~/.claude)
  --codex-skill-dir <path> Override the target Codex skills directory (default: ~/.codex/skills)
  -h, --help               Show this help

Exit codes:
  0  success (or already installed)
  2  invalid arguments
  1  write failed
`);
}

function resolveTargets(flags: OnboardFlags): OnboardTargets {
  const claudeRoot = flags.skillDir ?? join(homedir(), ".claude");
  const claudeSkillDir = join(claudeRoot, "skills", SKILL_DIR_NAME);
  const claudeCommandsDir = join(claudeRoot, "commands");
  const codexRoot = process.env["CODEX_HOME"] ?? join(homedir(), ".codex");
  const codexSkillsDir = flags.codexSkillDir ?? join(codexRoot, "skills");
  return {
    claudeSkillFile: join(claudeSkillDir, SKILL_FILE_NAME),
    claudeCommandFile: join(claudeCommandsDir, COMMAND_FILE_NAME),
    codexSkillFile: join(codexSkillsDir, SKILL_DIR_NAME, SKILL_FILE_NAME),
  };
}

function isObject(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null;
}

function isOpenCodeEnabled(flags: OnboardFlags): boolean {
  return flags.opencode || flags.opencodeOnly;
}

function isMiMoEnabled(flags: OnboardFlags): boolean {
  return flags.mimo || flags.mimoOnly;
}

function hasOnlyAgentMcpTarget(flags: OnboardFlags): boolean {
  return flags.opencodeOnly || flags.mimoOnly;
}

function resolveOpenCodeConfigPath(flags: OnboardFlags): string {
  if (flags.opencodeConfig !== null) {
    return flags.opencodeConfig;
  }

  const configDir = join(homedir(), ".config", "opencode");
  const jsoncPath = join(configDir, "opencode.jsonc");
  if (existsSync(jsoncPath)) {
    return jsoncPath;
  }
  return join(configDir, "opencode.json");
}

function resolveMiMoConfigPath(flags: OnboardFlags): string {
  if (flags.mimoConfig !== null) {
    return flags.mimoConfig;
  }

  const configDir = join(homedir(), ".config", "mimocode");
  const jsoncPath = join(configDir, "mimocode.jsonc");
  if (existsSync(jsoncPath)) {
    return jsoncPath;
  }

  const jsonPath = join(configDir, "mimocode.json");
  if (existsSync(jsonPath)) {
    return jsonPath;
  }

  const legacyDir = join(homedir(), ".mimocode");
  const legacyJsoncPath = join(legacyDir, "config.jsonc");
  if (existsSync(legacyJsoncPath)) {
    return legacyJsoncPath;
  }

  const legacyJsonPath = join(legacyDir, "config.json");
  if (existsSync(legacyJsonPath)) {
    return legacyJsonPath;
  }

  return jsoncPath;
}

function openCodeTarget(flags: OnboardFlags): AgentMcpConfigTarget {
  return {
    label: "OpenCode",
    configLabel: "OpenCode MCP config",
    configPath: resolveOpenCodeConfigPath(flags),
    transport: flags.opencodeTransport,
    endpoint: flags.opencodeMcpUrl,
    setupAction: "agent-inspector onboard --opencode-only",
    forceAction: "agent-inspector onboard --opencode-only --force",
    verifyCommand: "opencode mcp list",
    invalidAction: "fix OpenCode JSON/JSONC, then rerun agent-inspector onboard --opencode-only",
    configObjectName: "mcp.agent-inspector",
    defaultSchema: "https://opencode.ai/config.json",
  };
}

function miMoTarget(flags: OnboardFlags): AgentMcpConfigTarget {
  return {
    label: "MiMo Code",
    configLabel: "MiMo Code MCP config",
    configPath: resolveMiMoConfigPath(flags),
    transport: flags.mimoTransport,
    endpoint: flags.mimoMcpUrl,
    setupAction: "agent-inspector onboard --mimo-only",
    forceAction: "agent-inspector onboard --mimo-only --force",
    verifyCommand: "mimo mcp list",
    invalidAction: "fix MiMo Code JSON/JSONC, then rerun agent-inspector onboard --mimo-only",
    configObjectName: "mcp.agent-inspector",
    defaultSchema: "https://mimo.xiaomi.com/mimocode/config.json",
  };
}

function stripJsoncComments(raw: string): string {
  let result = "";
  let index = 0;
  let inString = false;
  let stringChar = "";

  while (index < raw.length) {
    const ch = raw[index];
    const next = raw[index + 1];

    if (inString) {
      if (ch === "\\" && next !== undefined) {
        result += ch + next;
        index += 2;
        continue;
      }
      if (ch === stringChar) {
        inString = false;
        stringChar = "";
      }
      result += ch;
      index += 1;
      continue;
    }

    if (ch === '"' || ch === "'") {
      inString = true;
      stringChar = ch;
      result += ch;
      index += 1;
      continue;
    }

    if (ch === "/" && next === "/") {
      while (index < raw.length && raw[index] !== "\n") {
        index += 1;
      }
      continue;
    }

    if (ch === "/" && next === "*") {
      index += 2;
      while (index < raw.length - 1 && !(raw[index] === "*" && raw[index + 1] === "/")) {
        index += 1;
      }
      index += 2;
      continue;
    }

    result += ch;
    index += 1;
  }

  return result;
}

function readAgentConfig(path: string, target: AgentMcpConfigTarget): AgentConfigReadResult {
  if (!existsSync(path)) {
    return { ok: true, exists: false, config: {} };
  }

  try {
    const raw = readFileSync(path, "utf8");
    if (raw.trim().length === 0) {
      return { ok: true, exists: true, config: {} };
    }
    const parsed: unknown = JSON.parse(stripJsoncComments(raw));
    if (isObject(parsed)) {
      return { ok: true, exists: true, config: parsed };
    }
    return {
      ok: false,
      exists: true,
      message: `${target.label} config root must be a JSON object`,
    };
  } catch (err) {
    const message = err instanceof Error ? err.message : String(err);
    return {
      ok: false,
      exists: true,
      message,
    };
  }
}

function buildAgentMcpEntry(
  transport: AgentMcpTransport,
  endpoint: string,
): Record<string, unknown> {
  switch (transport) {
    case "local":
      return {
        type: "local",
        command: ["agent-inspector-mcp", "stdio", "--url", endpoint],
        enabled: true,
      };
    case "remote":
      return {
        type: "remote",
        url: endpoint,
        enabled: true,
      };
    default:
      return {
        type: "local",
        command: ["agent-inspector-mcp", "stdio", "--url", endpoint],
        enabled: true,
      };
  }
}

function stringArrayEquals(value: unknown, expected: readonly string[]): boolean {
  if (!Array.isArray(value) || value.length !== expected.length) {
    return false;
  }

  return value.every((item, index) => item === expected[index]);
}

function isAgentLocalEntry(value: unknown, endpoint: string, ignoreEnabled: boolean): boolean {
  if (!isObject(value)) {
    return false;
  }

  const enabled = value["enabled"];
  if (!ignoreEnabled && enabled === false) {
    return false;
  }

  const env = value["environment"];
  const usesUrlArg = stringArrayEquals(value["command"], [
    "agent-inspector-mcp",
    "stdio",
    "--url",
    endpoint,
  ]);
  const usesEnv =
    stringArrayEquals(value["command"], ["agent-inspector-mcp", "stdio"]) &&
    isObject(env) &&
    env["AGENT_INSPECTOR_MCP_URL"] === endpoint;

  return value["type"] === "local" && (usesUrlArg || usesEnv);
}

function isAgentRemoteEntry(value: unknown, endpoint: string, ignoreEnabled: boolean): boolean {
  if (!isObject(value)) {
    return false;
  }

  const enabled = value["enabled"];
  if (!ignoreEnabled && enabled === false) {
    return false;
  }

  return value["type"] === "remote" && value["url"] === endpoint;
}

function isKnownAgentEntry(value: unknown, endpoint: string, ignoreEnabled: boolean): boolean {
  return (
    isAgentLocalEntry(value, endpoint, ignoreEnabled) ||
    isAgentRemoteEntry(value, endpoint, ignoreEnabled)
  );
}

function detectAgentTransport(value: unknown): AgentMcpStatusTransport {
  if (!isObject(value)) {
    return "unknown";
  }

  switch (value["type"]) {
    case "local":
      return "local";
    case "remote":
      return "remote";
    default:
      return "unknown";
  }
}

function agentActionForStatus(target: AgentMcpConfigTarget, status: AgentMcpConfigStatus): string {
  switch (status.state) {
    case "missing":
      return target.setupAction;
    case "configured":
      return target.verifyCommand;
    case "disabled":
    case "custom":
      return target.forceAction;
    case "invalid":
      return target.invalidAction;
    default:
      return target.setupAction;
  }
}

function makeAgentStatus(
  target: AgentMcpConfigTarget,
  state: AgentMcpConfigState,
  transport: AgentMcpStatusTransport,
  detail: string,
): AgentMcpConfigStatus {
  const status: AgentMcpConfigStatus = {
    label: target.configLabel,
    path: target.configPath,
    state,
    transport,
    endpoint: target.endpoint,
    action: target.setupAction,
    forceAction: target.forceAction,
    verifyCommand: target.verifyCommand,
    detail,
  };

  return {
    ...status,
    action: agentActionForStatus(target, status),
  };
}

function getAgentEntry(config: Record<string, unknown>): unknown {
  const mcp = config["mcp"];
  if (!isObject(mcp)) {
    return undefined;
  }
  return mcp["agent-inspector"];
}

function buildAgentConfig(
  config: Record<string, unknown>,
  target: AgentMcpConfigTarget,
): Record<string, unknown> {
  const schema = typeof config["$schema"] === "string" ? config["$schema"] : target.defaultSchema;
  const nextConfig: Record<string, unknown> = {
    $schema: schema,
  };

  for (const [key, value] of Object.entries(config)) {
    if (key !== "$schema" && key !== "mcp") {
      nextConfig[key] = value;
    }
  }

  const currentMcp = config["mcp"];
  const nextMcp: Record<string, unknown> = isObject(currentMcp) ? { ...currentMcp } : {};
  nextMcp["agent-inspector"] = buildAgentMcpEntry(target.transport, target.endpoint);
  nextConfig["mcp"] = nextMcp;
  return nextConfig;
}

function formatAgentConfig(config: Record<string, unknown>): string {
  return `${JSON.stringify(config, null, 2)}\n`;
}

function removeAgentEntry(config: Record<string, unknown>): Record<string, unknown> {
  const nextConfig: Record<string, unknown> = {};
  for (const [key, value] of Object.entries(config)) {
    if (key !== "mcp") {
      nextConfig[key] = value;
    }
  }

  const currentMcp = config["mcp"];
  if (!isObject(currentMcp)) {
    return nextConfig;
  }

  const nextMcp: Record<string, unknown> = {};
  for (const [key, value] of Object.entries(currentMcp)) {
    if (key !== "agent-inspector") {
      nextMcp[key] = value;
    }
  }

  if (Object.keys(nextMcp).length > 0) {
    nextConfig["mcp"] = nextMcp;
  }
  return nextConfig;
}

function readAgentStatus(target: AgentMcpConfigTarget): AgentMcpConfigStatus {
  const read = readAgentConfig(target.configPath, target);
  if (!read.ok) {
    return makeAgentStatus(target, "invalid", "unknown", read.message);
  }

  const entry = getAgentEntry(read.config);
  if (entry === undefined) {
    const detail = read.exists
      ? `${target.label} config has no ${target.configObjectName}`
      : "Missing file";
    return makeAgentStatus(target, "missing", "unknown", detail);
  }

  const transport = detectAgentTransport(entry);
  if (isObject(entry) && entry["enabled"] === false) {
    return makeAgentStatus(
      target,
      "disabled",
      transport,
      `${target.configObjectName} is present but disabled`,
    );
  }

  if (isKnownAgentEntry(entry, target.endpoint, false)) {
    return makeAgentStatus(
      target,
      "configured",
      transport,
      `${target.configObjectName} points at Agent Inspector MCP`,
    );
  }

  return makeAgentStatus(
    target,
    "custom",
    transport,
    `${target.configObjectName} exists but does not match Agent Inspector defaults`,
  );
}

function buildAgentPlan(
  target: AgentMcpConfigTarget,
  uninstall: boolean,
  force: boolean,
): AgentMcpConfigPlan {
  const read = readAgentConfig(target.configPath, target);
  if (!read.ok) {
    return {
      status: makeAgentStatus(target, "invalid", "unknown", read.message),
      body: null,
      shouldWrite: false,
      shouldRemove: false,
      blockedMessage: `${target.label} config is invalid: ${read.message}`,
    };
  }

  const status = readAgentStatus(target);
  const entry = getAgentEntry(read.config);
  const nextBody = formatAgentConfig(buildAgentConfig(read.config, target));

  if (uninstall) {
    const shouldRemove = entry !== undefined && isKnownAgentEntry(entry, target.endpoint, true);
    const removeBody = shouldRemove ? formatAgentConfig(removeAgentEntry(read.config)) : null;
    return {
      status,
      body: removeBody,
      shouldWrite: false,
      shouldRemove,
      blockedMessage: shouldRemove
        ? null
        : `${target.label} ${target.configObjectName} is missing or custom; preserved`,
    };
  }

  switch (status.state) {
    case "configured":
      return {
        status,
        body: nextBody,
        shouldWrite: force,
        shouldRemove: false,
        blockedMessage: null,
      };
    case "missing":
      return {
        status,
        body: nextBody,
        shouldWrite: true,
        shouldRemove: false,
        blockedMessage: null,
      };
    case "custom":
    case "disabled":
      return {
        status,
        body: nextBody,
        shouldWrite: force,
        shouldRemove: false,
        blockedMessage: force
          ? null
          : `${target.label} ${target.configObjectName} is custom or disabled; use --force to replace`,
      };
    case "invalid":
      return {
        status,
        body: null,
        shouldWrite: false,
        shouldRemove: false,
        blockedMessage: `${target.label} config is invalid`,
      };
    default:
      return {
        status,
        body: null,
        shouldWrite: false,
        shouldRemove: false,
        blockedMessage: `${target.label} config state is unknown`,
      };
  }
}

function buildDetectedSummary(): string {
  const entries = detectAll();
  const present = entries.filter((e) => e.result.found);
  const absent = entries.filter((e) => !e.result.found);
  const presentList = present.map((e) => e.displayName).join(", ") || "(none)";
  const absentList = absent.map((e) => e.displayName).join(", ") || "(none)";
  return `  - Detected: ${presentList}\n  - Not detected: ${absentList}`;
}

function readPackageVersion(): string {
  const packageJsonPaths = [
    join(__dirname, "..", "package.json"),
    join(__dirname, "..", "..", "package.json"),
  ];

  try {
    for (const packageJsonPath of packageJsonPaths) {
      if (existsSync(packageJsonPath)) {
        const raw: unknown = JSON.parse(readFileSync(packageJsonPath, "utf8"));
        if (isObject(raw) && typeof raw["version"] === "string") {
          return raw["version"];
        }
      }
    }
  } catch {
    // best-effort: leave version as the fallback
  }
  return "0.0.0";
}

function parseVersion(version: string): ParsedVersion | null {
  const match = SEMVER_PATTERN.exec(version);
  if (match === null) {
    return null;
  }

  const majorText = match[1];
  const minorText = match[2];
  const patchText = match[3];
  if (majorText === undefined || minorText === undefined || patchText === undefined) {
    return null;
  }

  const major = Number(majorText);
  const minor = Number(minorText);
  const patch = Number(patchText);
  if (!Number.isInteger(major) || !Number.isInteger(minor) || !Number.isInteger(patch)) {
    return null;
  }

  return { major, minor, patch };
}

function compareVersions(left: string, right: string): number | null {
  const leftVersion = parseVersion(left);
  const rightVersion = parseVersion(right);
  if (leftVersion === null || rightVersion === null) {
    return null;
  }

  const leftParts = [leftVersion.major, leftVersion.minor, leftVersion.patch];
  const rightParts = [rightVersion.major, rightVersion.minor, rightVersion.patch];
  for (let index = 0; index < leftParts.length; index++) {
    const leftPart = leftParts[index];
    const rightPart = rightParts[index];
    if (leftPart === undefined || rightPart === undefined) {
      return null;
    }
    if (leftPart > rightPart) {
      return 1;
    }
    if (leftPart < rightPart) {
      return -1;
    }
  }

  return 0;
}

function extractGeneratedVersion(body: string): string | null {
  const frontmatterMatch = FRONTMATTER_PATTERN.exec(body);
  if (frontmatterMatch === null) {
    return null;
  }

  const frontmatter = frontmatterMatch[1];
  if (frontmatter === undefined) {
    return null;
  }

  const versionMatch = GENERATED_VERSION_PATTERN.exec(frontmatter);
  if (versionMatch === null) {
    return null;
  }

  return versionMatch[1] ?? null;
}

function hasAgentInspectorAuthor(body: string): boolean {
  const frontmatterMatch = FRONTMATTER_PATTERN.exec(body);
  if (frontmatterMatch === null) {
    return false;
  }

  const frontmatter = frontmatterMatch[1];
  if (frontmatter === undefined) {
    return false;
  }

  return /^\s*author:\s*agent-inspector\s*$/m.test(frontmatter);
}

function readGeneratedVersion(path: string): string | null {
  try {
    return extractGeneratedVersion(readFileSync(path, "utf8"));
  } catch {
    return null;
  }
}

function isMatchingGeneratedFile(path: string, currentVersion: string): boolean {
  try {
    const body = readFileSync(path, "utf8");
    return hasAgentInspectorAuthor(body) && extractGeneratedVersion(body) === currentVersion;
  } catch {
    return false;
  }
}

function readGeneratedFileStatus(file: PlannedFile, currentVersion: string): GeneratedFileStatus {
  if (!existsSync(file.path)) {
    return {
      label: file.label,
      path: file.path,
      state: "missing",
      version: null,
      action: actionForStatus(file.label, "missing"),
    };
  }

  try {
    const body = readFileSync(file.path, "utf8");
    const version = extractGeneratedVersion(body);
    if (!hasAgentInspectorAuthor(body) || version === null) {
      return {
        label: file.label,
        path: file.path,
        state: "custom",
        version,
        action: actionForStatus(file.label, "custom"),
      };
    }

    const comparison = compareVersions(version, currentVersion);
    if (comparison === null) {
      return {
        label: file.label,
        path: file.path,
        state: "custom",
        version,
        action: actionForStatus(file.label, "custom"),
      };
    }
    if (comparison < 0) {
      return {
        label: file.label,
        path: file.path,
        state: "outdated",
        version,
        action: actionForStatus(file.label, "outdated"),
      };
    }
    if (comparison > 0) {
      return {
        label: file.label,
        path: file.path,
        state: "newer",
        version,
        action: actionForStatus(file.label, "newer"),
      };
    }

    return {
      label: file.label,
      path: file.path,
      state: "current",
      version,
      action: actionForStatus(file.label, "current"),
    };
  } catch {
    return {
      label: file.label,
      path: file.path,
      state: "custom",
      version: null,
      action: actionForStatus(file.label, "custom"),
    };
  }
}

function buildPlannedFiles(
  flags: OnboardFlags,
  targets: OnboardTargets,
  version: string,
): PlannedFile[] {
  const installClaude = !flags.codexOnly && !hasOnlyAgentMcpTarget(flags);
  const installCodex = !flags.skipCodexSkill && !hasOnlyAgentMcpTarget(flags);
  const detectedSummary = installClaude ? buildDetectedSummary() : "";
  const claudeSkillBody = installClaude
    ? renderSkillOnboard({
        version,
        port: DEFAULT_PORT,
        detectedSummary,
      })
    : "";
  const commandBody = installClaude ? renderCommandOnboard({ version }) : "";
  const codexSkillBody = installCodex
    ? renderCodexSkillOnboard({
        version,
        port: DEFAULT_PORT,
      })
    : "";

  return [
    {
      label: "Claude skill",
      path: targets.claudeSkillFile,
      body: claudeSkillBody,
      enabled: installClaude,
    },
    {
      label: "Claude command",
      path: targets.claudeCommandFile,
      body: commandBody,
      enabled: installClaude,
    },
    {
      label: "Codex skill",
      path: targets.codexSkillFile,
      body: codexSkillBody,
      enabled: installCodex,
    },
  ];
}

function shouldWrite(file: PlannedFile, force: boolean, currentVersion: string): boolean {
  if (!file.enabled) {
    return false;
  }
  if (force || !existsSync(file.path)) {
    return true;
  }

  const existingVersion = readGeneratedVersion(file.path);
  if (existingVersion === null) {
    return false;
  }

  const comparison = compareVersions(existingVersion, currentVersion);
  return comparison !== null && comparison < 0;
}

function shouldUninstall(file: PlannedFile, currentVersion: string): boolean {
  return (
    file.enabled && existsSync(file.path) && isMatchingGeneratedFile(file.path, currentVersion)
  );
}

function removeEmptyDirectory(path: string): void {
  try {
    if (readdirSync(path).length === 0) {
      rmdirSync(path);
    }
  } catch {
    // best-effort cleanup only
  }
}

function runUninstall(
  plannedFiles: readonly PlannedFile[],
  currentVersion: string,
  dryRun: boolean,
  agentPlans: readonly AgentMcpConfigPlan[],
): number {
  const enabledFiles = plannedFiles.filter((file) => file.enabled);
  const filesToRemove = enabledFiles.filter((file) => shouldUninstall(file, currentVersion));

  if (dryRun) {
    process.stdout.write(`agent-inspector onboard --uninstall --dry-run\n\n`);
    for (const file of enabledFiles) {
      const status = shouldUninstall(file, currentVersion) ? "matching" : "skipped";
      process.stdout.write(`${file.label}: ${file.path} (${status})\n`);
    }
    for (const plan of agentPlans) {
      const status = plan.shouldRemove ? "matching" : "skipped";
      process.stdout.write(`${plan.status.label}: ${plan.status.path} (${status})\n`);
      if (plan.blockedMessage !== null) {
        process.stdout.write(`  ${plan.blockedMessage}\n`);
      }
    }
    process.stdout.write(`\nNo files were removed.\n`);
    return 0;
  }

  const hasAgentConfigToRemove = agentPlans.some((plan) => plan.shouldRemove);
  if (filesToRemove.length === 0 && !hasAgentConfigToRemove) {
    process.stdout.write(
      "agent-inspector onboard: no generated onboarding files match this package version\n",
    );
    for (const plan of agentPlans) {
      if (plan.blockedMessage !== null) {
        process.stdout.write(`${plan.blockedMessage}\n`);
      }
    }
    return 0;
  }

  try {
    for (const file of filesToRemove) {
      unlinkSync(file.path);
      removeEmptyDirectory(dirname(file.path));
      process.stdout.write(`Removed ${file.label}: ${file.path}\n`);
    }
    for (const plan of agentPlans) {
      if (plan.shouldRemove && plan.body !== null) {
        mkdirSync(dirname(plan.status.path), { recursive: true });
        writeFileSync(plan.status.path, plan.body, "utf8");
        process.stdout.write(`Removed ${plan.status.label} entry from: ${plan.status.path}\n`);
      }
    }
  } catch (err) {
    const msg = err instanceof Error ? err.message : String(err);
    process.stderr.write(`agent-inspector onboard: failed to remove files: ${msg}\n`);
    return 1;
  }

  return 0;
}

function formatStatusVersion(status: GeneratedFileStatus): string {
  return status.version ?? "-";
}

function summarizeStatusAction(
  statuses: readonly GeneratedFileStatus[],
  agentStatuses: readonly AgentMcpConfigStatus[],
): string {
  const actionable = statuses.filter(
    (status) => status.state === "missing" || status.state === "outdated",
  );
  const actions = actionable.map((status) => status.action);
  for (const agentStatus of agentStatuses) {
    if (agentStatus.state === "missing") {
      actions.push(agentStatus.action);
    }
  }
  if (actions.length > 0) {
    const uniqueActions = Array.from(new Set(actions));
    return `Run: ${uniqueActions.join(" && ")}`;
  }
  for (const agentStatus of agentStatuses) {
    if (
      agentStatus.state === "custom" ||
      agentStatus.state === "disabled" ||
      agentStatus.state === "invalid"
    ) {
      return `${agentStatus.label} needs review: ${agentStatus.action}`;
    }
  }
  if (statuses.some((status) => status.state === "custom")) {
    return "Custom files are preserved. Use --force only if you want to replace them.";
  }
  if (statuses.some((status) => status.state === "newer")) {
    return "Installed files are newer than this package. No action needed.";
  }
  return "All selected onboarding files are current.";
}

function runStatus(
  plannedFiles: readonly PlannedFile[],
  currentVersion: string,
  json: boolean,
  agentStatuses: readonly AgentMcpConfigStatus[],
): number {
  const enabledFiles = plannedFiles.filter((file) => file.enabled);
  const statuses = enabledFiles.map((file) => readGeneratedFileStatus(file, currentVersion));
  const summary = summarizeStatusAction(statuses, agentStatuses);
  const opencodeConfig =
    agentStatuses.find((status) => status.label === "OpenCode MCP config") ?? null;
  const mimoConfig =
    agentStatuses.find((status) => status.label === "MiMo Code MCP config") ?? null;

  if (json) {
    process.stdout.write(
      `${JSON.stringify(
        {
          packageVersion: currentVersion,
          files: statuses,
          opencodeConfig,
          mimoConfig,
          suggestedAction: summary,
        },
        null,
        2,
      )}\n`,
    );
    return 0;
  }

  process.stdout.write(`agent-inspector onboard status\n`);
  process.stdout.write(`Package version: ${currentVersion}\n\n`);

  for (const status of statuses) {
    process.stdout.write(`${status.label}: ${status.state}`);
    process.stdout.write(`, version ${formatStatusVersion(status)}\n`);
    process.stdout.write(`  ${status.path}\n`);
    process.stdout.write(`  Action: ${status.action}\n`);
  }

  for (const agentStatus of agentStatuses) {
    process.stdout.write(`${agentStatus.label}: ${agentStatus.state}`);
    process.stdout.write(`, transport ${agentStatus.transport}\n`);
    process.stdout.write(`  ${agentStatus.path}\n`);
    process.stdout.write(`  Endpoint: ${agentStatus.endpoint}\n`);
    process.stdout.write(`  Detail: ${agentStatus.detail}\n`);
    process.stdout.write(`  Action: ${agentStatus.action}\n`);
  }

  process.stdout.write(`\nSuggested action: ${summary}\n`);
  return 0;
}

export function runOnboard(argv: readonly string[]): Promise<number> {
  try {
    return Promise.resolve(runOnboardSync(argv));
  } catch (err) {
    // Last-resort safety net: report setup failures without blocking normal CLI use.
    const msg = err instanceof Error ? err.message : String(err);
    process.stderr.write(`agent-inspector onboard: ${msg}\n`);
    process.stderr.write("(run `agent-inspector onboard` again after fixing the issue)\n");
    return Promise.resolve(0);
  }
}

function runOnboardSync(argv: readonly string[]): number {
  const flags = parseFlags(argv);
  const targets = resolveTargets(flags);
  const version = readPackageVersion();
  const plannedFiles = buildPlannedFiles(flags, targets, version);
  const enabledFiles = plannedFiles.filter((file) => file.enabled);
  const agentPlans: AgentMcpConfigPlan[] = [];
  if (isOpenCodeEnabled(flags)) {
    agentPlans.push(buildAgentPlan(openCodeTarget(flags), flags.uninstall, flags.force));
  }
  if (isMiMoEnabled(flags)) {
    agentPlans.push(buildAgentPlan(miMoTarget(flags), flags.uninstall, flags.force));
  }
  const agentStatuses = agentPlans.map((plan) => plan.status);

  if (flags.json && !flags.status) {
    process.stderr.write("agent-inspector onboard: --json is only supported with --status\n");
    return 2;
  }

  if (enabledFiles.length === 0 && agentPlans.length === 0) {
    process.stderr.write("agent-inspector onboard: no onboarding target selected\n");
    return 2;
  }

  if (flags.status) {
    return runStatus(plannedFiles, version, flags.json, agentStatuses);
  }

  if (flags.uninstall) {
    return runUninstall(plannedFiles, version, flags.dryRun, agentPlans);
  }

  if (flags.dryRun) {
    process.stdout.write(`agent-inspector onboard --dry-run\n\n`);
    for (const file of enabledFiles) {
      const status = existsSync(file.path) ? "exists" : "missing";
      process.stdout.write(`${file.label}: ${file.path} (${status})\n`);
    }
    for (const plan of agentPlans) {
      process.stdout.write(`${plan.status.label}: ${plan.status.path} (${plan.status.state})\n`);
      process.stdout.write(`  Transport: ${plan.status.transport}\n`);
      process.stdout.write(`  Endpoint: ${plan.status.endpoint}\n`);
      process.stdout.write(`  Action: ${plan.status.action}\n`);
      if (plan.blockedMessage !== null) {
        process.stdout.write(`  ${plan.blockedMessage}\n`);
      }
    }
    process.stdout.write(`\nClaude skill headings:\n`);
    if (!flags.codexOnly && !hasOnlyAgentMcpTarget(flags)) {
      for (const heading of REQUIRED_PHASE_HEADINGS) {
        process.stdout.write(`  - ${heading}\n`);
      }
    } else {
      process.stdout.write(`  (disabled by selected targets)\n`);
    }
    process.stdout.write(`\nCodex skill headings:\n`);
    if (!flags.skipCodexSkill && !hasOnlyAgentMcpTarget(flags)) {
      for (const heading of REQUIRED_CODEX_PHASE_HEADINGS) {
        process.stdout.write(`  - ${heading}\n`);
      }
    } else {
      process.stdout.write(`  (disabled by selected targets)\n`);
    }
    if (!flags.codexOnly && !hasOnlyAgentMcpTarget(flags)) {
      process.stdout.write(`\nDetected tools:\n${buildDetectedSummary()}\n`);
    }
    process.stdout.write(`\nNo files were written.\n`);
    return 0;
  }

  const filesToWrite = plannedFiles.filter((file) => shouldWrite(file, flags.force, version));
  const agentPlansToWrite = agentPlans.filter((plan) => plan.shouldWrite && plan.body !== null);
  for (const plan of agentPlans) {
    if (plan.status.state === "invalid") {
      process.stderr.write(`agent-inspector onboard: ${plan.blockedMessage ?? "invalid"}\n`);
      return 1;
    }
  }
  if (filesToWrite.length === 0 && agentPlansToWrite.length === 0) {
    process.stdout.write(
      "agent-inspector onboard: all selected onboarding files are already up to date\n",
    );
    for (const plan of agentPlans) {
      if (plan.blockedMessage !== null) {
        process.stdout.write(`${plan.blockedMessage}\n`);
      }
    }
    process.stdout.write("Re-run with --force to refresh.\n");
    return 0;
  }

  try {
    for (const file of filesToWrite) {
      mkdirSync(dirname(file.path), { recursive: true });
      writeFileSync(file.path, file.body, "utf8");
    }
    for (const plan of agentPlansToWrite) {
      if (plan.body !== null) {
        mkdirSync(dirname(plan.status.path), { recursive: true });
        writeFileSync(plan.status.path, plan.body, "utf8");
      }
    }
  } catch (err) {
    const msg = err instanceof Error ? err.message : String(err);
    process.stderr.write(`agent-inspector onboard: failed to write files: ${msg}\n`);
    return 1;
  }

  const firstTool = detectFirst();
  const toolHint = firstTool !== null ? firstTool.displayName : "no known tool detected";

  for (const file of filesToWrite) {
    process.stdout.write(`Installed/updated ${file.label} to: ${file.path}\n`);
  }
  for (const plan of agentPlansToWrite) {
    process.stdout.write(`Installed/updated ${plan.status.label}: ${plan.status.path}\n`);
  }
  process.stdout.write(`\nNext steps:\n`);
  if (!flags.codexOnly && !hasOnlyAgentMcpTarget(flags)) {
    process.stdout.write(`  - Open Claude Code and run: /agent-inspector:onboard\n`);
  }
  if (!flags.skipCodexSkill && !hasOnlyAgentMcpTarget(flags)) {
    process.stdout.write(`  - Open Codex and ask to use the agent-inspector-onboard skill\n`);
  }
  for (const plan of agentPlans) {
    process.stdout.write(`  - Verify ${plan.status.label}: ${plan.status.verifyCommand}\n`);
  }
  if (hasOnlyAgentMcpTarget(flags)) {
    const refreshActions = agentPlans.map((plan) => plan.status.forceAction);
    const uniqueRefreshActions = Array.from(new Set(refreshActions));
    const refresh =
      uniqueRefreshActions.length > 0
        ? uniqueRefreshActions.join(" && ")
        : "agent-inspector onboard --force";
    process.stdout.write(`  - Refresh selected MCP config later: ${refresh}\n`);
  } else {
    process.stdout.write(`  - Refresh later: agent-inspector onboard --force\n`);
    process.stdout.write(`  - Detected primary tool: ${toolHint}\n`);
  }
  return 0;
}
