/**
 * Setup Command
 *
 * One-time global MCP configuration writer.
 * Detects installed AI editors and writes the appropriate MCP config
 * so the CodraGraph MCP server is available in all projects.
 */
import fs from 'fs/promises';
import path from 'path';
import os from 'os';
import { execFile, execFileSync } from 'child_process';
import { promisify } from 'util';
import { fileURLToPath } from 'url';
import { createRequire } from 'module';
import { glob } from 'glob';
import { parseTree, modify, applyEdits } from 'jsonc-parser';
import { getGlobalDir } from '../storage/repo-manager.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const execFileAsync = promisify(execFile);
const require = createRequire(import.meta.url);
const pkg = require('../../package.json');
const CLI_PACKAGE_SPEC = `@codragraph/cli@${pkg.version}`;
/**
 * Resolve the absolute path to the `codragraph` binary if it's installed
 * globally (or via npm -g / bun -g / yarn global). Returns null when not found.
 *
 * Note: the npm package is `@codragraph/cli`, but the executable it installs
 * is `codragraph` (see package.json `bin`). PATH lookup must use the bin name.
 *
 * Windows specifics: `where codragraph` returns every PATH entry, in order:
 * the extensionless Unix shim npm creates first (a sh script — Node's
 * spawn/execFile cannot launch it on Windows), then `codragraph.cmd`,
 * then `codragraph.ps1`. We must pick the .cmd / .exe / .bat entry; writing
 * the extensionless path into a downstream MCP config produces a launcher
 * that fails on every spawn.
 */
function resolveCodragraphBin() {
    try {
        const cmd = process.platform === 'win32' ? 'where' : 'which';
        const stdout = execFileSync(cmd, ['codragraph'], {
            encoding: 'utf-8',
            timeout: 5000,
            stdio: ['ignore', 'pipe', 'ignore'],
        });
        const lines = stdout
            .split('\n')
            .map((l) => l.trim())
            .filter(Boolean);
        if (process.platform === 'win32') {
            // Prefer a Windows-executable shim. If none is on PATH, return null so
            // the caller falls through to the package-runner fallback.
            const exe = lines.find((l) => /\.(cmd|exe|bat)$/i.test(l));
            return exe ?? null;
        }
        return lines[0] ?? null;
    }
    catch {
        return null;
    }
}
/**
 * The MCP server entry for all editors.
 *
 * Prefers the globally-installed `codragraph` binary (starts in ~1 s) over
 * `npx` / `bunx` package execution (cold-cache install of native deps can take
 * >60 s, exceeding Claude Code's 30 s MCP connection timeout).
 *
 * Falls back to the package runner that invoked setup when the binary isn't on
 * PATH — e.g. first-time `npx @codragraph/cli` or `bunx @codragraph/cli` users.
 *
 * Windows note: even when the bin is on PATH, we launch via `cmd /c codragraph
 * mcp` rather than writing the resolved path. Reason: `where codragraph`
 * returns the extensionless Unix shim before `codragraph.cmd`, and Node's
 * spawn/execFile cannot launch the extensionless shim on Windows. Letting
 * cmd resolve via PATHEXT is the only reliable path that works for npm-,
 * bun-, pnpm-, and yarn-installed shims alike.
 */
function isRunningUnderBun() {
    const userAgent = (process.env.npm_config_user_agent ?? '').toLowerCase();
    const rawExecPath = process.env.npm_execpath ?? '';
    const execPath = process.platform === 'win32'
        ? path.win32.basename(rawExecPath).toLowerCase()
        : path.basename(rawExecPath).toLowerCase();
    return userAgent.startsWith('bun/') || execPath === 'bun' || execPath === 'bun.exe';
}
function getPackageRunnerMcpEntry() {
    if (isRunningUnderBun()) {
        if (process.platform === 'win32') {
            return {
                command: 'cmd',
                args: ['/c', 'bunx', CLI_PACKAGE_SPEC, 'mcp'],
            };
        }
        return {
            command: 'bunx',
            args: [CLI_PACKAGE_SPEC, 'mcp'],
        };
    }
    if (process.platform === 'win32') {
        return {
            command: 'cmd',
            args: ['/c', 'npx', '-y', CLI_PACKAGE_SPEC, 'mcp'],
        };
    }
    return {
        command: 'npx',
        args: ['-y', CLI_PACKAGE_SPEC, 'mcp'],
    };
}
function getMcpEntry() {
    const bin = resolveCodragraphBin();
    if (bin) {
        if (process.platform === 'win32') {
            return { command: 'cmd', args: ['/c', 'codragraph', 'mcp'] };
        }
        return { command: bin, args: ['mcp'] };
    }
    return getPackageRunnerMcpEntry();
}
/**
 * OpenCode uses a different MCP format: { type: "local", command: [...] }
 * where command is a flat array (command + args combined).
 */
