#!/usr/bin/env npx tsx

// Interactive MCP configuration script
// Configures MCP server connection + agent rules for Cursor, Droid, Claude Code, or Codex

import * as fs from 'fs';
import * as path from 'path';
import * as readline from 'readline';
import * as os from 'os';
import { execSync } from 'child_process';

const HOME = os.homedir();

// Platform detection. WSL is reported as 'linux' by os.platform(); we split it
// out so callers can warn about Windows-mount footguns without changing the
// proxy install path (WSL stores the proxy under WSL's ext4 $HOME, not /mnt/c).
export type ResolvedPlatform = 'darwin' | 'linux' | 'linux-wsl' | 'win32';

export function detectPlatform(): ResolvedPlatform {
  const p = os.platform();
  if (p === 'darwin') return 'darwin';
  if (p === 'win32') return 'win32';
  if (p === 'linux') {
    if (process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP) return 'linux-wsl';
    try {
      const procVersion = fs.readFileSync('/proc/version', 'utf-8');
      if (/microsoft|wsl/i.test(procVersion)) return 'linux-wsl';
    } catch {
      // /proc/version missing — treat as plain linux
    }
    return 'linux';
  }
  return 'linux'; // unknown unix — fall back to XDG
}

// Stable install location for the proxy — independent of where the repo lives.
//   macOS: ~/Library/Application Support/priority-forge
//   Linux + WSL: ~/.local/share/priority-forge (XDG data home)
//   Windows: %LOCALAPPDATA%\priority-forge
export function resolveProxyInstallDir(platform: ResolvedPlatform = detectPlatform()): string {
  switch (platform) {
    case 'darwin':
      return path.join(HOME, 'Library', 'Application Support', 'priority-forge');
    case 'win32':
      return path.join(
        process.env.LOCALAPPDATA || path.join(HOME, 'AppData', 'Local'),
        'priority-forge'
      );
    case 'linux':
    case 'linux-wsl':
    default:
      return path.join(HOME, '.local', 'share', 'priority-forge');
  }
}

// Refuse to install MCP configs/proxy when the repo lives on a Windows-mounted
// path inside WSL. Mirrors setup.sh check_cross_mount — exported for tests.
export function isWindowsMountPath(p: string): boolean {
  return /^\/mnt\/[a-zA-Z]\//.test(p);
}

const PLATFORM = detectPlatform();
const PROXY_INSTALL_DIR = resolveProxyInstallDir(PLATFORM);
const PROXY_INSTALL_PATH = path.join(PROXY_INSTALL_DIR, 'mcp-proxy.js');

if (PLATFORM === 'linux-wsl' && isWindowsMountPath(process.cwd()) &&
    process.env.PRIORITY_FORGE_ALLOW_CROSS_MOUNT !== '1') {
  console.error('\n[ERROR] priority-forge is checked out on a Windows-mounted path:');
  console.error(`    ${process.cwd()}`);
  console.error('\nSQLite writes and file watching are unreliable on /mnt/<drive>/.');
  console.error('Move the repo into your WSL home filesystem before running setup:mcp:');
  console.error(`    mv "${process.cwd()}" ~/priority-forge`);
  console.error('    cd ~/priority-forge');
  console.error('    npm run setup:mcp');
  console.error('\nOverride (NOT recommended): PRIORITY_FORGE_ALLOW_CROSS_MOUNT=1 npm run setup:mcp\n');
  process.exit(1);
}

// ~/.claude/settings.json is Claude Code's user-level config. We merge a
// PreToolUse hook into it that blocks native Task* tools (TaskCreate,
// TaskUpdate, TaskList, TaskGet, TaskOutput, TaskStop) — forcing agents to
// route task tracking through the Priority Forge MCP server. Without this,
// compliance is limited to AGENT_RULES.md text which is model-soft.
const CLAUDE_SETTINGS_PATH = path.join(HOME, '.claude', 'settings.json');
const NATIVE_TASK_MATCHER = '^(TaskCreate|TaskUpdate|TaskList|TaskGet|TaskOutput|TaskStop)$';
// Single-quoted shell string; escaped double quotes so the resulting JSON is
// valid when serialized. Claude Code reads stdout of this command, parses it
// as JSON, and applies the `decision: block` verdict with the given reason.
const NATIVE_TASK_BLOCK_COMMAND = `printf '%s' '{"decision":"block","reason":"Native Task tools are disabled on this machine. Use Priority Forge MCP instead: mcp__priority-forge__create_task, mcp__priority-forge__update_task, mcp__priority-forge__get_priorities, mcp__priority-forge__get_top_priority, mcp__priority-forge__complete_task. If the MCP server is unreachable, fall back to REST at http://127.0.0.1:3456/tasks (see ~/.claude/CLAUDE.md)."}'`;

interface MCPConfig {
  mcpServers: {
    [key: string]: {
      type?: string;
      url: string;
    };
  };
}

interface ToolConfig {
  name: string;
  mcpConfigPath: string;
  agentRulesPath: string;
  mcpConfigFormat: (url: string) => MCPConfig;
  agentRulesHeader: string;
}

