import { spawn } from "node:child_process";
import { constants, existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync, accessSync } from "node:fs";
import { dirname, isAbsolute, join } from "node:path";
import { createRequire } from "node:module";

import { app } from "electron";
import { buildClaudeMcpGetCommand, buildClaudeMcpPreview, classifyClaudeMcpStatus, createOpenPetsHookSettingsPreview, doctorClaudeHooks, installClaudeHooks, mapAsarPathToUnpacked, uninstallClaudeHooks, type ClaudeCommandSpec, type ClaudeHookDoctorResult, type ClaudeMcpPreview, type OpenPetsCommandMode, type ParsedClaudeMcpEntry } from "@open-pets/claude";
import { buildCursorRulesPreview, classifyCursorMcpStatus, executeCursorMcpWrite, getCursorGlobalMcpPath, planCursorMcpInstall, planCursorMcpRemove, planCursorMcpReplace, readCursorMcpConfig, type CursorMcpStatusResult } from "@open-pets/cursor";
import { buildOpenPetsOnlyPreview, type RedactedPreview } from "@open-pets/cursor";
import { doctorOpenCodeGlobalSetup, getGlobalOpenCodeConfigDir, parseOpenCodeConfig, prepareOpenCodeGlobalRemove, prepareOpenCodeGlobalSetup, writePreparedOpenCodeGlobalRemove, writePreparedOpenCodeGlobalSetup } from "@open-pets/opencode";

import { getAppStateSnapshot, updatePreferences, type InstalledPetState, type OpenPetsStateV1 } from "./app-state.js";
import { doctorClaudeOpenPetsMemory, installClaudeOpenPetsMemory, uninstallClaudeOpenPetsMemory, type ClaudeOpenPetsMemoryStatus } from "./claude-memory.js";

export type AgentSetupAction = "configure" | "replace" | "remove" | "install-memory" | "doctor-hooks" | "install-hooks" | "uninstall-hooks" | "opencode-install" | "opencode-remove" | "cursor-install" | "cursor-replace" | "cursor-remove";
export type JournalAction = "configure" | "update" | "replace" | "remove";

export interface AgentSetupPetOption {
  readonly id: string;
  readonly displayName: string;
  readonly default: boolean;
}

export interface ClaudeCodeStatus {
  readonly state: "detected" | "not_detected" | "configured" | "needs_setup" | "error";
  readonly label: string;
  readonly details: string;
  readonly claudeCommand?: string;
  readonly version?: string;
  readonly mcpListWorks: boolean;
  readonly openPetsEntry: ParsedClaudeMcpEntry;
  readonly canConfigure: boolean;
  readonly canReplace: boolean;
  readonly canRemove: boolean;
}

export interface AgentSetupSnapshot {
  readonly selectedPetId?: string;
  readonly commandMode: OpenPetsCommandMode;
  readonly localDevAvailable: boolean;
  readonly petOptions: readonly AgentSetupPetOption[];
  readonly preview: ClaudeMcpPreview;
  readonly status: ClaudeCodeStatus;
  readonly hookStatus: ClaudeHookDoctorResult;
  readonly memoryStatus: ClaudeOpenPetsMemoryStatus;
  readonly opencodeStatus: OpenCodeSetupStatus;
  readonly opencodePreview: OpenCodeSetupPreview;
  readonly cursorStatus: CursorSetupStatus;
  readonly cursorPreview: CursorSetupPreview;
  readonly commandPaths: AgentSetupCommandPaths;
  readonly busy: boolean;
  readonly lastAction?: AgentSetupActionResult;
}

export interface AgentSetupCommandPaths {
  readonly claude: string;
  readonly node: string;
  readonly opencode: string;
}

export interface OpenCodeSetupStatus {
  readonly state: "configured" | "needs_setup" | "not_detected" | "error";
  readonly label: string;
  readonly details: string;
  readonly configDir: string;
  readonly canInstall: boolean;
  readonly canRemove: boolean;
}

export interface OpenCodeSetupPreview {
  readonly global: true;
  readonly configDir: string;
  readonly configPath: string;
  readonly cleanupConfigPaths: readonly string[];
  readonly mcpCommand: readonly string[];
  readonly plugin: readonly unknown[] | string;
  readonly instructionPath: string;
  readonly configPreview: Record<string, unknown>;
}

export interface CursorSetupStatus {
  readonly state: "configured" | "needs_setup" | "not_detected" | "error" | "conflict" | "needs_update";
  readonly label: string;
  readonly details: string;
  readonly configPath: string;
  readonly canInstall: boolean;
  readonly canReplace: boolean;
  readonly canRemove: boolean;
}

export interface CursorSetupPreview {
  readonly global: true;
  readonly configPath: string;
  readonly mcpEntry: RedactedPreview;
  readonly rulesPath: string;
  readonly rulesContent: string;
  readonly commandMode: "published" | "local" | "bundled";
}

export interface AgentSetupActionResult {
  readonly ok: boolean;
  readonly action: AgentSetupAction;
  readonly message: string;
  readonly changed: boolean;
}

export interface AgentSetupJournalEntry {
  readonly timestamp: string;
  readonly action: JournalAction;
  readonly selectedPetId?: string;
  readonly command: readonly string[];
  readonly previousStatus: string;
  readonly success: boolean;
  readonly message: string;
}

interface CommandResult {
  readonly ok: boolean;
  readonly timedOut: boolean;
  readonly exitCode: number | null;
  readonly stdout: string;
  readonly stderr: string;
  readonly error?: string;
}

const commandTimeoutMs = 6_000;
const maxOutputBytes = 16_384;
const require = createRequire(import.meta.url);
let operationRunning = false;
let lastAction: AgentSetupActionResult | undefined;

