/**
 * `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: OpenCodeTransport;
  opencodeMcpUrl: string;
  uninstall: boolean;
  status: boolean;
  json: boolean;
  skillDir: string | null;
  codexSkillDir: string | null;
};

type OpenCodeTransport = "local" | "remote";
type OpenCodeStatusTransport = OpenCodeTransport | "unknown";
type OpenCodeConfigState = "missing" | "configured" | "custom" | "disabled" | "invalid";

type OpenCodeConfigStatus = {
  label: "OpenCode MCP config";
  path: string;
  state: OpenCodeConfigState;
  transport: OpenCodeStatusTransport;
  endpoint: string;
  action: string;
  detail: string;
};

type OpenCodeConfigPlan = {
  status: OpenCodeConfigStatus;
  body: string | null;
  shouldWrite: boolean;
  shouldRemove: boolean;
  blockedMessage: string | null;
};

type OpenCodeReadResult =
  | {
      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,
    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 "--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
  --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 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 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 readOpenCodeConfig(path: string): OpenCodeReadResult {
  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: "OpenCode 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 buildOpenCodeMcpEntry(
  transport: OpenCodeTransport,
  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 isOpenCodeLocalEntry(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 isOpenCodeRemoteEntry(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 isKnownOpenCodeEntry(value: unknown, endpoint: string, ignoreEnabled: boolean): boolean {
  return (
    isOpenCodeLocalEntry(value, endpoint, ignoreEnabled) ||
    isOpenCodeRemoteEntry(value, endpoint, ignoreEnabled)
  );
}

function detectOpenCodeTransport(value: unknown): OpenCodeStatusTransport {
  if (!isObject(value)) {
    return "unknown";
  }

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

function openCodeActionForStatus(status: OpenCodeConfigStatus): string {
  switch (status.state) {
    case "missing":
      return "agent-inspector onboard --opencode-only";
    case "configured":
      return "opencode mcp list";
    case "disabled":
    case "custom":
      return "agent-inspector onboard --opencode-only --force";
    case "invalid":
      return "fix OpenCode JSON/JSONC, then rerun agent-inspector onboard --opencode-only";
    default:
      return "agent-inspector onboard --opencode-only";
  }
}

function makeOpenCodeStatus(
  path: string,
  state: OpenCodeConfigState,
  transport: OpenCodeStatusTransport,
  endpoint: string,
  detail: string,
): OpenCodeConfigStatus {
  const status: OpenCodeConfigStatus = {
    label: "OpenCode MCP config",
    path,
    state,
    transport,
    endpoint,
    action: "agent-inspector onboard --opencode-only",
    detail,
  };

  return {
    ...status,
    action: openCodeActionForStatus(status),
  };
}

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

function buildOpenCodeConfig(
  config: Record<string, unknown>,
  transport: OpenCodeTransport,
  endpoint: string,
): Record<string, unknown> {
  const schema =
    typeof config["$schema"] === "string" ? config["$schema"] : "https://opencode.ai/config.json";
  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"] = buildOpenCodeMcpEntry(transport, endpoint);
  nextConfig["mcp"] = nextMcp;
  return nextConfig;
}

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

function removeOpenCodeEntry(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 readOpenCodeStatus(flags: OnboardFlags): OpenCodeConfigStatus {
  const path = resolveOpenCodeConfigPath(flags);
  const read = readOpenCodeConfig(path);
  if (!read.ok) {
    return makeOpenCodeStatus(path, "invalid", "unknown", flags.opencodeMcpUrl, read.message);
  }

  const entry = getOpenCodeEntry(read.config);
  if (entry === undefined) {
    const detail = read.exists
      ? "OpenCode config has no mcp.agent-inspector entry"
      : "Missing file";
    return makeOpenCodeStatus(path, "missing", "unknown", flags.opencodeMcpUrl, detail);
  }

  const transport = detectOpenCodeTransport(entry);
  if (isObject(entry) && entry["enabled"] === false) {
    return makeOpenCodeStatus(
      path,
      "disabled",
      transport,
      flags.opencodeMcpUrl,
      "mcp.agent-inspector is present but disabled",
    );
  }

  if (isKnownOpenCodeEntry(entry, flags.opencodeMcpUrl, false)) {
    return makeOpenCodeStatus(
      path,
      "configured",
      transport,
      flags.opencodeMcpUrl,
      "mcp.agent-inspector points at Agent Inspector MCP",
    );
  }

  return makeOpenCodeStatus(
    path,
    "custom",
    transport,
    flags.opencodeMcpUrl,
    "mcp.agent-inspector exists but does not match Agent Inspector defaults",
  );
}

function buildOpenCodePlan(flags: OnboardFlags): OpenCodeConfigPlan {
  const path = resolveOpenCodeConfigPath(flags);
  const read = readOpenCodeConfig(path);
  if (!read.ok) {
    return {
      status: makeOpenCodeStatus(path, "invalid", "unknown", flags.opencodeMcpUrl, read.message),
      body: null,
      shouldWrite: false,
      shouldRemove: false,
      blockedMessage: `OpenCode config is invalid: ${read.message}`,
    };
  }

  const status = readOpenCodeStatus(flags);
  const entry = getOpenCodeEntry(read.config);
  const nextBody = formatOpenCodeConfig(
    buildOpenCodeConfig(read.config, flags.opencodeTransport, flags.opencodeMcpUrl),
  );

  if (flags.uninstall) {
    const shouldRemove =
      entry !== undefined && isKnownOpenCodeEntry(entry, flags.opencodeMcpUrl, true);
    const removeBody = shouldRemove ? formatOpenCodeConfig(removeOpenCodeEntry(read.config)) : null;
    return {
      status,
      body: removeBody,
      shouldWrite: false,
      shouldRemove,
      blockedMessage: shouldRemove
        ? null
        : "OpenCode mcp.agent-inspector is missing or custom; preserved",
    };
  }

  switch (status.state) {
    case "configured":
      return {
        status,
        body: nextBody,
        shouldWrite: flags.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: flags.force,
        shouldRemove: false,
        blockedMessage: flags.force
          ? null
          : "OpenCode mcp.agent-inspector is custom or disabled; use --force to replace",
      };
    case "invalid":
      return {
        status,
        body: null,
        shouldWrite: false,
        shouldRemove: false,
        blockedMessage: "OpenCode config is invalid",
      };
    default:
      return {
        status,
        body: null,
        shouldWrite: false,
        shouldRemove: false,
        blockedMessage: "OpenCode 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 && !flags.opencodeOnly;
  const installCodex = !flags.skipCodexSkill && !flags.opencodeOnly;
  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,
  openCodePlan: OpenCodeConfigPlan | null,
): 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`);
    }
    if (openCodePlan !== null) {
      const status = openCodePlan.shouldRemove ? "matching" : "skipped";
      process.stdout.write(
        `${openCodePlan.status.label}: ${openCodePlan.status.path} (${status})\n`,
      );
      if (openCodePlan.blockedMessage !== null) {
        process.stdout.write(`  ${openCodePlan.blockedMessage}\n`);
      }
    }
    process.stdout.write(`\nNo files were removed.\n`);
    return 0;
  }

  const removeOpenCode = openCodePlan !== null && openCodePlan.shouldRemove;
  if (filesToRemove.length === 0 && !removeOpenCode) {
    process.stdout.write(
      "agent-inspector onboard: no generated onboarding files match this package version\n",
    );
    if (openCodePlan !== null && openCodePlan.blockedMessage !== null) {
      process.stdout.write(`${openCodePlan.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`);
    }
    if (openCodePlan !== null && openCodePlan.shouldRemove && openCodePlan.body !== null) {
      mkdirSync(dirname(openCodePlan.status.path), { recursive: true });
      writeFileSync(openCodePlan.status.path, openCodePlan.body, "utf8");
      process.stdout.write(`Removed OpenCode MCP entry from: ${openCodePlan.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[],
  openCodeStatus: OpenCodeConfigStatus | null,
): string {
  const actionable = statuses.filter(
    (status) => status.state === "missing" || status.state === "outdated",
  );
  const actions = actionable.map((status) => status.action);
  if (openCodeStatus !== null && openCodeStatus.state === "missing") {
    actions.push(openCodeStatus.action);
  }
  if (actions.length > 0) {
    const uniqueActions = Array.from(new Set(actions));
    return `Run: ${uniqueActions.join(" && ")}`;
  }
  if (
    openCodeStatus !== null &&
    (openCodeStatus.state === "custom" ||
      openCodeStatus.state === "disabled" ||
      openCodeStatus.state === "invalid")
  ) {
    return `OpenCode config needs review: ${openCodeStatus.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,
  openCodeStatus: OpenCodeConfigStatus | null,
): number {
  const enabledFiles = plannedFiles.filter((file) => file.enabled);
  const statuses = enabledFiles.map((file) => readGeneratedFileStatus(file, currentVersion));
  const summary = summarizeStatusAction(statuses, openCodeStatus);

  if (json) {
    process.stdout.write(
      `${JSON.stringify(
        {
          packageVersion: currentVersion,
          files: statuses,
          opencodeConfig: openCodeStatus,
          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`);
  }

  if (openCodeStatus !== null) {
    process.stdout.write(`${openCodeStatus.label}: ${openCodeStatus.state}`);
    process.stdout.write(`, transport ${openCodeStatus.transport}\n`);
    process.stdout.write(`  ${openCodeStatus.path}\n`);
    process.stdout.write(`  Endpoint: ${openCodeStatus.endpoint}\n`);
    process.stdout.write(`  Detail: ${openCodeStatus.detail}\n`);
    process.stdout.write(`  Action: ${openCodeStatus.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 openCodePlan = isOpenCodeEnabled(flags) ? buildOpenCodePlan(flags) : null;
  const openCodeStatus = openCodePlan === null ? null : openCodePlan.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 && openCodePlan === null) {
    process.stderr.write("agent-inspector onboard: no onboarding target selected\n");
    return 2;
  }

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

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

  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`);
    }
    if (openCodePlan !== null) {
      process.stdout.write(
        `${openCodePlan.status.label}: ${openCodePlan.status.path} (${openCodePlan.status.state})\n`,
      );
      process.stdout.write(`  Transport: ${flags.opencodeTransport}\n`);
      process.stdout.write(`  Endpoint: ${openCodePlan.status.endpoint}\n`);
      process.stdout.write(`  Action: ${openCodePlan.status.action}\n`);
      if (openCodePlan.blockedMessage !== null) {
        process.stdout.write(`  ${openCodePlan.blockedMessage}\n`);
      }
    }
    process.stdout.write(`\nClaude skill headings:\n`);
    if (!flags.codexOnly && !flags.opencodeOnly) {
      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 && !flags.opencodeOnly) {
      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 && !flags.opencodeOnly) {
      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 shouldWriteOpenCode =
    openCodePlan !== null && openCodePlan.shouldWrite && openCodePlan.body !== null;
  if (openCodePlan !== null && openCodePlan.status.state === "invalid") {
    process.stderr.write(`agent-inspector onboard: ${openCodePlan.blockedMessage ?? "invalid"}\n`);
    return 1;
  }
  if (filesToWrite.length === 0 && !shouldWriteOpenCode) {
    process.stdout.write(
      "agent-inspector onboard: all selected onboarding files are already up to date\n",
    );
    if (openCodePlan !== null && openCodePlan.blockedMessage !== null) {
      process.stdout.write(`${openCodePlan.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");
    }
    if (shouldWriteOpenCode && openCodePlan !== null && openCodePlan.body !== null) {
      mkdirSync(dirname(openCodePlan.status.path), { recursive: true });
      writeFileSync(openCodePlan.status.path, openCodePlan.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`);
  }
  if (shouldWriteOpenCode && openCodePlan !== null) {
    process.stdout.write(`Installed/updated OpenCode MCP config: ${openCodePlan.status.path}\n`);
  }
  process.stdout.write(`\nNext steps:\n`);
  if (!flags.codexOnly && !flags.opencodeOnly) {
    process.stdout.write(`  - Open Claude Code and run: /agent-inspector:onboard\n`);
  }
  if (!flags.skipCodexSkill && !flags.opencodeOnly) {
    process.stdout.write(`  - Open Codex and ask to use the agent-inspector-onboard skill\n`);
  }
  if (openCodePlan !== null) {
    process.stdout.write(`  - Verify OpenCode MCP: opencode mcp list\n`);
  }
  if (flags.opencodeOnly) {
    process.stdout.write(
      `  - Refresh OpenCode later: agent-inspector onboard --opencode-only --force\n`,
    );
  } else {
    process.stdout.write(`  - Refresh later: agent-inspector onboard --force\n`);
    process.stdout.write(`  - Detected primary tool: ${toolHint}\n`);
  }
  return 0;
}