// Cursor-specific install locations. Declared above TOOLS because the TOOLS
// literal references CURSOR_MCP_JSON / CURSOR_RULE_PATH during module init —
// declaring them after TOOLS would trip the TDZ and throw
// "Cannot access 'CURSOR_MCP_JSON' before initialization" on every run.
// The ~/.cursor/hooks layout mirrors Cursor's documented hook schema;
// priority-forge scopes its hook to a subdirectory so upgrades/uninstalls
// never touch foreign hooks.
const CURSOR_DIR = path.join(HOME, '.cursor');
const CURSOR_MCP_JSON = path.join(CURSOR_DIR, 'mcp.json');
const CURSOR_RULE_PATH = path.join(CURSOR_DIR, 'rules', 'priority-forge.mdc');
const CURSOR_HOOKS_JSON = path.join(CURSOR_DIR, 'hooks', 'hooks.json');
const CURSOR_HOOK_DIR = path.join(CURSOR_DIR, 'hooks', 'priority-forge');
const CURSOR_HOOK_SCRIPT = path.join(CURSOR_HOOK_DIR, 'sessionStart.sh');
const CURSOR_RULES_LEGACY = path.join(HOME, '.cursorrules');

const TOOLS: Record<string, ToolConfig> = {
  '1': {
    name: 'Cursor',
    mcpConfigPath: CURSOR_MCP_JSON,
    // Modern Cursor reads rules from ~/.cursor/rules/*.mdc. The legacy ~/.cursorrules
    // flat file is still honored but has no frontmatter (no alwaysApply, no description)
    // so rule injection is inconsistent. See migrateCursorRules() for the migration path.
    agentRulesPath: CURSOR_RULE_PATH,
    mcpConfigFormat: (url: string) => ({
      mcpServers: {
        'priority-forge': { url }
      }
    }),
    // Frontmatter is emitted separately (configureAgentRules detects .mdc target);
    // this header is just the Cursor-flavored banner inside the marker block.
    agentRulesHeader: '# Priority Forge Configuration (auto-generated)\n\n'
  },
  '2': {
    name: 'Droid (Factory CLI)',
    mcpConfigPath: path.join(HOME, '.factory', 'mcp.json'),
    agentRulesPath: path.join(HOME, '.factory', 'AGENTS.md'),
    mcpConfigFormat: (url: string) => ({
      mcpServers: {
        'priority-forge': { type: 'http', url }
      }
    }),
    agentRulesHeader: '<!-- Priority Forge Configuration (auto-generated) -->\n\n'
  },
  '3': {
    name: 'Claude Code CLI',
    // MCP is registered via `claude mcp add` (local scope, per working directory).
    // --scope user writes to top-level mcpServers but Claude Code does NOT load that.
    // Only projects[cwd].mcpServers (local scope) is loaded in sessions.
    // mcpConfigPath is unused for Claude Code — configureMCP handles it specially.
    mcpConfigPath: '',
    agentRulesPath: path.join(HOME, '.claude', 'CLAUDE.md'),
    mcpConfigFormat: (_url: string) => ({ mcpServers: {} }),
    agentRulesHeader: '<!-- Priority Forge Configuration (auto-generated) -->\n\n'
  },
  '4': {
    name: 'OpenAI Codex CLI',
    // Codex stores MCP config in TOML at ~/.codex/config.toml. The
    // mcpConfigPath/mcpConfigFormat fields are unused for Codex —
    // configureMCP() dispatches to configureMCPCodex() which handles
    // TOML serialization (no JSON merging applies).
    mcpConfigPath: '',
    agentRulesPath: path.join(HOME, '.codex', 'AGENTS.md'),
    mcpConfigFormat: (_url: string) => ({ mcpServers: {} }),
    agentRulesHeader: '<!-- Priority Forge Configuration (auto-generated) -->\n\n'
  }
};

const SERVER_URL = 'http://127.0.0.1:3456/mcp';
const AGENT_RULES_SOURCE = path.join(__dirname, '..', 'AGENT_RULES.md');
const AGENT_RULES_CODEX_SOURCE = path.join(__dirname, '..', 'AGENT_RULES_CODEX.md');
const AGENT_RULES_CURSOR_SOURCE = path.join(__dirname, '..', 'AGENT_RULES_CURSOR.md');

// Absolute repo root — stable handle for self-contained files we install
// outside the repo (hooks, repo-path discovery file, plugin bundle).
const FORGE_REPO_ROOT = path.resolve(__dirname, '..');

// XDG data home for the repo-path discovery file. Hooks and the Cursor plugin
// both read this so they can locate the forge repo even when Cursor is launched
// from an arbitrary cwd or the plugin lives under ~/.cursor/plugins/.
const REPO_PATH_DISCOVERY_DIR = os.platform() === 'darwin'
  ? path.join(HOME, 'Library', 'Application Support', 'priority-forge')
  : path.join(HOME, '.local', 'share', 'priority-forge');
const REPO_PATH_DISCOVERY_FILE = path.join(REPO_PATH_DISCOVERY_DIR, 'repo-path');

// (CURSOR_* install-location constants are declared above TOOLS; see TDZ note.)

// Keys in ~/.cursor/mcp.json that look like competing project-management tools.
// Match is case-insensitive substring so common variants (linear-api, plugin-linear,
// jira-server, etc.) are all covered. Purely a warning heuristic — we never mutate.
const COMPETING_PM_PATTERN = /linear|jira|asana|clickup|monday|trello|todoist/i;

const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout
});

function question(prompt: string): Promise<string> {
  return new Promise((resolve) => {
    rl.question(prompt, resolve);
  });
}