export async function getAgentSetupSnapshot(selectedPetId?: unknown, commandModeInput?: unknown): Promise<AgentSetupSnapshot> {
  const petId = validateSelectedPetId(selectedPetId);
  const commandMode = validateCommandMode(commandModeInput);
  const preview = safeBuildClaudeMcpPreview(petId, commandMode);
  const status = preview.error ? createBundledResourceErrorStatus(preview.error) : await detectClaudeCodeStatus(petId, commandMode);
  const rawHookStatus = preview.error ? createHookErrorStatus(preview.error) : safeDoctorClaudeHooks(commandMode, petId);
  const hookStatus = { ...rawHookStatus, settingsPath: formatUserPath(rawHookStatus.settingsPath) ?? rawHookStatus.settingsPath, backupPath: formatUserPath(rawHookStatus.backupPath) };
  const rawMemoryStatus = doctorClaudeOpenPetsMemory(app.getPath("home"));
  const memoryStatus = { ...rawMemoryStatus, claudeMdPath: formatUserPath(rawMemoryStatus.claudeMdPath) ?? rawMemoryStatus.claudeMdPath, openPetsMemoryPath: formatUserPath(rawMemoryStatus.openPetsMemoryPath) ?? rawMemoryStatus.openPetsMemoryPath };
  const opencode = await getOpenCodeSetup(commandMode, petId);
  const cursor = await getCursorSetup(commandMode, petId);

  return {
    selectedPetId: petId,
    commandMode,
    localDevAvailable: !app.isPackaged,
    petOptions: getPetOptions(),
    preview: preview.preview,
    status,
    hookStatus,
    memoryStatus,
    opencodeStatus: opencode.status,
    opencodePreview: opencode.preview,
    cursorStatus: cursor.status,
    cursorPreview: cursor.preview,
    commandPaths: getAgentSetupCommandPaths(),
    busy: operationRunning,
    lastAction,
  };
}

export function updateAgentSetupCommandPaths(patch: unknown): AgentSetupCommandPaths {
  if (!isRecord(patch)) throw new Error("Invalid command path settings.");
  for (const key of Object.keys(patch)) {
    if (key !== "claude" && key !== "node" && key !== "opencode") throw new Error("Invalid command path setting.");
  }
  const updates: Writable<Partial<OpenPetsStateV1["preferences"]>> = {};
  if ("claude" in patch) updates.claudeCommandPath = normalizeOptionalCommandPath(patch.claude, "Claude");
  if ("node" in patch) updates.nodeCommandPath = normalizeOptionalCommandPath(patch.node, "Node.js");
  if ("opencode" in patch) updates.opencodeCommandPath = normalizeOptionalCommandPath(patch.opencode, "OpenCode");
  updatePreferences(updates);
  return getAgentSetupCommandPaths();
}

type Writable<T> = { -readonly [K in keyof T]: T[K] };

export async function runAgentSetupAction(action: AgentSetupAction, selectedPetId?: unknown, commandModeInput?: unknown): Promise<AgentSetupSnapshot> {
  if (operationRunning) throw new Error("Another Claude setup operation is already running.");
  const petId = validateSelectedPetId(selectedPetId);
  const commandMode = validateCommandMode(commandModeInput);
  operationRunning = true;

  try {
    lastAction = await runAction(action, petId, commandMode);
    operationRunning = false;
    return getAgentSetupSnapshot(petId, commandMode);
  } finally {
    operationRunning = false;
  }
}