function getOpenCodeMcpEntry() {
    const bin = resolveCodragraphBin();
    if (bin) {
        if (process.platform === 'win32') {
            return { type: 'local', command: ['cmd', '/c', 'codragraph', 'mcp'] };
        }
        return { type: 'local', command: [bin, 'mcp'] };
    }
    const entry = getPackageRunnerMcpEntry();
    return { type: 'local', command: [entry.command, ...entry.args] };
}
/**
 * Merge codragraph entry into an existing MCP config JSON object.
 * Returns the updated config.
 */
function mergeMcpConfig(existing) {
    if (!existing || typeof existing !== 'object') {
        existing = {};
    }
    if (!existing.mcpServers || typeof existing.mcpServers !== 'object') {
        existing.mcpServers = {};
    }
    existing.mcpServers.codragraph = getMcpEntry();
    return existing;
}
/**
 * Try to read a JSON file, returning null if it doesn't exist or is invalid.
 */
async function readJsonFile(filePath) {
    try {
        const raw = await fs.readFile(filePath, 'utf-8');
        return JSON.parse(raw);
    }
    catch {
        return null;
    }
}
/**
 * Write JSON to a file, creating parent directories if needed.
 */
async function writeJsonFile(filePath, data) {
    await fs.mkdir(path.dirname(filePath), { recursive: true });
    await fs.writeFile(filePath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
}
/**
 * Detect indentation style from file content.
 * Returns formatting options matching the file's existing style.
 */
function detectIndentation(raw) {
    const firstIndented = raw.match(/^( +|\t)/m);
    if (!firstIndented)
        return { tabSize: 2, insertSpaces: true };
    if (firstIndented[1] === '\t')
        return { tabSize: 1, insertSpaces: false };
    return { tabSize: firstIndented[1].length, insertSpaces: true };
}
/**
 * Merge a key/value pair into a JSONC config file, preserving comments and formatting.
 * If the file is genuinely corrupt (not valid JSONC), leaves it untouched.
 */
async function mergeJsoncFile(filePath, keyPath, value) {
    let raw;
    try {
        raw = await fs.readFile(filePath, 'utf-8');
    }
    catch {
        raw = '';
    }
    if (raw.trim().length === 0) {
        const config = {};
        let parent = config;
        for (let i = 0; i < keyPath.length; i++) {
            if (i === keyPath.length - 1) {
                parent[keyPath[i]] = value;
            }
            else {
                parent[keyPath[i]] = {};
                parent = parent[keyPath[i]];
            }
        }
        await writeJsonFile(filePath, config);
        return true;
    }
    const parseErrors = [];
    const tree = parseTree(raw, parseErrors);
    if (tree && tree.type === 'object' && parseErrors.length === 0) {
        const formattingOptions = detectIndentation(raw);
        const edits = modify(raw, keyPath, value, { formattingOptions });
        const result = applyEdits(raw, edits);
        await fs.writeFile(filePath, result, 'utf-8');
        return true;
    }
    return false;
}
/**
 * Check if a directory exists
 */
async function dirExists(dirPath) {
    try {
        const stat = await fs.stat(dirPath);
        return stat.isDirectory();
    }
    catch {
        return false;
    }
}
// ─── Editor-specific setup ─────────────────────────────────────────
async function setupCursor(result) {
    const cursorDir = path.join(os.homedir(), '.cursor');
    if (!(await dirExists(cursorDir))) {
        result.skipped.push('Cursor (not installed)');
        return;
    }
    const mcpPath = path.join(cursorDir, 'mcp.json');
    try {
        const existing = await readJsonFile(mcpPath);
        const updated = mergeMcpConfig(existing);
        await writeJsonFile(mcpPath, updated);
        result.configured.push('Cursor');
    }
    catch (err) {
        result.errors.push(`Cursor: ${err.message}`);
    }
}
async function setupClaudeCode(result) {
    const claudeDir = path.join(os.homedir(), '.claude');
    if (!(await dirExists(claudeDir))) {
        result.skipped.push('Claude Code (not installed)');
        return;
    }
    // Claude Code stores MCP config in ~/.claude.json
    const mcpPath = path.join(os.homedir(), '.claude.json');
    try {
        const existing = await readJsonFile(mcpPath);
        const updated = mergeMcpConfig(existing);
        await writeJsonFile(mcpPath, updated);
        result.configured.push('Claude Code');
    }
    catch (err) {
        result.errors.push(`Claude Code: ${err.message}`);
    }
}
/**
 * Install CodraGraph skills to ~/.claude/skills/ for Claude Code.
 */
async function installClaudeCodeSkills(result) {
    const claudeDir = path.join(os.homedir(), '.claude');
    if (!(await dirExists(claudeDir)))
        return;
    const skillsDir = path.join(claudeDir, 'skills');
    try {
        const installed = await installSkillsTo(skillsDir);
        if (installed.length > 0) {
            result.configured.push(`Claude Code skills (${installed.length} skills → ~/.claude/skills/)`);
        }
    }
    catch (err) {
        result.errors.push(`Claude Code skills: ${err.message}`);
    }
}
/**
 * Install CodraGraph hooks to ~/.claude/settings.json for Claude Code.
 * Merges hook config without overwriting existing hooks.
 */
async function installClaudeCodeHooks(result) {
    const claudeDir = path.join(os.homedir(), '.claude');
    if (!(await dirExists(claudeDir)))
        return;
    const settingsPath = path.join(claudeDir, 'settings.json');
    // Source hooks bundled within the codragraph package (hooks/claude/)
    const pluginHooksPath = path.join(__dirname, '..', '..', 'hooks', 'claude');
    // Copy unified hook script to ~/.claude/hooks/codragraph/
    const destHooksDir = path.join(claudeDir, 'hooks', 'codragraph');
    try {
        await fs.mkdir(destHooksDir, { recursive: true });
        const src = path.join(pluginHooksPath, 'codragraph-hook.cjs');
        const dest = path.join(destHooksDir, 'codragraph-hook.cjs');
        try {
            let content = await fs.readFile(src, 'utf-8');
            // Inject resolved CLI path so the copied hook can find the CLI
            // even when it's no longer inside the npm package tree
            const resolvedCli = path.join(__dirname, '..', 'cli', 'index.js');
            const normalizedCli = path.resolve(resolvedCli).replace(/\\/g, '/');
            const jsonCli = JSON.stringify(normalizedCli);
            content = content.replace("let cliPath = path.resolve(__dirname, '..', '..', 'dist', 'cli', 'index.js');", `let cliPath = ${jsonCli};`);
            await fs.writeFile(dest, content, 'utf-8');
        }
        catch {
            // Script not found in source — skip
        }
        const hookPath = path.join(destHooksDir, 'codragraph-hook.cjs').replace(/\\/g, '/');
        const hookCmd = `node "${hookPath.replace(/"/g, '\\"')}"`;
        // Merge hook config into ~/.claude/settings.json
        const existing = (await readJsonFile(settingsPath)) || {};
        if (!existing.hooks)
            existing.hooks = {};
        function ensureHookEntry(eventName, matcher, timeout, statusMessage) {
            if (!existing.hooks[eventName])
                existing.hooks[eventName] = [];
            const hasHook = existing.hooks[eventName].some((h) => h.hooks?.some((hh) => hh.command?.includes('codragraph-hook')));
            if (!hasHook) {
                existing.hooks[eventName].push({
                    matcher,
                    hooks: [{ type: 'command', command: hookCmd, timeout, statusMessage }],
                });
            }
        }
        ensureHookEntry('PreToolUse', 'Grep|Glob|Bash', 10, 'Enriching with CodraGraph graph context...');
        ensureHookEntry('PostToolUse', 'Bash', 10, 'Checking CodraGraph index freshness...');
        await writeJsonFile(settingsPath, existing);
        result.configured.push('Claude Code hooks (PreToolUse, PostToolUse)');
    }
    catch (err) {
        result.errors.push(`Claude Code hooks: ${err.message}`);
    }
}
async function setupOpenCode(result) {
    const opencodeDir = path.join(os.homedir(), '.config', 'opencode');
    if (!(await dirExists(opencodeDir))) {
        result.skipped.push('OpenCode (not installed)');
        return;
    }
    const configPath = path.join(opencodeDir, 'opencode.json');
    try {
        const ok = await mergeJsoncFile(configPath, ['mcp', 'codragraph'], getOpenCodeMcpEntry());
        if (ok) {
            result.configured.push('OpenCode');
        }
        else {
            result.errors.push('OpenCode: opencode.json is corrupt — skipping to preserve existing content');
        }
    }
    catch (err) {
        result.errors.push(`OpenCode: ${err.message}`);
    }
}
/**
 * Build a TOML section for Codex MCP config (~/.codex/config.toml).
 */
function getCodexMcpTomlSection() {
    const entry = getMcpEntry();
    const command = JSON.stringify(entry.command);
    const args = `[${entry.args.map((arg) => JSON.stringify(arg)).join(', ')}]`;
    return `[mcp_servers.codragraph]\ncommand = ${command}\nargs = ${args}\n`;
}
/**
 * Append CodraGraph MCP server config to Codex's config.toml if missing.
 */
async function upsertCodexConfigToml(configPath) {
    let existing = '';
    try {
        existing = await fs.readFile(configPath, 'utf-8');
    }
    catch {
        existing = '';
    }
    if (existing.includes('[mcp_servers.codragraph]')) {
        return;
    }
    const section = getCodexMcpTomlSection();
    const nextContent = existing.trim().length > 0 ? `${existing.trimEnd()}\n\n${section}` : section;
    await fs.mkdir(path.dirname(configPath), { recursive: true });
    await fs.writeFile(configPath, `${nextContent.trimEnd()}\n`, 'utf-8');
}
async function setupCodex(result) {
    const codexDir = path.join(os.homedir(), '.codex');
    if (!(await dirExists(codexDir))) {
        result.skipped.push('Codex (not installed)');
        return;
    }
    try {
        const entry = getMcpEntry();
        // On Windows, npm-installed CLIs ship as .cmd shims that Node's execFile
        // can only invoke when the file extension is explicit (no PATHEXT lookup).
        const codexBin = process.platform === 'win32' ? 'codex.cmd' : 'codex';
        await execFileAsync(codexBin, ['mcp', 'add', 'codragraph', '--', entry.command, ...entry.args]);
        result.configured.push('Codex');
        return;
    }
    catch {
        // Fallback for environments where `codex` binary isn't on PATH.
    }
    try {
        const configPath = path.join(codexDir, 'config.toml');
        await upsertCodexConfigToml(configPath);
        result.configured.push('Codex (MCP added to ~/.codex/config.toml)');
    }
    catch (err) {
        result.errors.push(`Codex: ${err.message}`);
    }
}
// ─── Skill Installation ───────────────────────────────────────────
/**
 * Install CodraGraph skills to a target directory.
 * Each skill is installed as {targetDir}/codragraph-{skillName}/SKILL.md
 * following the Agent Skills standard (Cursor, Claude Code, and Codex).
 *
 * Supports two source layouts:
 *   - Flat file:  skills/{name}.md           → copied as SKILL.md
 *   - Directory:  skills/{name}/SKILL.md     → copied recursively (includes references/, etc.)
 */
async function installSkillsTo(targetDir) {
    const installed = [];
    const skillsRoot = path.join(__dirname, '..', '..', 'skills');
    let flatFiles = [];
    let dirSkillFiles = [];
    try {
        [flatFiles, dirSkillFiles] = await Promise.all([
            glob('*.md', { cwd: skillsRoot }),
            glob('*/SKILL.md', { cwd: skillsRoot }),
        ]);
    }
    catch {
        return [];
    }
    const skillSources = new Map();
    for (const relPath of dirSkillFiles) {
        skillSources.set(path.dirname(relPath), { isDirectory: true });
    }
    for (const relPath of flatFiles) {
        const skillName = path.basename(relPath, '.md');
        if (!skillSources.has(skillName)) {
            skillSources.set(skillName, { isDirectory: false });
        }
    }
    for (const [skillName, source] of skillSources) {
        const skillDir = path.join(targetDir, skillName);
        try {
            if (source.isDirectory) {
                const dirSource = path.join(skillsRoot, skillName);
                await copyDirRecursive(dirSource, skillDir);
                installed.push(skillName);
            }
            else {
                const flatSource = path.join(skillsRoot, `${skillName}.md`);
                const content = await fs.readFile(flatSource, 'utf-8');
                await fs.mkdir(skillDir, { recursive: true });
                await fs.writeFile(path.join(skillDir, 'SKILL.md'), content, 'utf-8');
                installed.push(skillName);
            }
        }
        catch {
            // Source skill not found — skip
        }
    }
    return installed;
}
/**
 * Recursively copy a directory tree.
 */
async function copyDirRecursive(src, dest) {
    await fs.mkdir(dest, { recursive: true });
    const entries = await fs.readdir(src, { withFileTypes: true });
    for (const entry of entries) {
        const srcPath = path.join(src, entry.name);
        const destPath = path.join(dest, entry.name);
        if (entry.isDirectory()) {
            await copyDirRecursive(srcPath, destPath);
        }
        else {
            await fs.copyFile(srcPath, destPath);
        }
    }
}
/**
 * Install global Cursor skills to ~/.cursor/skills/codragraph/
 */
async function installCursorSkills(result) {
    const cursorDir = path.join(os.homedir(), '.cursor');
    if (!(await dirExists(cursorDir)))
        return;
    const skillsDir = path.join(cursorDir, 'skills');
    try {
        const installed = await installSkillsTo(skillsDir);
        if (installed.length > 0) {
            result.configured.push(`Cursor skills (${installed.length} skills → ~/.cursor/skills/)`);
        }
    }
    catch (err) {
        result.errors.push(`Cursor skills: ${err.message}`);
    }
}
/**
 * Install global OpenCode skills to ~/.config/opencode/skill/codragraph/
 */
async function installOpenCodeSkills(result) {
    const opencodeDir = path.join(os.homedir(), '.config', 'opencode');
    if (!(await dirExists(opencodeDir)))
        return;
    const skillsDir = path.join(opencodeDir, 'skill');
    try {
        const installed = await installSkillsTo(skillsDir);
        if (installed.length > 0) {
            result.configured.push(`OpenCode skills (${installed.length} skills → ~/.config/opencode/skill/)`);
        }
    }
    catch (err) {
        result.errors.push(`OpenCode skills: ${err.message}`);
    }
}
/**
 * Install global Codex skills to ~/.agents/skills/codragraph/
 */
async function installCodexSkills(result) {
    const codexDir = path.join(os.homedir(), '.codex');
    if (!(await dirExists(codexDir)))
        return;
    const skillsDir = path.join(os.homedir(), '.agents', 'skills');
    try {
        const installed = await installSkillsTo(skillsDir);
        if (installed.length > 0) {
            result.configured.push(`Codex skills (${installed.length} skills → ~/.agents/skills/)`);
        }
    }
    catch (err) {
        result.errors.push(`Codex skills: ${err.message}`);
    }
}
export const runSetup = async (options = {}) => {
    if (options.compactHeader) {
        console.log('  CodraGraph: first-run editor setup');
        console.log('');
    }
    else {
        console.log('');
        console.log('  CodraGraph Setup');
        console.log('  ==============');
        console.log('');
    }
    // Ensure global directory exists
    const globalDir = getGlobalDir();
    await fs.mkdir(globalDir, { recursive: true });
    const result = {
        configured: [],
        skipped: [],
        errors: [],
    };
    // Detect and configure each editor's MCP
    await setupCursor(result);
    await setupClaudeCode(result);
    await setupOpenCode(result);
    await setupCodex(result);
    // Install global skills for platforms that support them
    await installClaudeCodeSkills(result);
    await installClaudeCodeHooks(result);
    await installCursorSkills(result);
    await installOpenCodeSkills(result);
    await installCodexSkills(result);
    // Print results
    if (result.configured.length > 0) {
        console.log('  Configured:');
        for (const name of result.configured) {
            console.log(`    + ${name}`);
        }
    }
    if (result.skipped.length > 0) {
        console.log('');
        console.log('  Skipped:');
        for (const name of result.skipped) {
            console.log(`    - ${name}`);
        }
    }
    if (result.errors.length > 0) {
        console.log('');
        console.log('  Errors:');
        for (const err of result.errors) {
            console.log(`    ! ${err}`);
        }
    }
    console.log('');
    console.log('  Summary:');
    console.log(`    MCP configured for: ${result.configured.filter((c) => !c.includes('skills')).join(', ') || 'none'}`);
    console.log(`    Skills installed to: ${result.configured.filter((c) => c.includes('skills')).length > 0 ? result.configured.filter((c) => c.includes('skills')).join(', ') : 'none'}`);
    if (!options.skipNextSteps) {
        console.log('');
        console.log('  Next steps:');
        console.log('    1. cd into any git repo');
        console.log('    2. Run: codragraph analyze');
        console.log('    3. Open the repo in your editor — MCP is ready!');
    }
    console.log('');
    return result;
};
export const setupCommand = async () => {
    await runSetup();
};