function ensureDirectoryExists(filePath: string): void {
  const dir = path.dirname(filePath);
  if (!fs.existsSync(dir)) {
    fs.mkdirSync(dir, { recursive: true });
    console.log(`  Created directory: ${dir}`);
  }
}

function readJsonFile(filePath: string): any {
  if (fs.existsSync(filePath)) {
    try {
      const content = fs.readFileSync(filePath, 'utf-8');
      return JSON.parse(content);
    } catch {
      return null;
    }
  }
  return null;
}

/**
 * Returns a string reason if the candidate directory should NOT have an
 * `.mcp.json` written into it, or null if the write is safe.
 *
 * Refusal categories (all silent skips, no throw):
 *   - filesystem root (/, C:\) — wholesale EROFS / world-readable concern
 *   - parent or ancestor of HOME — would pollute the user's whole tree
 *   - HOME itself, HOME/Desktop, ~/.claude — hub dirs, not project dirs
 *   - non-existent path — Claude Code history can outlive the project
 *   - not a directory — symlinks-to-files, files-where-dirs-expected, etc.
 *   - not writable for the current user — read-only mounts, root-owned dirs
 *
 * Conservative on purpose: the prior version wrote .mcp.json into every
 * directory Claude Code had ever opened from, including one that
 * resolved to '/' on at least one user's machine. The crash there left
 * setup.sh's build + launchd steps unfinished. Refusing aggressively
 * here keeps unrelated trees clean and prevents single-bad-path crashes.
 */
export function validateRegistrationCandidate(dir: string): string | null {
  const resolved = path.resolve(dir);
  const parsed = path.parse(resolved);

  // Filesystem root / drive root.
  if (resolved === parsed.root) {
    return 'filesystem root — refusing to write .mcp.json at /';
  }

  // HOME and immediate hub dirs that aren't projects.
  if (resolved === HOME) return 'is $HOME, not a project directory';
  if (resolved === path.join(HOME, 'Desktop')) return 'is Desktop hub, not a project directory';
  if (resolved === path.join(HOME, '.claude')) return 'is Claude Code config dir, not a project directory';

  // Parent or ancestor of HOME (e.g., /Users on macOS, /home on Linux).
  if (HOME.startsWith(resolved + path.sep) || HOME === resolved + path.sep.slice(0, 1)) {
    return 'is ancestor of $HOME — refusing to pollute the whole user tree';
  }

  // Path must exist + be a directory.
  let stat: fs.Stats;
  try {
    stat = fs.statSync(resolved);
  } catch (err) {
    return `does not exist (${err instanceof Error ? err.message : 'stat failed'})`;
  }
  if (!stat.isDirectory()) {
    return 'not a directory';
  }

  // Must be writable. accessSync throws on no-permission.
  try {
    fs.accessSync(resolved, fs.constants.W_OK);
  } catch {
    return 'not writable for current user';
  }

  return null;
}

function mergeMCPConfig(existing: MCPConfig | null, newConfig: MCPConfig): MCPConfig {
  if (!existing) {
    return newConfig;
  }
  
  return {
    ...existing,
    mcpServers: {
      ...existing.mcpServers,
      ...newConfig.mcpServers
    }
  };
}

