/**
 * Tool detection for the onboard subcommand (and the SKILL.md instructions
 * that read this code path). Pure functions only — no side effects, no fs
 * watchers. Each detector returns a discriminated union so callers can tell
 * a "found" from a "not found" without throwing.
 *
 * Detection is heuristic and best-effort. A miss is never an error — the
 * user might have a tool installed in a non-standard location, or might be
 * using a tool we don't enumerate. The skill falls through to a generic
 * `curl` example in that case.
 */

import { execFileSync } from "node:child_process";
import { existsSync } from "node:fs";
import { join } from "node:path";
import { homedir } from "node:os";

export type DetectedTool = {
  readonly found: true;
  /** Absolute path to the most distinctive artifact (config dir or binary). */
  readonly path: string;
  /** Best-effort version string, if we can cheaply read one. */
  readonly versionHint?: string;
};

export type MissingTool = { readonly found: false };

export type ToolDetectionResult = DetectedTool | MissingTool;

export type SupportedToolId = "claude-code" | "opencode" | "mimo" | "cursor" | "cody";

export type DetectedToolEntry = {
  readonly id: SupportedToolId;
  readonly displayName: string;
  readonly result: ToolDetectionResult;
};

/**
 * Run `command -v <bin>` (bash) or `where <bin>` (cmd) and return the first
 * line of stdout. Returns `null` if the binary isn't on PATH. We use the
 * platform's own lookup so we don't have to maintain a hard-coded list of
 * candidate install dirs.
 */
function which(bin: string): string | null {
  try {
    if (process.platform === "win32") {
      const out = execFileSync("where", [bin], {
        encoding: "utf8",
        timeout: 3000,
        stdio: ["ignore", "pipe", "ignore"],
      });
      const first = out.split(/\r?\n/).find((line) => line.trim().length > 0);
      return first?.trim() ?? null;
    }
    const out = execFileSync("sh", ["-c", `command -v ${bin}`], {
      encoding: "utf8",
      timeout: 3000,
      stdio: ["ignore", "pipe", "ignore"],
    });
    return out.trim() || null;
  } catch {
    return null;
  }
}

function tryDir(path: string): string | null {
  return existsSync(path) ? path : null;
}

function detectClaudeCode(): ToolDetectionResult {
  // Claude Code stores its config under ~/.claude/. The binary is `claude`.
  const configDir = tryDir(join(homedir(), ".claude"));
  const bin = which("claude");
  if (configDir === null && bin === null) return { found: false };
  return { found: true, path: configDir ?? bin ?? "" };
}

function detectOpenCode(): ToolDetectionResult {
  const configDir = tryDir(join(homedir(), ".config", "opencode"));
  const bin = which("opencode");
  if (configDir === null && bin === null) return { found: false };
  return { found: true, path: configDir ?? bin ?? "" };
}

function detectMiMo(): ToolDetectionResult {
  const configDir = tryDir(join(homedir(), ".mimo"));
  const bin = which("mimo");
  if (configDir === null && bin === null) return { found: false };
  return { found: true, path: configDir ?? bin ?? "" };
}

function detectCursor(): ToolDetectionResult {
  const configDir = tryDir(join(homedir(), ".cursor"));
  const bin = which("cursor");
  if (configDir === null && bin === null) return { found: false };
  return { found: true, path: configDir ?? bin ?? "" };
}

function detectCody(): ToolDetectionResult {
  // Sourcegraph's Cody uses `~/.config/cody/` (Linux/macOS) and stores
  // its binary on PATH as `cody`. Note: `~/.codex/` is OpenAI Codex CLI's
  // config dir, *not* Cody's — keep these distinct.
  const configDir = tryDir(join(homedir(), ".config", "cody"));
  const bin = which("cody");
  if (configDir === null && bin === null) return { found: false };
  return { found: true, path: configDir ?? bin ?? "" };
}

const DETECTORS: ReadonlyArray<{
  id: SupportedToolId;
  displayName: string;
  detect: () => ToolDetectionResult;
}> = [
  { id: "claude-code", displayName: "Claude Code", detect: detectClaudeCode },
  { id: "opencode", displayName: "OpenCode", detect: detectOpenCode },
  { id: "mimo", displayName: "MiMo Code", detect: detectMiMo },
  { id: "cursor", displayName: "Cursor", detect: detectCursor },
  { id: "cody", displayName: "Cody", detect: detectCody },
];

/**
 * Run every detector and return the full list, including misses. Useful for
 * the SKILL.md instructions that want to give the user a summary of what's
 * available ("detected: Claude Code, OpenCode; not detected: Cursor, Cody").
 */
export function detectAll(): ReadonlyArray<DetectedToolEntry> {
  return DETECTORS.map(({ id, displayName, detect }) => ({
    id,
    displayName,
    result: detect(),
  }));
}

/**
 * Return the first tool that the detector finds. Used by the onboard
 * subcommand when it needs to pick a single "wiring example" to print.
 * Order in `DETECTORS` is the priority order — Claude Code first because
 * it's the namesake of the project.
 */
export function detectFirst(): DetectedToolEntry | null {
  for (const entry of DETECTORS) {
    const result = entry.detect();
    if (result.found) return { id: entry.id, displayName: entry.displayName, result };
  }
  return null;
}