export function sanitizeAgentSetupOutput(value: string): string {
  const home = app.isReady() ? app.getPath("home") : "";
  return value
    .replaceAll(home, "~")
    .replace(/(?:[A-Za-z]:)?[\\/][^\s"']{2,}/g, "<path>")
    .replace(/-----BEGIN [^-]+PRIVATE KEY-----[\s\S]*?-----END [^-]+PRIVATE KEY-----/gi, "<redacted-private-key>")
    .replace(/bearer\s+[A-Za-z0-9._~+\/-]+=*/gi, "Bearer <redacted>")
    .replace(/\b(api[_-]?key|secret|password|token)\s*[:=]\s*\S+/gi, "$1=<redacted>")
    .slice(0, 500);
}

function safeBuildClaudeMcpPreview(selectedPetId: string | undefined, commandMode: OpenPetsCommandMode): { readonly preview: ClaudeMcpPreview; readonly error?: string } {
  try {
    return { preview: withPreferredClaudeCommand(buildClaudeMcpPreview(selectedPetId, commandMode, getPreferredNodeCommand())) };
  } catch (error) {
    const message = error instanceof Error ? error.message : "Packaged OpenPets command resources are unavailable.";
    return { preview: createErrorPreview(commandMode, message), error: message };
  }
}

function safeDoctorClaudeHooks(commandMode: OpenPetsCommandMode, selectedPetId: string | undefined): ClaudeHookDoctorResult {
  try {
    return doctorClaudeHooks(undefined, commandMode, selectedPetId, getPreferredNodeCommand());
  } catch (error) {
    return createHookErrorStatus(error instanceof Error ? error.message : "Packaged OpenPets hook resources are unavailable.");
  }
}

function createErrorPreview(commandMode: OpenPetsCommandMode, message: string): ClaudeMcpPreview {
  const claude = getPreferredClaudeCommand();
  return {
    commandMode,
    add: { command: claude, args: [] },
    remove: { command: claude, args: ["mcp", "remove", "--scope", "user", "openpets"] },
    mcpJson: { mcpServers: { openpets: { type: "stdio", command: "node", args: [] } } },
    displayCommand: message,
  };
}

function withPreferredClaudeCommand(preview: ClaudeMcpPreview): ClaudeMcpPreview {
  const claude = getPreferredClaudeCommand();
  if (claude === preview.add.command && claude === preview.remove.command) return preview;
  return {
    ...preview,
    add: { ...preview.add, command: claude },
    remove: { ...preview.remove, command: claude },
    displayCommand: preview.displayCommand.replace(/^claude(?=\s|$)/, quoteCommandForDisplay(claude)),
  };
}

function createBundledResourceErrorStatus(message: string): ClaudeCodeStatus {
  return createStatus("error", "Packaged commands unavailable", message, undefined, { ok: false, timedOut: false, exitCode: null, stdout: "", stderr: "", error: message }, { present: false, source: "none", verified: false, matchesExpected: false });
}

function createHookErrorStatus(message: string): ClaudeHookDoctorResult {
  return { status: "error", settingsPath: "~/.claude/settings.json", exists: false, valid: false, message, preview: {}, asyncSupported: false };
}

async function runAction(action: AgentSetupAction, selectedPetId: string | undefined, commandMode: OpenPetsCommandMode): Promise<AgentSetupActionResult> {
  if (action === "opencode-install") return installOpenCodeGlobal(selectedPetId, commandMode);
  if (action === "opencode-remove") return removeOpenCodeGlobal();
  if (action === "cursor-install") return installCursorGlobal(selectedPetId, commandMode);
  if (action === "cursor-replace") return replaceCursorGlobal(selectedPetId, commandMode);
  if (action === "cursor-remove") return removeCursorGlobal();
  if (action === "doctor-hooks") {
    const doctor = safeDoctorClaudeHooks(commandMode, selectedPetId);
    writeActionJournal({ action: "update", selectedPetId, command: createHookJournalCommand("doctor-hooks", selectedPetId), previousStatus: doctor.status, success: doctor.status !== "error", message: doctor.message });
    return { ok: doctor.status !== "error", action, message: doctor.message, changed: false };
  }
  if (action === "uninstall-hooks") {
    let result;
    try {
      result = uninstallClaudeHooks(undefined, commandMode);
    } catch (error) {
      return { ok: false, action, message: error instanceof Error ? error.message : "OpenPets hook uninstall failed.", changed: false };
    }
    const message = result.changed ? `Uninstalled OpenPets Claude hooks. Backup: ${formatUserPath(result.backupPath) ?? "not needed"}` : result.message;
    writeActionJournal({ action: "remove", selectedPetId, command: ["open-pets-claude", "uninstall-hooks"], previousStatus: result.status, success: result.status !== "error", message });
    return { ok: result.status !== "error", action, message, changed: result.changed };
  }
  if (action === "install-memory") {
    const result = safeInstallClaudeMemory();
    return { ok: result.ok, action, message: result.ok ? result.message : `Claude instructions were not updated: ${result.message}`, changed: result.ok && result.message.startsWith("Added") };
  }
  if (action === "remove") {
    return runRemove(createErrorPreview(commandMode, ""), selectedPetId, "Unknown", action);
  }
  if (commandMode === "bundled") {
    const node = await runCommand({ command: getPreferredNodeCommand(), args: ["--version"] });
    if (!node.ok) return { ok: false, action, message: `Node.js is required for packaged OpenPets commands. Open Claude configuration, set the Node.js command path, then try again. ${summarizeCommandResult(node)}`, changed: false };
  }
  const previewResult = safeBuildClaudeMcpPreview(selectedPetId, commandMode);
  if (previewResult.error) return { ok: false, action, message: previewResult.error, changed: false };

  if (action === "install-hooks") {
    let result;
    try {
      result = installClaudeHooks(undefined, commandMode, selectedPetId, getPreferredNodeCommand());
    } catch (error) {
      return { ok: false, action, message: error instanceof Error ? error.message : "OpenPets hook install failed.", changed: false };
    }
    const message = result.changed ? `Installed OpenPets Claude hooks. Backup: ${formatUserPath(result.backupPath) ?? "not needed"}` : result.message;
    writeActionJournal({ action: "update", selectedPetId, command: createHookJournalCommand("install-hooks", selectedPetId), previousStatus: result.status, success: result.status !== "error", message });
    return { ok: result.status !== "error", action, message, changed: result.changed };
  }
  const detection = await detectClaudeCodeStatus(selectedPetId, commandMode);
  const previousStatus = detection.label;
  const preview = previewResult.preview;

  if (detection.state === "not_detected") {
    const result = { ok: false, action, message: "Claude Code was not found. Install Claude Code or use Copy command to configure manually.", changed: false };
    writeActionJournal({ action: journalActionFor(action), selectedPetId, command: [preview.add.command, ...preview.add.args], previousStatus, success: false, message: result.message });
    return result;
  }

  if (action === "configure") {
    if (detection.openPetsEntry.present && detection.openPetsEntry.verified && detection.openPetsEntry.matchesExpected) {
      const memoryResult = safeInstallClaudeMemory();
      const message = `OpenPets MCP is already configured for Claude Code.${memoryResult.ok ? ` ${memoryResult.message}` : ` Claude instructions were not updated: ${memoryResult.message}`}`;
      return { ok: true, action, message, changed: memoryResult.ok && memoryResult.message.startsWith("Added") };
    }
    if (detection.openPetsEntry.present) {
      return { ok: false, action, message: "Claude already has an openpets MCP entry. OpenPets will keep it as installed; use Replace only if you want to recreate it with the recommended command.", changed: false };
    }
    return runAdd(preview, selectedPetId, previousStatus, action);
  }

  if (!detection.openPetsEntry.present) {
    return runAdd(preview, selectedPetId, previousStatus, action);
  }

  const removed = await runRemove(preview, selectedPetId, previousStatus, action);
  if (!removed.ok) return removed;
  const added = await runAdd(preview, selectedPetId, previousStatus, action);
  if (!added.ok) {
    return {
      ok: false,
      action,
      message: `${added.message} The previous openpets entry was removed; use this command to restore the intended entry: ${preview.displayCommand}`,
      changed: true,
    };
  }
  return { ok: true, action, message: `Replaced Claude Code OpenPets MCP entry.${summarizeMemoryMessages(removed.message, added.message)}`, changed: true };
}

async function getOpenCodeSetup(commandMode: OpenPetsCommandMode, selectedPetId: string | undefined): Promise<{ readonly status: OpenCodeSetupStatus; readonly preview: OpenCodeSetupPreview }> {
  const configDir = getGlobalOpenCodeConfigDir(process.env, app.getPath("home"), process.platform);
  const petId = selectedPetId || undefined;
  const cliVersion = getCliPackageVersion();
  const pluginVersion = getOpenCodePackageVersion();
  const cliEntryPath = commandMode === "published" ? undefined : getDesktopCliEntryPath(commandMode);
  const prepared = safePrepareOpenCode(configDir, petId, cliVersion, pluginVersion, commandMode, cliEntryPath);
  const detected = await runCommand({ command: getPreferredOpenCodeCommand(), args: ["--version"] });
  const globalState = doctorOpenCodeGlobalSetup(configDir);
  const configured = globalState.status === "installed";
  return {
    status: {
      state: globalState.status === "error" || globalState.status === "custom" || globalState.status === "conflict" ? "error" : configured ? "configured" : detected.ok ? "needs_setup" : "not_detected",
      label: configured ? "Installed" : globalState.status === "custom" || globalState.status === "conflict" ? "Needs attention" : detected.ok ? "Ready" : "Not detected",
      details: globalState.status === "custom" || globalState.status === "conflict" || globalState.status === "error" ? globalState.message : configured ? globalState.message : detected.ok ? "OpenCode was detected. Desktop setup writes global OpenCode config." : getPreferredOpenCodeCommand() === (process.platform === "win32" ? "opencode.cmd" : "opencode") ? "OpenCode was not found on PATH. You can still preview setup, but OpenCode must be installed to use it." : "OpenCode did not run from the saved command path. You can still preview setup, but OpenCode must be installed to use it.",
      configDir: formatUserPath(configDir) ?? configDir,
      canInstall: prepared.ok && !configured,
      canRemove: configured,
    },
    preview: {
      global: true,
      configDir: formatUserPath(configDir) ?? configDir,
      configPath: prepared.ok ? (formatUserPath(prepared.configPath) ?? prepared.configPath) : "",
      cleanupConfigPaths: prepared.ok ? prepared.cleanupConfigPaths.map((path) => formatUserPath(path) ?? path) : [],
      mcpCommand: prepared.ok ? prepared.command : [],
      plugin: prepared.ok ? prepared.plugin : (petId ? [`@open-pets/opencode@${pluginVersion}`, { pet: petId }] : `@open-pets/opencode@${pluginVersion}`),
      instructionPath: prepared.ok ? (formatUserPath(prepared.instructionPath) ?? prepared.instructionPath) : "",
      configPreview: prepared.ok ? prepared.configPreview : {},
    },
  };
}

async function getCursorSetup(commandMode: OpenPetsCommandMode, selectedPetId: string | undefined): Promise<{ readonly status: CursorSetupStatus; readonly preview: CursorSetupPreview }> {
  const homeDir = app.getPath("home");
  const configPath = getCursorGlobalMcpPath(homeDir);
  const petId = selectedPetId || undefined;
  const mcpVersion = getMcpPackageVersion();

  const configResult = readCursorMcpConfig(configPath);
  const statusResult = classifyCursorMcpStatus(configResult, configPath, { mcpVersion, petId, commandMode: "published" });

  const state = mapCursorStatusToState(statusResult.status);
  const label = mapCursorStatusToLabel(statusResult.status);
  const details = statusResult.message;

  return {
    status: {
      state,
      label,
      details,
      configPath: formatUserPath(configPath) ?? configPath,
      canInstall: statusResult.canInstall,
      canReplace: statusResult.canReplace,
      canRemove: statusResult.canRemove,
    },
    preview: {
      global: true,
      configPath: formatUserPath(configPath) ?? configPath,
      mcpEntry: buildOpenPetsOnlyPreview({ mcpVersion, petId, commandMode: "published" }),
      rulesPath: ".cursor/rules/openpets.mdc",
      rulesContent: buildCursorRulesPreview(),
      commandMode: "published",
    },
  };
}

function mapCursorStatusToState(status: CursorMcpStatusResult["status"]): CursorSetupStatus["state"] {
  switch (status) {
    case "installed":
      return "configured";
    case "missing":
      return "needs_setup";
    case "needs-update":
      return "needs_update";
    case "conflict":
      return "conflict";
    case "invalid":
    case "error":
      return "error";
    default:
      return "error";
  }
}

function mapCursorStatusToLabel(status: CursorMcpStatusResult["status"]): string {
  switch (status) {
    case "installed":
      return "Configured";
    case "missing":
      return "Not configured";
    case "needs-update":
      return "Needs update";
    case "conflict":
      return "Conflict";
    case "invalid":
    case "error":
      return "Config error";
    default:
      return "Checking";
  }
}

function getAgentSetupCommandPaths(): AgentSetupCommandPaths {
  const preferences = getAppStateSnapshot().preferences;
  return {
    claude: preferences.claudeCommandPath ?? "",
    node: preferences.nodeCommandPath ?? "",
    opencode: preferences.opencodeCommandPath ?? "",
  };
}

function getPreferredClaudeCommand(): string {
  return getAppStateSnapshot().preferences.claudeCommandPath || "claude";
}

function getPreferredNodeCommand(): string {
  return getAppStateSnapshot().preferences.nodeCommandPath || "node";
}

function getPreferredOpenCodeCommand(): string {
  return getAppStateSnapshot().preferences.opencodeCommandPath || (process.platform === "win32" ? "opencode.cmd" : "opencode");
}

function normalizeOptionalCommandPath(value: unknown, label: string): string | undefined {
  if (value === undefined || value === null) return undefined;
  if (typeof value !== "string") throw new Error(`${label} command path must be text.`);
  const trimmed = value.trim();
  if (!trimmed) return undefined;
  if (trimmed.length > 4096 || /[\r\n\0]/.test(trimmed)) throw new Error(`${label} command path is invalid.`);
  if (!isAbsolute(trimmed)) throw new Error(`${label} command path must be a full absolute path.`);
  if (process.platform === "win32" && /[&|<>^%!]/.test(trimmed)) throw new Error(`${label} command path contains unsupported shell characters.`);
  try {
    const stat = statSync(trimmed);
    if (!stat.isFile()) throw new Error();
    if (process.platform !== "win32") accessSync(trimmed, constants.X_OK);
  } catch {
    throw new Error(`${label} command path must point to an existing executable file.`);
  }
  return trimmed;
}

function quoteCommandForDisplay(command: string): string {
  return /\s/.test(command) ? JSON.stringify(command) : command;
}

function safePrepareOpenCode(configDir: string, selectedPetId: string | undefined, cliVersion: string, pluginVersion: string, commandMode: OpenPetsCommandMode, cliEntryPath: string | undefined): { readonly ok: true; readonly command: readonly string[]; readonly configPath: string; readonly cleanupConfigPaths: readonly string[]; readonly instructionPath: string; readonly plugin: readonly unknown[] | string; readonly configPreview: Record<string, unknown> } | { readonly ok: false; readonly message: string } {
  try {
    const prepared = prepareOpenCodeGlobalSetup({ configDir, petId: selectedPetId || undefined, cliVersion, pluginVersion, commandMode, cliEntryPath });
    const parsed = parseOpenCodeConfig(prepared.configWrite.content);
    if (!parsed.ok) return { ok: false, message: parsed.message };
    const config = parsed.value as { mcp?: { openpets?: { command?: readonly string[] } }; plugin?: readonly unknown[] };
    const plugin = Array.isArray(config.plugin) ? config.plugin[config.plugin.length - 1] : undefined;
    return { ok: true, command: config.mcp?.openpets?.command ?? [], configPath: prepared.configPath, cleanupConfigPaths: prepared.cleanupConfigWrites.map((write) => write.targetPath), instructionPath: prepared.instructionPath, plugin: plugin === undefined ? [] : (plugin as readonly unknown[] | string), configPreview: parsed.value };
  } catch (error) {
    return { ok: false, message: error instanceof Error ? error.message : "OpenCode setup preview failed." };
  }
}

async function installOpenCodeGlobal(selectedPetId: string | undefined, commandMode: OpenPetsCommandMode): Promise<AgentSetupActionResult> {
  if (commandMode === "bundled") {
    const node = await runCommand({ command: getPreferredNodeCommand(), args: ["--version"] });
    if (!node.ok) return { ok: false, action: "opencode-install", message: `Node.js is required for packaged OpenPets commands. Open OpenCode configuration, set the Node.js command path, then try again. ${summarizeCommandResult(node)}`, changed: false };
  }
  try {
    const configDir = getGlobalOpenCodeConfigDir(process.env, app.getPath("home"), process.platform);
    const prepared = prepareOpenCodeGlobalSetup({ configDir, petId: selectedPetId || undefined, cliVersion: getCliPackageVersion(), pluginVersion: getOpenCodePackageVersion(), commandMode, cliEntryPath: commandMode === "published" ? undefined : getDesktopCliEntryPath(commandMode) });
    writePreparedOpenCodeGlobalSetup(prepared);
    return { ok: true, action: "opencode-install", message: `Installed global OpenCode OpenPets setup. Config: ${formatUserPath(prepared.configPath) ?? prepared.configPath}. Instructions: ${formatUserPath(prepared.instructionPath) ?? prepared.instructionPath}.`, changed: true };
  } catch (error) {
    return { ok: false, action: "opencode-install", message: error instanceof Error ? error.message : "OpenCode setup failed.", changed: false };
  }
}

async function removeOpenCodeGlobal(): Promise<AgentSetupActionResult> {
  try {
    const configDir = getGlobalOpenCodeConfigDir(process.env, app.getPath("home"), process.platform);
    const prepared = prepareOpenCodeGlobalRemove(configDir);
    writePreparedOpenCodeGlobalRemove(prepared);
    return { ok: true, action: "opencode-remove", message: prepared.configWrites.length > 0 ? "Removed global OpenCode OpenPets setup." : "Global OpenCode OpenPets setup was already absent.", changed: prepared.configWrites.length > 0 };
  } catch (error) {
    return { ok: false, action: "opencode-remove", message: error instanceof Error ? error.message : "OpenCode removal failed.", changed: false };
  }
}

async function installCursorGlobal(selectedPetId: string | undefined, commandMode: OpenPetsCommandMode): Promise<AgentSetupActionResult> {
  void commandMode;
  try {
    const homeDir = app.getPath("home");
    const configPath = getCursorGlobalMcpPath(homeDir);
    const mcpVersion = getMcpPackageVersion();
    const plan = planCursorMcpInstall(configPath, { mcpVersion, petId: selectedPetId || undefined, commandMode: "published" });
    if ("ok" in plan && !plan.ok) {
      return { ok: false, action: "cursor-install", message: plan.message, changed: false };
    }
    if ("targetPath" in plan) {
      executeCursorMcpWrite(plan);
      const backupMsg = plan.backupPath ? ` Backup: ${formatUserPath(plan.backupPath) ?? plan.backupPath}.` : "";
      return { ok: true, action: "cursor-install", message: `Installed Cursor OpenPets MCP config at ${formatUserPath(configPath) ?? configPath}.${backupMsg} Cursor may need to be restarted or reloaded.`, changed: true };
    }
    return { ok: false, action: "cursor-install", message: "Failed to plan Cursor MCP install.", changed: false };
  } catch (error) {
    return { ok: false, action: "cursor-install", message: error instanceof Error ? error.message : "Cursor MCP install failed.", changed: false };
  }
}

async function replaceCursorGlobal(selectedPetId: string | undefined, commandMode: OpenPetsCommandMode): Promise<AgentSetupActionResult> {
  void commandMode;
  try {
    const homeDir = app.getPath("home");
    const configPath = getCursorGlobalMcpPath(homeDir);
    const mcpVersion = getMcpPackageVersion();
    const plan = planCursorMcpReplace(configPath, { mcpVersion, petId: selectedPetId || undefined, commandMode: "published" });
    if ("ok" in plan && !plan.ok) {
      return { ok: false, action: "cursor-replace", message: plan.message, changed: false };
    }
    if ("targetPath" in plan) {
      executeCursorMcpWrite(plan);
      const backupMsg = plan.backupPath ? ` Backup: ${formatUserPath(plan.backupPath) ?? plan.backupPath}.` : "";
      return { ok: true, action: "cursor-replace", message: `Replaced Cursor OpenPets MCP config at ${formatUserPath(configPath) ?? configPath}.${backupMsg} Cursor may need to be restarted or reloaded.`, changed: true };
    }
    return { ok: false, action: "cursor-replace", message: "Failed to plan Cursor MCP replace.", changed: false };
  } catch (error) {
    return { ok: false, action: "cursor-replace", message: error instanceof Error ? error.message : "Cursor MCP replace failed.", changed: false };
  }
}

async function removeCursorGlobal(): Promise<AgentSetupActionResult> {
  try {
    const homeDir = app.getPath("home");
    const configPath = getCursorGlobalMcpPath(homeDir);
    const plan = planCursorMcpRemove(configPath);
    if ("ok" in plan && !plan.ok) {
      return { ok: false, action: "cursor-remove", message: plan.message, changed: false };
    }
    if ("targetPath" in plan) {
      executeCursorMcpWrite(plan);
      return { ok: true, action: "cursor-remove", message: `Removed Cursor OpenPets MCP config at ${formatUserPath(configPath) ?? configPath}. Cursor may need to be restarted or reloaded.`, changed: true };
    }
    return { ok: false, action: "cursor-remove", message: "Failed to plan Cursor MCP remove.", changed: false };
  } catch (error) {
    return { ok: false, action: "cursor-remove", message: error instanceof Error ? error.message : "Cursor MCP remove failed.", changed: false };
  }
}

function getDesktopCliEntryPath(commandMode: OpenPetsCommandMode): string {
  const path = require.resolve("@open-pets/cli");
  return commandMode === "bundled" ? mapAsarPathToUnpacked(path) : path;
}

function getCliPackageVersion(): string {
  return getWorkspacePackageVersion("@open-pets/cli");
}

function getOpenCodePackageVersion(): string {
  return getWorkspacePackageVersion("@open-pets/opencode");
}

function getWorkspacePackageVersion(packageName: string): string {
  try {
    const entryPath = require.resolve(packageName);
    const packageJsonPath = join(dirname(dirname(entryPath)), "package.json");
    const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8")) as { readonly version?: unknown };
    return typeof packageJson.version === "string" && packageJson.version ? packageJson.version : "0.0.0";
  } catch {
    return "0.0.0";
  }
}

function getMcpPackageVersion(): string {
  return getWorkspacePackageVersion("@open-pets/mcp");
}

function summarizeMemoryMessages(...messages: readonly string[]): string {
  const memoryMessages = messages.flatMap((message) => message.match(/Claude (?:OpenPets )?instructions[^.]*\./g) ?? []);
  return memoryMessages.length > 0 ? ` ${memoryMessages.join(" ")}` : "";
}

function createHookJournalCommand(command: "doctor-hooks" | "install-hooks", selectedPetId: string | undefined): readonly string[] {
  return selectedPetId ? ["open-pets-claude", command, "--pet", selectedPetId] : ["open-pets-claude", command];
}

async function runAdd(preview: ClaudeMcpPreview, selectedPetId: string | undefined, previousStatus: string, action: AgentSetupAction): Promise<AgentSetupActionResult> {
  const result = await runClaudeCommand(preview.add);
  const memoryResult = result.ok ? safeInstallClaudeMemory() : { ok: false as const, message: "" };
  const message = result.ok
    ? `Configured Claude Code OpenPets MCP entry.${memoryResult.ok ? ` ${memoryResult.message}` : ` Claude instructions were not updated: ${memoryResult.message}`}`
    : `Claude MCP add failed: ${summarizeCommandResult(result)}`;
  writeActionJournal({ action: journalActionFor(action), selectedPetId, command: [preview.add.command, ...preview.add.args], previousStatus, success: result.ok, message });
  return { ok: result.ok, action, message, changed: result.ok };
}

async function runRemove(preview: ClaudeMcpPreview, selectedPetId: string | undefined, previousStatus: string, action: AgentSetupAction): Promise<AgentSetupActionResult> {
  const result = await runClaudeCommand(preview.remove);
  const memoryResult = result.ok ? safeUninstallClaudeMemory() : { ok: false as const, message: "" };
  const message = result.ok
    ? `Removed Claude Code OpenPets MCP entry.${memoryResult.ok ? ` ${memoryResult.message}` : ` Claude instructions were not updated: ${memoryResult.message}`}`
    : `Claude MCP remove failed: ${summarizeCommandResult(result)}`;
  writeActionJournal({ action: journalActionFor(action), selectedPetId, command: [preview.remove.command, ...preview.remove.args], previousStatus, success: result.ok, message });
  return { ok: result.ok, action, message, changed: result.ok };
}

function safeInstallClaudeMemory(): { readonly ok: true; readonly message: string } | { readonly ok: false; readonly message: string } {
  try {
    const result = installClaudeOpenPetsMemory(app.getPath("home"));
    return { ok: true, message: result.changed ? "Added Claude OpenPets instructions." : "Claude OpenPets instructions already present." };
  } catch (error) {
    return { ok: false, message: error instanceof Error ? error.message : "Unknown error." };
  }
}

function safeUninstallClaudeMemory(): { readonly ok: true; readonly message: string } | { readonly ok: false; readonly message: string } {
  try {
    const result = uninstallClaudeOpenPetsMemory(app.getPath("home"));
    return { ok: true, message: result.changed ? "Removed Claude OpenPets instructions." : "Claude OpenPets instructions were already absent." };
  } catch (error) {
    return { ok: false, message: error instanceof Error ? error.message : "Unknown error." };
  }
}

async function detectClaudeCodeStatus(selectedPetId: string | undefined, commandMode: OpenPetsCommandMode): Promise<ClaudeCodeStatus> {
  if (commandMode === "bundled") {
    const node = await runCommand({ command: getPreferredNodeCommand(), args: ["--version"] });
    if (!node.ok) return createStatus("error", "Node required", `Node.js is required for packaged OpenPets commands. Open Claude configuration, expand Advanced detection, set the Node.js command path, then try again. ${summarizeCommandResult(node)}`, undefined, node, { present: false, source: "none", verified: false, matchesExpected: false });
  }

  const version = await runClaudeCommand({ command: "claude", args: ["--version"] });
  if (!version.ok) {
    const hasOverride = getPreferredClaudeCommand() !== "claude";
    return createStatus("not_detected", "Not detected", `${hasOverride ? "Claude Code did not run from the saved command path" : "Claude Code was not found or did not run"}: ${summarizeCommandResult(version)}`, undefined, version, { present: false, source: "none", verified: false, matchesExpected: false });
  }

  const list = await runClaudeCommandWithTimeoutRetry({ command: "claude", args: ["mcp", "list"] });
  if (!list.ok) {
    return createStatus("error", "Error / needs attention", `Claude Code was detected, but MCP status failed: ${summarizeCommandResult(list)}`, sanitizeAgentSetupOutput(version.stdout || version.stderr), list, { present: false, source: "none", verified: false, matchesExpected: false });
  }

  const listed = classifyClaudeMcpStatus(list.stdout, undefined, selectedPetId, commandMode, getPreferredNodeCommand());
  let entry = listed;
  if (listed.present) {
    const get = await runClaudeCommand(buildClaudeMcpGetCommand());
    if (get.ok) entry = classifyClaudeMcpStatus(list.stdout, get.stdout, selectedPetId, commandMode, getPreferredNodeCommand());
  }

  if (!entry.present) return createStatus("needs_setup", "Needs setup", "Claude Code is detected, but OpenPets MCP is not configured.", sanitizeAgentSetupOutput(version.stdout || version.stderr), list, entry);
  if (entry.verified && entry.matchesExpected) return createStatus("configured", "Configured", "Claude Code has the expected OpenPets MCP entry.", sanitizeAgentSetupOutput(version.stdout || version.stderr), list, entry);
  if (entry.verified) return createStatus("configured", "Installed — custom", "Claude Code has an openpets MCP entry with a custom command. OpenPets will leave it alone unless you choose Replace with recommended.", sanitizeAgentSetupOutput(version.stdout || version.stderr), list, entry);
  return createStatus("configured", "Installed — unverified", "Claude Code lists an openpets MCP entry, but command details were not available. OpenPets will leave it alone unless you choose Replace with recommended.", sanitizeAgentSetupOutput(version.stdout || version.stderr), list, entry);
}

async function runClaudeCommandWithTimeoutRetry(spec: ClaudeCommandSpec): Promise<CommandResult> {
  const first = await runClaudeCommand(spec);
  if (!first.timedOut) return first;
  await delay(250);
  const second = await runClaudeCommand(spec);
  return second.ok ? second : first;
}

function createStatus(state: ClaudeCodeStatus["state"], label: string, details: string, version: string | undefined, listResult: CommandResult, entry: ParsedClaudeMcpEntry): ClaudeCodeStatus {
  return {
    state,
    label,
    details,
    claudeCommand: "claude",
    version,
    mcpListWorks: listResult.ok,
    openPetsEntry: entry,
    canConfigure: state === "needs_setup",
    canReplace: entry.present && !(entry.verified && entry.matchesExpected),
    canRemove: entry.present,
  };
}

function validateSelectedPetId(value: unknown): string | undefined {
  if (value === undefined || value === null || value === "") return undefined;
  if (typeof value !== "string") throw new Error("Invalid selected pet id.");
  const pet = getAppStateSnapshot().pets.installed.find((candidate) => candidate.id === value);
  if (!pet || pet.broken) throw new Error("Selected pet is not installed or is broken.");
  return pet.id;
}

function validateCommandMode(value: unknown): OpenPetsCommandMode {
  if (app.isPackaged) return "bundled";
  return value === "local" ? "local" : "published";
}

function getPetOptions(): readonly AgentSetupPetOption[] {
  const state = getAppStateSnapshot();
  return state.pets.installed.filter(isUsablePet).map((pet) => ({ id: pet.id, displayName: pet.displayName, default: pet.id === state.preferences.defaultPetId }));
}

function isUsablePet(pet: InstalledPetState): boolean {
  return pet.installed && !pet.broken && !pet.builtIn;
}

async function runClaudeCommand(spec: ClaudeCommandSpec): Promise<CommandResult> {
  for (const command of getClaudeCommandCandidates(spec.command)) {
    const result = await runCommand({ command, args: spec.args });
    if (result.ok || !isCommandNotFound(result)) return result;
  }
  return { ok: false, timedOut: false, exitCode: null, stdout: "", stderr: "", error: "Claude command was not found." };
}

function runCommand(spec: ClaudeCommandSpec): Promise<CommandResult> {
  return new Promise((resolve) => {
    const command = process.platform === "win32" && spec.command.toLowerCase().endsWith(".cmd") ? "cmd.exe" : spec.command;
    const args = process.platform === "win32" && spec.command.toLowerCase().endsWith(".cmd") ? ["/d", "/s", "/c", spec.command, ...spec.args] : spec.args;
    let child;
    try {
      child = spawn(command, args, { cwd: app.getPath("home"), env: createCommandEnv(), windowsHide: true, shell: false });
    } catch (error) {
      resolve({ ok: false, timedOut: false, exitCode: null, stdout: "", stderr: "", error: error instanceof Error ? error.message : "Command failed to start." });
      return;
    }
    let stdout = "";
    let stderr = "";
    let settled = false;
    const timer = setTimeout(() => {
      if (settled) return;
      settled = true;
      child.kill();
      resolve({ ok: false, timedOut: true, exitCode: null, stdout: sanitizeAgentSetupOutput(stdout), stderr: sanitizeAgentSetupOutput(stderr), error: "Command timed out." });
    }, commandTimeoutMs);

    child.stdout?.on("data", (chunk: Buffer) => { stdout = appendBounded(stdout, chunk.toString("utf8")); });
    child.stderr?.on("data", (chunk: Buffer) => { stderr = appendBounded(stderr, chunk.toString("utf8")); });
    child.on("error", (error) => {
      if (settled) return;
      settled = true;
      clearTimeout(timer);
      resolve({ ok: false, timedOut: false, exitCode: null, stdout: sanitizeAgentSetupOutput(stdout), stderr: sanitizeAgentSetupOutput(stderr), error: error.message });
    });
    child.on("close", (code) => {
      if (settled) return;
      settled = true;
      clearTimeout(timer);
      resolve({ ok: code === 0, timedOut: false, exitCode: code, stdout: sanitizeAgentSetupOutput(stdout), stderr: sanitizeAgentSetupOutput(stderr), error: undefined });
    });
  });
}

function delay(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

function getClaudeCommandCandidates(command: string): readonly string[] {
  if (command !== "claude") return [command];
  const preferred = getPreferredClaudeCommand();
  if (preferred !== "claude") return [preferred];
  if (process.platform === "win32") return ["claude", "claude.cmd"];
  return ["claude"];
}

function createCommandEnv(): NodeJS.ProcessEnv {
  const separator = process.platform === "win32" ? ";" : ":";
  const existingPath = process.env.PATH ?? "";
  return { ...process.env, PATH: dedupePathEntries([existingPath, ...getExtraCommandPaths()], separator).join(separator) };
}

function getExtraCommandPaths(): readonly string[] {
  if (process.platform === "win32") return [];
  const home = app.getPath("home");
  const env = process.env;
  return filterExistingPaths([
    "/opt/homebrew/bin",
    "/opt/homebrew/sbin",
    "/usr/local/bin",
    "/usr/local/sbin",
    "/usr/bin",
    "/bin",
    "/usr/sbin",
    "/sbin",
    join(home, "bin"),
    join(home, ".local", "bin"),
    join(home, ".opencode", "bin"),
    join(env.VOLTA_HOME || join(home, ".volta"), "bin"),
    join(env.BUN_INSTALL || join(home, ".bun"), "bin"),
    join(env.MISE_DATA_DIR || join(home, ".local", "share", "mise"), "shims"),
    join(env.ASDF_DATA_DIR || join(home, ".asdf"), "shims"),
    env.PNPM_HOME,
    join(home, ".local", "share", "pnpm"),
    join(home, "Library", "pnpm"),
    join(env.NVM_DIR || join(home, ".nvm"), "current", "bin"),
  ]);
}

function filterExistingPaths(paths: readonly (string | undefined)[]): readonly string[] {
  return paths.filter((path): path is string => Boolean(path && existsSync(path)));
}

function dedupePathEntries(paths: readonly string[], separator: string): readonly string[] {
  const seen = new Set<string>();
  const entries: string[] = [];
  for (const path of paths.flatMap((value) => value.split(separator)).filter(Boolean)) {
    if (seen.has(path)) continue;
    seen.add(path);
    entries.push(path);
  }
  return entries;
}

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

function isCommandNotFound(result: CommandResult): boolean {
  return Boolean(result.error && /ENOENT|not found/i.test(result.error));
}

function summarizeCommandResult(result: CommandResult): string {
  if (result.timedOut) return "command timed out.";
  const output = sanitizeAgentSetupOutput(result.stderr || result.stdout || result.error || `exit code ${result.exitCode ?? "unknown"}`);
  return output || "command failed.";
}

function formatUserPath(path: string | undefined): string | undefined {
  if (!path) return undefined;
  return path.replace(app.getPath("home"), "~");
}

function appendBounded(existing: string, next: string): string {
  const combined = existing + next;
  return combined.length > maxOutputBytes ? combined.slice(combined.length - maxOutputBytes) : combined;
}

function writeActionJournal(entry: Omit<AgentSetupJournalEntry, "timestamp"> & { readonly timestamp?: string }): void {
  try {
    const path = getJournalPath();
    const entries = readActionJournal().concat({ ...entry, command: entry.command.map((part) => formatUserPath(part) ?? part), message: sanitizeAgentSetupOutput(entry.message), timestamp: entry.timestamp || new Date().toISOString() }).slice(-20);
    mkdirSync(dirname(path), { recursive: true });
    const tempPath = `${path}.${process.pid}.tmp`;
    writeFileSync(tempPath, `${JSON.stringify(entries, null, 2)}\n`, "utf8");
    renameSync(tempPath, path);
  } catch (error) {
    console.error("Failed to write OpenPets agent setup action journal.", error);
  }
}

function readActionJournal(): AgentSetupJournalEntry[] {
  const path = getJournalPath();
  if (!existsSync(path)) return [];
  try {
    const parsed = JSON.parse(readFileSync(path, "utf8")) as unknown;
    return Array.isArray(parsed) ? parsed.filter(isJournalEntry).slice(-20) : [];
  } catch {
    return [];
  }
}

function getJournalPath(): string {
  return join(app.getPath("userData"), "agent-setup-actions.json");
}

function isJournalEntry(value: unknown): value is AgentSetupJournalEntry {
  return typeof value === "object" && value !== null && typeof (value as { timestamp?: unknown }).timestamp === "string";
}

function journalActionFor(action: AgentSetupAction): JournalAction {
  if (action === "replace") return "replace";
  if (action === "remove") return "remove";
  return "configure";
}

export const agentSetupInternalsForChecks = {
  sanitizeAgentSetupOutput,
  createOpenPetsHookSettingsPreview,
};