function configureMCPClaudeCode(): boolean {
  console.log(`\n  Configuring MCP for Claude Code CLI...`);

  // Default to HTTP transport (Claude Code supports `type: "http"` in .mcp.json,
  // documented at https://code.claude.com/docs/en/mcp). HTTP eliminates the
  // stdio proxy entirely — no orphan processes, no framing auto-detect, no
  // client-side stdio wedge (see docs/mcp-hang-rca.md for why that matters).
  // Cursor and Droid already use HTTP via the same backend endpoint.
  //
  // Escape hatch: set PRIORITY_FORGE_USE_STDIO=1 to fall back to the legacy
  // stdio-proxy registration if HTTP transport ever regresses in Claude Code.
  const useStdio = process.env.PRIORITY_FORGE_USE_STDIO === '1';

  // Even when running HTTP, install the proxy to its stable location so the
  // stdio fallback is always one env var away. Cheap insurance.
  const proxySource = path.resolve(__dirname, 'mcp-stdio-proxy.js');
  if (!fs.existsSync(proxySource)) {
    console.log(`  ✗ Proxy source not found: ${proxySource}`);
    console.log(`  Make sure you are running this from the priority-forge repo directory.`);
    return false;
  }
  fs.mkdirSync(PROXY_INSTALL_DIR, { recursive: true });
  fs.copyFileSync(proxySource, PROXY_INSTALL_PATH);
  console.log(`  ✓ Proxy installed to: ${PROXY_INSTALL_PATH} (fallback)`);

  const claudeJsonPath = path.join(HOME, '.claude.json');

  // Remove any stale user-scope registration (top-level mcpServers — not loaded by sessions)
  const globalCfg = readJsonFile(claudeJsonPath);
  if (globalCfg?.mcpServers?.['priority-forge']) {
    try {
      execSync(`claude mcp remove --scope user priority-forge 2>/dev/null || true`, { stdio: 'pipe' });
      console.log(`  ✓ Removed stale user-scope registration (was not loaded by sessions)`);
    } catch { /* ignore */ }
  }

  // Determine which directories to register in.
  //
  // DEFAULT: only the current working directory (where setup.sh / npm run
  // setup:mcp was invoked from). That's the one place we KNOW the user
  // wants PF available — they explicitly opted in by running the
  // configurator from that directory.
  //
  // OPT-IN: PRIORITY_FORGE_REGISTER_ALL=1 enables the legacy "scan every
  // project Claude Code has ever opened from ~/.claude.json" behavior,
  // for users who want PF available across all their repos. Still
  // path-validated — root filesystem, HOME itself, HOME/Desktop, and
  // non-writable / non-directory entries are always skipped.
  //
  // Bug fixed: the previous default iterated ~/.claude.json's projects
  // map blindly and wrote .mcp.json into every directory Claude Code
  // had ever opened (sometimes 15+ unrelated projects). One entry could
  // resolve to '/' on some setups, causing an EROFS crash that left the
  // rest of setup.sh (build, launchd, server start) unfinished. The
  // crash + the spray were independent failures of the same iteration
  // pattern; scoping to cwd by default + per-write try/catch fixes both.
  const registerAll = process.env.PRIORITY_FORGE_REGISTER_ALL === '1';
  const dirsToRegister = new Set<string>();
  dirsToRegister.add(process.cwd());

  if (registerAll) {
    console.log('  ℹ PRIORITY_FORGE_REGISTER_ALL=1 — also scanning Claude Code project history');
    const updated = readJsonFile(claudeJsonPath);
    const knownProjects = Object.keys(updated?.projects || {});
    for (const dir of knownProjects) {
      dirsToRegister.add(dir);
    }
  }

  // For Claude Code we ship project-scoped discovery via .mcp.json.
  // Claude prompts once before using project-scoped servers; that approval is
  // intentionally user-mediated rather than something we try to synthesize by
  // mutating ~/.claude.json. Users who want a personal no-prompt setup can add
  // a local-scoped server with `claude mcp add --scope local ...`.
  const mcpServerEntry = useStdio
    ? {
        type: 'stdio',
        command: 'node',
        args: [PROXY_INSTALL_PATH],
      }
    : {
        type: 'http',
        url: SERVER_URL,
      };

  console.log(`  ℹ Transport: ${useStdio ? 'stdio (legacy proxy)' : 'http (direct, recommended)'}`);

  let registered = 0;
  let skipped = 0;
  let failed = 0;
  const refusedReasons = new Map<string, string>();

  for (const dir of Array.from(dirsToRegister)) {
    const refusal = validateRegistrationCandidate(dir);
    if (refusal) {
      refusedReasons.set(dir, refusal);
      continue;
    }

    const mcpJsonPath = path.join(dir, '.mcp.json');
    try {
      const existingMcpJson = readJsonFile(mcpJsonPath) || {};
      const existingEntry = existingMcpJson?.mcpServers?.['priority-forge'];
      const isCorrect = useStdio
        ? (existingEntry?.command === 'node' &&
           Array.isArray(existingEntry?.args) &&
           existingEntry.args.includes(PROXY_INSTALL_PATH))
        : (existingEntry?.type === 'http' && existingEntry?.url === SERVER_URL);

      if (!isCorrect) {
        const updated = {
          ...existingMcpJson,
          mcpServers: {
            ...existingMcpJson?.mcpServers,
            'priority-forge': mcpServerEntry,
          },
        };
        // Remove any stale top-level 'priority-forge' key from previous buggy writes
        delete (updated as any)['priority-forge'];
        fs.writeFileSync(mcpJsonPath, JSON.stringify(updated, null, 2));
        console.log(`  ✓ Written .mcp.json in: ${dir}`);
        registered++;
      } else {
        skipped++;
      }
    } catch (err) {
      const msg = err instanceof Error ? err.message : String(err);
      console.log(`  ⚠ Failed to write .mcp.json in ${dir}: ${msg}`);
      failed++;
    }
  }

  // Summary — always print so the user knows what got touched.
  if (registered === 0 && skipped > 0 && failed === 0) {
    console.log(`  ✓ All ${skipped} director${skipped === 1 ? 'y' : 'ies'} already up to date`);
  } else {
    const parts = [
      `${registered} configured`,
      `${skipped} already up to date`,
    ];
    if (failed > 0) parts.push(`${failed} failed`);
    if (refusedReasons.size > 0) parts.push(`${refusedReasons.size} refused`);
    console.log(`  ✓ ${parts.join(', ')}`);
  }
  if (refusedReasons.size > 0) {
    console.log('  ℹ Refused (path validation):');
    for (const [dir, reason] of refusedReasons) {
      console.log(`     - ${dir}: ${reason}`);
    }
  }
  if (!registerAll && dirsToRegister.size === 1) {
    console.log('  ℹ Default: registered in CWD only. Set PRIORITY_FORGE_REGISTER_ALL=1 to also');
    console.log('    register in every project Claude Code has opened (path-validated).');
  }
  console.log('  ℹ Claude Code will prompt once before using project-scoped .mcp.json servers.');
  console.log('  ℹ If you want a personal no-prompt setup, add a local-scoped server with `claude mcp add --scope local`.');
  cleanLegacyMcpJson();
  installNativeTaskBlocker();
  return true;
}

// Installs a PreToolUse hook into ~/.claude/settings.json that blocks
// Claude Code's native Task tools. Safe to run repeatedly: merges with
// any existing hooks config and replaces our own entry on updates (keyed
// by the matcher string). See src/types/hooks.ts in claude-code-stuff
// for the SyncHookJSONOutput schema this produces.
function installNativeTaskBlocker(): void {
  console.log('\n  Installing PreToolUse hook to block native Task tools...');

  ensureDirectoryExists(CLAUDE_SETTINGS_PATH);

  const settings = readJsonFile(CLAUDE_SETTINGS_PATH) || {};
  if (!settings.hooks || typeof settings.hooks !== 'object') settings.hooks = {};
  if (!Array.isArray(settings.hooks.PreToolUse)) settings.hooks.PreToolUse = [];

  const entry = {
    matcher: NATIVE_TASK_MATCHER,
    hooks: [{ type: 'command', command: NATIVE_TASK_BLOCK_COMMAND }],
  };

  // Key on matcher — replace in place if we've already installed this hook
  // on a prior run, otherwise append so we don't clobber user's other hooks.
  const existingIdx = settings.hooks.PreToolUse.findIndex(
    (e: { matcher?: string }) => e?.matcher === NATIVE_TASK_MATCHER
  );

  let action: 'added' | 'updated' | 'unchanged';
  if (existingIdx >= 0) {
    const existing = settings.hooks.PreToolUse[existingIdx];
    const existingCommand = existing?.hooks?.[0]?.command;
    if (existingCommand === NATIVE_TASK_BLOCK_COMMAND) {
      action = 'unchanged';
    } else {
      settings.hooks.PreToolUse[existingIdx] = entry;
      action = 'updated';
    }
  } else {
    settings.hooks.PreToolUse.push(entry);
    action = 'added';
  }

  fs.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2) + '\n');
  console.log(`  ✓ Task-blocker hook ${action} in: ${CLAUDE_SETTINGS_PATH}`);
  console.log(`  ℹ  Takes effect on next Claude Code session start`);
}

function cleanLegacyMcpJson(): void {
  const legacyPath = path.join(HOME, '.claude', 'mcp.json');
  if (!fs.existsSync(legacyPath)) return;
  const legacy = readJsonFile(legacyPath);
  const keys = Object.keys(legacy?.mcpServers || {});
  if (keys.length === 1 && keys[0] === 'priority-forge') {
    fs.unlinkSync(legacyPath);
    console.log(`  ✓ Removed legacy ~/.claude/mcp.json (superseded by user-scope stdio registration)`);
  } else if (keys.includes('priority-forge')) {
    delete legacy.mcpServers['priority-forge'];
    fs.writeFileSync(legacyPath, JSON.stringify(legacy, null, 2));
    console.log(`  ✓ Removed priority-forge from legacy ~/.claude/mcp.json`);
  }
}

// Idempotently writes/updates the priority-forge entry in ~/.codex/config.toml.
// Codex MCP config syntax (per https://developers.openai.com/codex/mcp):
//   [mcp_servers.priority-forge]
//   url = "http://127.0.0.1:3456/mcp"
// We avoid pulling in a TOML dependency for one section. The block is
// detected/replaced by header-line scan and section-end heuristic
// (next "[..." line at column 0 or EOF). Comments and other sections
// are preserved verbatim.
function configureMCPCodex(): boolean {
  console.log(`\n  Configuring MCP for OpenAI Codex CLI...`);

  const codexConfigPath = path.join(HOME, '.codex', 'config.toml');
  ensureDirectoryExists(codexConfigPath);

  const existing = fs.existsSync(codexConfigPath)
    ? fs.readFileSync(codexConfigPath, 'utf-8')
    : '';

  const targetHeader = '[mcp_servers.priority-forge]';
  const newBlock = `${targetHeader}\nurl = "${SERVER_URL}"\n`;

  const lines = existing.split('\n');
  const headerIdx = lines.findIndex(l => l.trim() === targetHeader);

  let updated: string;
  let action: 'added' | 'updated' | 'unchanged';

  if (headerIdx === -1) {
    // Append the block; ensure exactly one blank line of separation.
    let prefix = existing;
    if (prefix.length > 0) {
      while (prefix.endsWith('\n')) prefix = prefix.slice(0, -1);
      prefix += '\n\n';
    }
    updated = prefix + newBlock;
    action = 'added';
  } else {
    // Find the end of the existing section: next header line or EOF.
    let endIdx = lines.length;
    for (let i = headerIdx + 1; i < lines.length; i++) {
      if (lines[i].trimStart().startsWith('[')) { endIdx = i; break; }
    }
    const currentSection = lines.slice(headerIdx, endIdx).join('\n').trim();
    if (currentSection === newBlock.trim()) {
      action = 'unchanged';
      updated = existing;
    } else {
      const before = lines.slice(0, headerIdx).join('\n').replace(/\n+$/, '');
      const after = lines.slice(endIdx).join('\n').replace(/^\n+/, '');
      const sepBefore = before.length > 0 ? '\n\n' : '';
      const sepAfter = after.length > 0 ? '\n' + after : '';
      updated = before + sepBefore + newBlock + sepAfter;
      action = 'updated';
    }
  }

  if (action !== 'unchanged') fs.writeFileSync(codexConfigPath, updated);
  console.log(`  ✓ MCP entry ${action} in: ${codexConfigPath}`);
  return true;
}

// Strips our marker block from the legacy ~/.cursorrules flat file. Called
// during Cursor configuration so users who were on the old installer don't
// end up with duplicated rules in two places. Only removes content between
// our markers — foreign content in the file is preserved verbatim. Deletes
// the file if our block was its only content.
function migrateCursorRules(): void {
  if (!fs.existsSync(CURSOR_RULES_LEGACY)) return;
  const content = fs.readFileSync(CURSOR_RULES_LEGACY, 'utf-8');
  const markerStart = '<!-- PRIORITY_FORGE_START -->';
  const markerEnd = '<!-- PRIORITY_FORGE_END -->';
  if (!content.includes(markerStart)) return;

  const regex = new RegExp(`${markerStart}[\\s\\S]*?${markerEnd}`, 'g');
  const stripped = content.replace(regex, '').replace(/\n{3,}/g, '\n\n').trim();

  if (stripped.length === 0) {
    fs.unlinkSync(CURSOR_RULES_LEGACY);
    console.log(`  ✓ Removed legacy ${CURSOR_RULES_LEGACY} (content migrated to ~/.cursor/rules/priority-forge.mdc)`);
  } else {
    fs.writeFileSync(CURSOR_RULES_LEGACY, stripped + '\n');
    console.log(`  ✓ Removed Priority Forge block from legacy ${CURSOR_RULES_LEGACY} (other content preserved)`);
  }
}

// Writes the absolute path of this repo to a well-known discovery file so
// hooks (installed here) and the Cursor plugin bundle (which has no idea
// where the repo lives) can locate setup.sh without relying on $PWD.
function writeRepoPathDiscoveryFile(): void {
  fs.mkdirSync(REPO_PATH_DISCOVERY_DIR, { recursive: true });
  const existing = fs.existsSync(REPO_PATH_DISCOVERY_FILE)
    ? fs.readFileSync(REPO_PATH_DISCOVERY_FILE, 'utf-8').trim()
    : '';
  if (existing === FORGE_REPO_ROOT) return;
  fs.writeFileSync(REPO_PATH_DISCOVERY_FILE, FORGE_REPO_ROOT + '\n');
  console.log(`  ✓ Repo path discovery file: ${REPO_PATH_DISCOVERY_FILE}`);
}

// Installs a sessionStart hook that ensures the Priority Forge backend is
// reachable whenever Cursor starts a session. Order of operations:
//   1. If /health already responds, exit silently (common steady state).
//   2. Try to kick the persistent service (systemd user unit / launchd agent).
//   3. Fall back to `nohup setup.sh start:backend` using the repo path baked
//      in at install time.
// All branches emit a single-line JSON status to Cursor's hook log so drift
// (service missing, repo moved, port taken) shows up without silent failure.
function installCursorSessionStartHook(): void {
  console.log(`\n  Installing Cursor sessionStart hook...`);

  fs.mkdirSync(CURSOR_HOOK_DIR, { recursive: true });
  fs.mkdirSync(path.dirname(CURSOR_HOOKS_JSON), { recursive: true });

  const hookScript = `#!/usr/bin/env bash
# Priority Forge sessionStart hook (auto-generated by npm run setup:mcp).
# Ensures the backend at :3456 is reachable when a Cursor session starts.
# Safe to re-run; never blocks session startup — max wall time ~2s.

set -u

PRIORITY_FORGE_DIR=${shellQuote(FORGE_REPO_ROOT)}
HEALTH_URL="http://127.0.0.1:3456/health"

emit() { printf '{"priority-forge":"%s"}\\n' "$1"; }

if curl -fsS --max-time 1 "$HEALTH_URL" >/dev/null 2>&1; then
  emit "already-running"
  exit 0
fi

if command -v systemctl >/dev/null 2>&1; then
  if systemctl --user start priority-forge-backend >/dev/null 2>&1; then
    emit "service-kicked-systemd"
    exit 0
  fi
fi

if command -v launchctl >/dev/null 2>&1; then
  if launchctl kickstart "gui/$(id -u)/com.priority-forge.backend" >/dev/null 2>&1; then
    emit "service-kicked-launchd"
    exit 0
  fi
fi

if [ -x "$PRIORITY_FORGE_DIR/setup.sh" ]; then
  nohup bash "$PRIORITY_FORGE_DIR/setup.sh" start:backend >/dev/null 2>&1 &
  disown 2>/dev/null || true
  emit "nohup-fallback"
  exit 0
fi

emit "failed"
exit 0
`;

  fs.writeFileSync(CURSOR_HOOK_SCRIPT, hookScript);
  fs.chmodSync(CURSOR_HOOK_SCRIPT, 0o755);
  console.log(`  ✓ Hook script: ${CURSOR_HOOK_SCRIPT}`);

  // Merge entry into hooks.json, keyed by hook id "priority-forge". Preserves
  // any unrelated hooks the user already has. Format follows Cursor's documented
  // hooks schema: { "<event>": [ { "id": "...", "command": "..." } ] }.
  const hooks = readJsonFile(CURSOR_HOOKS_JSON) || {};
  if (!Array.isArray(hooks.sessionStart)) hooks.sessionStart = [];

  const entry = { id: 'priority-forge', command: CURSOR_HOOK_SCRIPT };
  const existingIdx = (hooks.sessionStart as Array<{ id?: string }>).findIndex(
    (e) => e?.id === 'priority-forge'
  );

  let action: 'added' | 'updated' | 'unchanged';
  if (existingIdx >= 0) {
    const existing = hooks.sessionStart[existingIdx];
    if (existing?.command === CURSOR_HOOK_SCRIPT) {
      action = 'unchanged';
    } else {
      hooks.sessionStart[existingIdx] = entry;
      action = 'updated';
    }
  } else {
    hooks.sessionStart.push(entry);
    action = 'added';
  }

  fs.writeFileSync(CURSOR_HOOKS_JSON, JSON.stringify(hooks, null, 2) + '\n');
  console.log(`  ✓ hooks.json entry ${action}: ${CURSOR_HOOKS_JSON}`);
}

// Minimal POSIX shell single-quote escaper for paths baked into generated scripts.
// Wraps in single quotes and escapes embedded single quotes with '\''.
function shellQuote(s: string): string {
  return `'${s.replace(/'/g, "'\\''")}'`;
}

// Non-failing warning when other PM MCPs share ~/.cursor/mcp.json with us.
// Agents may pick the other tool for task operations unless rules disambiguate,
// which creates silent split-brain between two PM stores. We never modify the
// user's config — this is a visibility aid only.
function warnCompetingCursorPMMcps(): void {
  if (!fs.existsSync(CURSOR_MCP_JSON)) return;
  const cfg = readJsonFile(CURSOR_MCP_JSON);
  const servers: string[] = Object.keys(cfg?.mcpServers || {});
  const competing = servers.filter(
    (key) => key !== 'priority-forge' && COMPETING_PM_PATTERN.test(key)
  );
  if (competing.length === 0) return;

  const yellow = '\x1b[33m';
  const reset = '\x1b[0m';
  console.log(`\n${yellow}  ⚠  Competing PM MCP servers detected in ${CURSOR_MCP_JSON}:${reset}`);
  for (const key of competing) {
    console.log(`${yellow}      - ${key}${reset}`);
  }
  console.log(`${yellow}     Cursor agents may route task operations through one of these instead of${reset}`);
  console.log(`${yellow}     priority-forge. If they are unused, remove them from ~/.cursor/mcp.json.${reset}`);
  console.log(`${yellow}     (Priority Forge's installer never modifies other MCP entries.)${reset}`);
}

function configureMCP(tool: ToolConfig): boolean {
  // Claude Code uses a different registration mechanism
  if (tool.name === 'Claude Code CLI') {
    return configureMCPClaudeCode();
  }
  // Codex uses TOML, not JSON
  if (tool.name === 'OpenAI Codex CLI') {
    return configureMCPCodex();
  }

  console.log(`\n  Configuring MCP for ${tool.name}...`);

  ensureDirectoryExists(tool.mcpConfigPath);

  const existingConfig = readJsonFile(tool.mcpConfigPath);
  const newConfig = tool.mcpConfigFormat(SERVER_URL);

  // Check if already configured
  if (existingConfig?.mcpServers?.['priority-forge']) {
    const existingUrl = existingConfig.mcpServers['priority-forge'].url;
    if (existingUrl === SERVER_URL) {
      console.log('  MCP already configured (skipping)');
      return true;
    }
    console.log(`  Updating existing MCP config (was: ${existingUrl})`);
  }

  const mergedConfig = mergeMCPConfig(existingConfig, newConfig);

  fs.writeFileSync(tool.mcpConfigPath, JSON.stringify(mergedConfig, null, 2));
  console.log(`  ✓ MCP config written to: ${tool.mcpConfigPath}`);

  return true;
}

function configureAgentRules(tool: ToolConfig): boolean {
  console.log(`\n  Configuring agent rules for ${tool.name}...`);

  const sourcePath = tool.name === 'OpenAI Codex CLI'
    ? AGENT_RULES_CODEX_SOURCE
    : tool.name === 'Cursor'
      ? AGENT_RULES_CURSOR_SOURCE
      : AGENT_RULES_SOURCE;

  if (!fs.existsSync(sourcePath)) {
    console.log(`  ✗ Agent rules source not found: ${sourcePath}`);
    return false;
  }

  const agentRulesContent = fs.readFileSync(sourcePath, 'utf-8');
  const markerStart = '<!-- PRIORITY_FORGE_START -->';
  const markerEnd = '<!-- PRIORITY_FORGE_END -->';
  const wrappedContent = `${markerStart}\n${tool.agentRulesHeader}${agentRulesContent}\n${markerEnd}`;

  // Cursor's modern rule format lives in its own file under ~/.cursor/rules/
  // and requires YAML frontmatter (description + alwaysApply) for the rule to
  // be injected into every session reliably. The file is exclusively ours, so
  // we overwrite the whole thing each run instead of merging with foreign content.
  if (tool.name === 'Cursor') {
    ensureDirectoryExists(tool.agentRulesPath);
    const frontmatter = [
      '---',
      'description: Use Priority Forge MCP for all task tracking, priority queue, and project-management queries. Never use competing PM MCPs.',
      'alwaysApply: true',
      '---',
      '',
    ].join('\n');
    fs.writeFileSync(tool.agentRulesPath, frontmatter + wrappedContent + '\n');
    console.log(`  ✓ Cursor rule written: ${tool.agentRulesPath}`);
    return true;
  }

  ensureDirectoryExists(tool.agentRulesPath);

  // For Claude Code, also write the external protocol file for @imports users.
  // This allows users to keep CLAUDE.md modular with @priority-forge-protocol.md
  // instead of inlining 180+ lines.
  if (tool.name === 'Claude Code CLI') {
    const externalProtocolPath = path.join(path.dirname(tool.agentRulesPath), 'priority-forge-protocol.md');
    fs.writeFileSync(externalProtocolPath, wrappedContent);
    console.log(`  ✓ External protocol file written: ${externalProtocolPath}`);
  }

  if (fs.existsSync(tool.agentRulesPath)) {
    const existingContent = fs.readFileSync(tool.agentRulesPath, 'utf-8');

    // Check if the file uses @imports to reference Priority Forge content.
    // If so, the external protocol file (written above) is the source of truth —
    // do not inline the rules into CLAUDE.md. Just update the external file.
    if (existingContent.includes('@priority-forge-protocol.md')) {
      console.log('  ✓ Agent rules: @imports detected — external protocol file updated, CLAUDE.md unchanged');
      return true;
    }

    // Check if our content is already there (inline marker style)
    if (existingContent.includes(markerStart)) {
      // Replace existing Priority Forge section
      const regex = new RegExp(`${markerStart}[\\s\\S]*?${markerEnd}`, 'g');
      const updatedContent = existingContent.replace(regex, wrappedContent);
      fs.writeFileSync(tool.agentRulesPath, updatedContent);
      console.log('  ✓ Agent rules updated (replaced existing section)');
    } else {
      // Append to existing file
      const updatedContent = existingContent + '\n\n' + wrappedContent;
      fs.writeFileSync(tool.agentRulesPath, updatedContent);
      console.log('  ✓ Agent rules appended to existing file');
    }
  } else {
    // Create new file
    fs.writeFileSync(tool.agentRulesPath, wrappedContent);
    console.log(`  ✓ Agent rules created: ${tool.agentRulesPath}`);
  }

  return true;
}

function migrateAgentsMd(): void {
  // Claude Code used to write to AGENTS.md but now reads CLAUDE.md.
  // If AGENTS.md exists with Priority Forge content but CLAUDE.md doesn't, migrate it.
  const agentsPath = path.join(HOME, '.claude', 'AGENTS.md');
  const claudePath = path.join(HOME, '.claude', 'CLAUDE.md');
  const marker = '<!-- PRIORITY_FORGE_START -->';

  if (fs.existsSync(agentsPath) && !fs.existsSync(claudePath)) {
    const content = fs.readFileSync(agentsPath, 'utf-8');
    if (content.includes(marker)) {
      fs.writeFileSync(claudePath, content);
      console.log('  ✓ Migrated Priority Forge rules from AGENTS.md → CLAUDE.md');
    }
  }
}

async function main() {
  migrateAgentsMd();

  console.log('\n┌─────────────────────────────────────────┐');
  console.log('│     MCP Configuration                   │');
  console.log('└─────────────────────────────────────────┘\n');
  
  console.log('Which AI tool do you use?\n');
  console.log('  [1] Cursor');
  console.log('  [2] Droid (Factory CLI)');
  console.log('  [3] Claude Code CLI');
  console.log('  [4] OpenAI Codex CLI');
  console.log('');

  let choice = '';
  while (!TOOLS[choice]) {
    choice = await question('Enter choice (1-4): ');
    if (!TOOLS[choice]) {
      console.log('Invalid choice. Please enter 1, 2, 3, or 4.');
    }
  }
  
  const tool = TOOLS[choice];
  console.log(`\nConfiguring for ${tool.name}...`);

  // Cursor-only prep: migrate off the legacy ~/.cursorrules flat file and
  // write the repo-path discovery file so the sessionStart hook can locate
  // setup.sh even when Cursor launches from an unrelated cwd.
  if (tool.name === 'Cursor') {
    migrateCursorRules();
    writeRepoPathDiscoveryFile();
  }

  const mcpSuccess = configureMCP(tool);
  const agentSuccess = configureAgentRules(tool);

  // Cursor-only follow-ups: install the sessionStart hook and surface any
  // competing PM MCPs sharing ~/.cursor/mcp.json. Both are idempotent and
  // never mutate foreign entries.
  if (tool.name === 'Cursor' && mcpSuccess) {
    installCursorSessionStartHook();
    warnCompetingCursorPMMcps();
  }

  if (mcpSuccess && agentSuccess) {
    console.log('\n┌─────────────────────────────────────────┐');
    console.log('│  ✓ Configuration complete!              │');
    console.log('└─────────────────────────────────────────┘\n');
    if (tool.name === 'Claude Code CLI') {
      console.log('MCP Config:    repo-root .mcp.json (shared project scope)');
      console.log('               optional personal override via `claude mcp add --scope local`');
    } else if (tool.name === 'OpenAI Codex CLI') {
      console.log('MCP Config:    ~/.codex/config.toml');
    } else {
      console.log(`MCP Config:    ${tool.mcpConfigPath}`);
    }
    console.log(`Agent Rules:   ${tool.agentRulesPath}`);
    console.log(`Server URL:    ${SERVER_URL}`);
    console.log('\nIMPORTANT: Restart ' + tool.name + ' for changes to take effect.\n');
  } else {
    console.log('\n⚠ Configuration completed with warnings. Check messages above.');
  }
  
  rl.close();
}

// Only execute interactive flow when invoked as a script. Tests import this
// module to exercise the pure helpers (detectPlatform, resolveProxyInstallDir,
// isWindowsMountPath) and shouldn't trigger the readline prompt.
const isMain = (typeof require !== 'undefined' && require.main === module) ||
  /configure-mcp\.ts$/.test(process.argv[1] || '');

if (isMain) {
  main().catch((err) => {
    console.error('Error:', err);
    rl.close();
    process.exit(1);
  });
}
