/**
 * Nexus Prime — Install Wizard
 * Auto-detects installed IDEs and writes MCP configuration for each.
 * Invoked via: nexus-prime setup
 */
import { createHash } from 'crypto';
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'fs';
import { homedir } from 'os';
import { fileURLToPath } from 'url';
import { dirname, join, resolve } from 'path';
import { detectInstalledIDEs, getMcpConfigForIDE } from '../agents/adapters/ide-compat.js';
import { DEFAULT_DAEMON_READY_TIMEOUT_MS, ensureDaemonReady } from '../daemon/client.js';
import { getSetupDefinition, installSetup } from '../engines/client-bootstrap.js';
import { buildNexusMcpServerConfig } from '../engines/mcp-entrypoint.js';
import { resolveNexusStateDir } from '../engines/runtime-registry.js';
import { resolveWorkspaceContext } from '../engines/workspace-resolver.js';
import { runPostinstallCleanup } from '../postinstall/cleanup.js';
import { openBrowser, printHandoffBanner, withSpinner } from './interactive-setup.js';
import { appendToManifest, detectArchitectureUpgrade, loadManifest, recordArchitectureGeneration, recordPath, recordRegistration, recordSetupMarker, INSTALL_ARCH_GENERATION, } from '../install/manifest.js';
import { enumerateNgramArchives } from '../install/state-locator.js';
import { NEXUS_HOOK_COMMAND_MARKER, writeNexusClaudeCodeHooks } from '../install/claude-code-hooks.js';
/** Compute sha256 of a file's content. Returns null if the file cannot be read. */
export function computeFileHash(filePath) {
    try {
        return createHash('sha256').update(readFileSync(filePath, 'utf8'), 'utf8').digest('hex');
    }
    catch {
        return null;
    }
}
export const SETUP_MARKER_VERSION = '6.0.1';
const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
const SETUP_MARKER_PATH = join(resolveNexusStateDir(), 'setup-marker-v6.json');
const SETUP_CLIENT_BY_IDE = {
    'claude-code': 'claude-code',
    cursor: 'cursor',
    windsurf: 'windsurf',
    continue: 'continue',
    cline: 'cline',
    openclaw: 'openclaw',
    hermes: 'hermes',
    nanoclaw: 'nanoclaw',
    picoclaw: 'picoclaw',
    codex: 'codex',
};
function resolveSetupWorkspaceRoot(explicitWorkspaceRoot) {
    return resolveWorkspaceContext({
        workspaceRoot: explicitWorkspaceRoot,
        cwd: process.cwd(),
        env: process.env,
    }).workspaceRoot;
}
function isSetupMarker(value) {
    if (!value || typeof value !== 'object') {
        return false;
    }
    const marker = value;
    return typeof marker.version === 'string'
        && Array.isArray(marker.configuredIDEs)
        && typeof marker.setupCompletedAt === 'number';
}
export function readSetupMarker(markerPath = SETUP_MARKER_PATH) {
    if (!existsSync(markerPath)) {
        return null;
    }
    try {
        const parsed = JSON.parse(readFileSync(markerPath, 'utf8'));
        if (!isSetupMarker(parsed)) {
            return null;
        }
        const writtenHash = parsed.writtenHash && typeof parsed.writtenHash === 'object' && !Array.isArray(parsed.writtenHash)
            ? Object.fromEntries(Object.entries(parsed.writtenHash)
                .filter(([, v]) => typeof v === 'string')
                .map(([k, v]) => [k, v]))
            : undefined;
        return {
            version: parsed.version,
            configuredIDEs: [...new Set(parsed.configuredIDEs.filter((ide) => typeof ide === 'string'))],
            setupCompletedAt: parsed.setupCompletedAt,
            writtenHash,
        };
    }
    catch {
        return null;
    }
}
export function writeSetupMarker(marker, markerPath = SETUP_MARKER_PATH) {
    const nextMarker = {
        version: marker.version,
        configuredIDEs: [...new Set(marker.configuredIDEs)],
        setupCompletedAt: marker.setupCompletedAt,
        writtenHash: marker.writtenHash,
    };
    mkdirSync(dirname(markerPath), { recursive: true });
    writeFileSync(markerPath, JSON.stringify(nextMarker, null, 2), 'utf8');
    appendToManifest((m) => {
        const withMarker = recordSetupMarker(m, markerPath);
        return recordPath(withMarker, { path: markerPath, kind: 'file', scope: 'state' });
    });
    return nextMarker;
}
export async function configureIDE(ide, opts = {}) {
    const workspaceRoot = resolveSetupWorkspaceRoot(opts.workspaceRoot);
    const mappedClient = SETUP_CLIENT_BY_IDE[ide];
    if (mappedClient) {
        const definition = getSetupDefinition(mappedClient, {
            packageRoot: PACKAGE_ROOT,
            workspaceRoot,
        });
        if (!opts.dryRun) {
            installSetup(definition, {
                scope: 'all',
                silent: opts.verbose === false,
                workspaceRoot,
            });
            // Write Claude Code hooks alongside the MCP entry (idempotent merge).
            // Write to both workspace and global user settings so desktop-mode
            // Claude Code (which reads ~/.claude/settings.json) also auto-bootstraps.
            if (ide === 'claude-code') {
                _writeClaudeCodeHooks(workspaceRoot);
                _writeClaudeCodeHooks(homedir());
            }
            // Write workspace-local configs for other IDEs that may be present
            _writeWorkspaceLocalConfigs(ide, workspaceRoot);
        }
        const configPath = definition.configPath ?? null;
        const writtenHash = configPath ? computeFileHash(configPath) : null;
        return { configPath, writtenHash };
    }
    const { configPath, content, merge } = getMcpConfigForIDE(ide, workspaceRoot);
    if (!configPath) {
        return { configPath: null, writtenHash: null };
    }
    if (!opts.dryRun) {
        const dir = dirname(configPath);
        if (!existsSync(dir)) {
            mkdirSync(dir, { recursive: true });
        }
        let finalContent = content;
        if (merge && existsSync(configPath)) {
            finalContent = mergeIntoExistingConfig(configPath, content, ide);
        }
        writeFileSync(configPath, finalContent, 'utf8');
        _writeWorkspaceLocalConfigs(ide, workspaceRoot);
    }
    const writtenHash = computeFileHash(configPath);
    return { configPath, writtenHash };
}
/** Nexus Prime MCP server entry used in workspace-local config writes. */
function _nexusMcpEntry(workspaceRoot) {
    return buildNexusMcpServerConfig(workspaceRoot);
}
/**
 * Write Claude Code PreToolUse/PostToolUse/UserPromptSubmit/Stop hooks to
 * `workspaceRoot/.claude/settings.json`. Idempotent — removes stale nexus-prime
 * entries before re-inserting so we never accumulate duplicates.
 */
function _writeClaudeCodeHooks(workspaceRoot) {
    const settingsPath = join(workspaceRoot, '.claude', 'settings.json');
    // Single source of truth: hook spec + dedup logic live in install/claude-code-hooks.ts.
    writeNexusClaudeCodeHooks(settingsPath);
    const isHome = workspaceRoot === homedir();
    appendToManifest((m) => {
        const withPath = recordPath(m, { path: settingsPath, kind: 'file', scope: 'state' });
        return recordRegistration(withPath, {
            target: isHome ? 'claude-home' : 'claude-workspace',
            filePath: settingsPath,
            jsonPath: ['hooks'],
            entryMarker: NEXUS_HOOK_COMMAND_MARKER,
        });
    });
}
function _isManagedGraphPeer(server) {
    if (!server || typeof server !== 'object' || Array.isArray(server))
        return false;
    const record = server;
    const args = Array.isArray(record.args) ? record.args.map(String) : [];
    return record.command === 'goatlas' && args.length === 1 && args[0] === 'mcp';
}
function _stripInternalGraphPeers(mcpServers) {
    if (!mcpServers)
        return;
    delete mcpServers['code-review-graph'];
    if (_isManagedGraphPeer(mcpServers['atlas'])) {
        delete mcpServers['atlas'];
    }
}
/**
 * Write workspace-local MCP config files for IDEs whose workspace directories
 * already exist (`.vscode/`, `.cursor/`). Non-fatal; errors are swallowed.
 */
function _writeWorkspaceLocalConfigs(callerIde, workspaceRoot) {
    const entry = _nexusMcpEntry(workspaceRoot);
    // .vscode/settings.json — for VS Code-based extensions (Cline, Roo, etc.)
    const vscodePath = join(workspaceRoot, '.vscode');
    if (existsSync(vscodePath)) {
        const settingsPath = join(vscodePath, 'settings.json');
        try {
            let settings = {};
            if (existsSync(settingsPath)) {
                try {
                    settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
                }
                catch { /* ok */ }
            }
            const servers = (settings.mcpServers && typeof settings.mcpServers === 'object' && !Array.isArray(settings.mcpServers))
                ? settings.mcpServers
                : {};
            servers['nexus-prime'] = entry;
            _stripInternalGraphPeers(servers);
            settings.mcpServers = servers;
            writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf8');
            appendToManifest((m) => {
                const withPath = recordPath(m, { path: settingsPath, kind: 'file', scope: 'state' });
                return recordRegistration(withPath, {
                    target: 'vscode-workspace',
                    filePath: settingsPath,
                    jsonPath: ['mcpServers', 'nexus-prime'],
                });
            });
        }
        catch { /* non-fatal */ }
    }
    // .cursor/mcp.json — Cursor workspace MCP config (skip if cursor is the primary IDE
    // being configured here, since its main config is already handled)
    if (callerIde !== 'cursor') {
        const cursorPath = join(workspaceRoot, '.cursor');
        if (existsSync(cursorPath)) {
            const mcpPath = join(cursorPath, 'mcp.json');
            try {
                let mcp = {};
                if (existsSync(mcpPath)) {
                    try {
                        mcp = JSON.parse(readFileSync(mcpPath, 'utf8'));
                    }
                    catch { /* ok */ }
                }
                const servers = (mcp.mcpServers && typeof mcp.mcpServers === 'object' && !Array.isArray(mcp.mcpServers))
                    ? mcp.mcpServers
                    : {};
                servers['nexus-prime'] = entry;
                _stripInternalGraphPeers(servers);
                mcp.mcpServers = servers;
                writeFileSync(mcpPath, JSON.stringify(mcp, null, 2), 'utf8');
                appendToManifest((m) => {
                    const withPath = recordPath(m, { path: mcpPath, kind: 'file', scope: 'state' });
                    return recordRegistration(withPath, {
                        target: 'cursor-workspace',
                        filePath: mcpPath,
                        jsonPath: ['mcpServers', 'nexus-prime'],
                    });
                });
            }
            catch { /* non-fatal */ }
        }
    }
}
export function runArchitectureUpgrade(opts = {}) {
    const manifest = loadManifest();
    const detection = detectArchitectureUpgrade(manifest);
    if (!detection.requiresMigration) {
        if (manifest.architectureGeneration !== INSTALL_ARCH_GENERATION && !opts.dryRun) {
            // Fresh install or no recorded entries — just stamp the marker.
            appendToManifest((m) => recordArchitectureGeneration(m));
        }
        return {
            previousGeneration: detection.previousGeneration,
            currentGeneration: detection.currentGeneration,
            migrated: false,
            ngramArchivesPruned: 0,
        };
    }
    let ngramArchivesPruned = 0;
    if (!opts.dryRun) {
        // Stale ngram archives from prior generations: keep nothing — the new
        // rotation path archives at most one and uses footprint accounting.
        try {
            const archives = enumerateNgramArchives();
            for (const archive of archives) {
                try {
                    rmSync(archive.path, { force: true });
                    ngramArchivesPruned += 1;
                }
                catch { /* best-effort */ }
            }
        }
        catch { /* best-effort */ }
        appendToManifest((m) => recordArchitectureGeneration(m));
    }
    return {
        previousGeneration: detection.previousGeneration,
        currentGeneration: detection.currentGeneration,
        migrated: true,
        ngramArchivesPruned,
    };
}
/** Run the install wizard: detect IDEs and write MCP configs. */
export async function runInstallWizard(opts = {}) {
    const workspaceRoot = resolveSetupWorkspaceRoot(opts.workspaceRoot);
    const verbose = opts.verbose ?? true;
    const dryRun = opts.dryRun ?? false;
    const setupMarker = readSetupMarker();
    const log = (msg) => { if (verbose)
        process.stdout.write(msg + '\n'); };
    const upgrade = runArchitectureUpgrade({ dryRun });
    if (upgrade.migrated && verbose) {
        log(`  [arch] migrated install ${upgrade.previousGeneration ?? 'legacy'} → ${upgrade.currentGeneration} (ngram archives pruned: ${upgrade.ngramArchivesPruned})`);
    }
    const detected = detectInstalledIDEs(workspaceRoot);
    if (detected.length === 0) {
        log('  No supported IDEs detected in this workspace. Skipping MCP config setup.');
        log('  Manually add nexus-prime to your IDE\'s MCP config using:');
        log('    nexus-prime mcp  (prints MCP server config)');
        return { configured: [], skipped: [], errors: [] };
    }
    log(`  Detected IDEs: ${detected.join(', ')}`);
    const result = { configured: [], skipped: [], errors: [] };
    const alreadyConfigured = new Set(setupMarker?.configuredIDEs ?? []);
    const hashes = { ...(setupMarker?.writtenHash ?? {}) };
    for (const ide of detected) {
        if (alreadyConfigured.has(ide)) {
            result.skipped.push(ide);
            log(`  [skip] ${ide}: already configured`);
            continue;
        }
        try {
            const { configPath, writtenHash } = await configureIDE(ide, {
                workspaceRoot,
                verbose,
                dryRun,
            });
            if (!configPath) {
                result.skipped.push(ide);
                log(`  [skip] ${ide}: no config path determined`);
                continue;
            }
            if (dryRun) {
                log(`  [dry-run] Would write ${ide} MCP config to: ${configPath}`);
                result.configured.push(ide);
                continue;
            }
            if (writtenHash !== null) {
                hashes[ide] = writtenHash;
            }
            result.configured.push(ide);
            log(`  [\x1b[32m✓\x1b[0m] ${ide}: MCP config written → ${configPath}`);
        }
        catch (err) {
            const reason = err instanceof Error ? err.message : String(err);
            result.errors.push({ ide, reason });
            log(`  [\x1b[31m✗\x1b[0m] ${ide}: ${reason}`);
        }
    }
    if (!dryRun && (result.configured.length > 0 || setupMarker)) {
        writeSetupMarker({
            version: SETUP_MARKER_VERSION,
            configuredIDEs: [...alreadyConfigured, ...result.configured],
            setupCompletedAt: Date.now(),
            writtenHash: hashes,
        });
    }
    return result;
}
/**
 * Attempt to merge nexus-prime MCP entry into an existing config file.
 * Falls back to returning `newContent` if the existing file cannot be parsed.
 */
function mergeIntoExistingConfig(configPath, newContent, ide) {
    try {
        const existing = JSON.parse(readFileSync(configPath, 'utf8'));
        const incoming = JSON.parse(newContent);
        if (ide === 'cursor' || ide === 'windsurf' || ide === 'continue' || ide === 'openclaw' || ide === 'hermes' || ide === 'nanoclaw' || ide === 'picoclaw') {
            // These use { mcpServers: { "nexus-prime": {...} } }
            existing.mcpServers = {
                ...(existing.mcpServers ?? {}),
                ...(incoming.mcpServers ?? {}),
            };
            _stripInternalGraphPeers(existing.mcpServers);
            return JSON.stringify(existing, null, 2);
        }
        if (ide === 'claude-code') {
            // Claude Code settings use { mcpServers: [...] } array or object
            if (Array.isArray(existing.mcpServers)) {
                const already = existing.mcpServers.some((s) => s?.name === 'nexus-prime');
                if (!already) {
                    existing.mcpServers.push(...(incoming.mcpServers ?? []));
                }
            }
            else {
                existing.mcpServers = {
                    ...(existing.mcpServers ?? {}),
                    ...(incoming.mcpServers ?? {}),
                };
                _stripInternalGraphPeers(existing.mcpServers);
            }
            return JSON.stringify(existing, null, 2);
        }
    }
    catch {
        // Parse failed — return new content as-is
    }
    return newContent;
}
/** Print how to add nexus-prime manually to an MCP client. */
export function printManualMcpInstructions() {
    const server = buildNexusMcpServerConfig(process.cwd());
    const snippet = JSON.stringify({
        mcpServers: {
            'nexus-prime': server,
        },
    }, null, 2);
    process.stdout.write('\n  Add this to your IDE\'s MCP configuration:\n\n');
    process.stdout.write(snippet.split('\n').map(l => `    ${l}`).join('\n') + '\n\n');
    process.stdout.write('  Or run:  nexus-prime setup\n\n');
}
/** Entry point for CLI: nexus-prime install / setup auto */
export async function cliSetup(opts = []) {
    // Backwards-compat: accept string[] args as before
    const args = Array.isArray(opts) ? opts : [];
    const options = Array.isArray(opts)
        ? { dryRun: args.includes('--dry-run'), verbose: !args.includes('--silent') }
        : opts;
    const dryRun = options.dryRun ?? false;
    const verbose = options.verbose ?? true;
    const port = process.env.NEXUS_DASHBOARD_PORT ?? '3377';
    const dashUrl = `http://localhost:${port}`;
    const isNew = options.isNewUser ?? false;
    const workspaceRoot = resolveSetupWorkspaceRoot();
    const ANSI = { cyan: '\x1b[36m', green: '\x1b[32m', yellow: '\x1b[33m', dim: '\x1b[2m', reset: '\x1b[0m' };
    const isTTY = process.stdout.isTTY === true && !process.env.NO_COLOR;
    const c = (s, k) => isTTY ? `${ANSI[k]}${s}${ANSI.reset}` : s;
    const log = (msg) => { if (verbose)
        process.stdout.write(msg + '\n'); };
    const step = (n, t, label) => log(`${c(`◇ Step ${n}/${t}`, 'yellow')} · ${label}`);
    log('');
    log(c('◆ Nexus Prime Setup', 'cyan'));
    const cleanup = await runPostinstallCleanup({ dryRun, silent: true });
    if (cleanup.staleLockfilesRemoved + cleanup.staleDaemonsKilled > 0) {
        log(`  ${c('↺', 'dim')} Cleaned ${cleanup.staleLockfilesRemoved} lockfiles, stopped ${cleanup.staleDaemonsKilled} stale daemons`);
    }
    // Step 1 — detect + configure IDEs (per-IDE animated checklist)
    const totalSteps = options.licenseKey ? 5 : 4;
    step(1, totalSteps, 'Detecting IDEs…');
    // Emit install.step event helper (best-effort; daemon may not be running yet)
    const emitInstallStep = (stepN, total, label, status, detail) => {
        try {
            import('../engines/event-bus.js').then(({ nexusEventBus }) => {
                nexusEventBus.emit('install.step', { step: stepN, total, label, status, detail });
            }).catch(() => { });
        }
        catch { /* non-fatal */ }
    };
    const result = await runInstallWizard({ workspaceRoot, dryRun, verbose: false });
    const allIDEs = [...result.configured, ...result.skipped, ...result.errors.map(e => e.ide)];
    if (allIDEs.length > 0) {
        for (const ide of allIDEs) {
            const isConfigured = result.configured.includes(ide);
            const isSkipped = result.skipped.includes(ide);
            const errEntry = result.errors.find(e => e.ide === ide);
            if (isConfigured) {
                await withSpinner(`${ide} · configuring`, Promise.resolve());
                emitInstallStep(1, totalSteps, ide, 'ok', 'MCP config written');
            }
            else if (isSkipped) {
                log(`  ${c('─', 'dim')} ${c(ide, 'dim')} already set up`);
                emitInstallStep(1, totalSteps, ide, 'skip', 'already configured');
            }
            else if (errEntry) {
                log(`  ${c('✗', 'yellow')} ${c(ide, 'yellow')} · ${errEntry.reason}`);
                emitInstallStep(1, totalSteps, ide, 'fail', errEntry.reason);
            }
        }
    }
    else {
        log(`  ${c('—', 'dim')} No IDEs auto-detected. Run: nexus-prime setup <ide>`);
    }
    // Step 2 — MCP configs confirmed
    step(2, totalSteps, 'MCP configs written.');
    // Optional license activation step
    let licStepBase = 2;
    if (options.licenseKey) {
        licStepBase = 3;
        step(3, totalSteps, 'Activating license…');
        if (!dryRun) {
            try {
                const licRes = await fetch(`${dashUrl}/api/license/activate`, {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ key: options.licenseKey }),
                    signal: AbortSignal.timeout(8_000),
                });
                if (licRes.ok) {
                    const data = await licRes.json();
                    log(`  ${c('✓', 'green')} License activated · ${c(data?.tier?.toUpperCase() ?? 'PRO', 'yellow')} plan`);
                }
                else {
                    log(`  ${c('⚠', 'yellow')} License activation returned ${licRes.status} — check key and retry: nexus-prime license activate <key>`);
                }
            }
            catch {
                log(`  ${c('—', 'dim')} License activation skipped (daemon not ready yet) — retry: nexus-prime license activate <key>`);
            }
        }
        else {
            log(`  ${c('[dry-run]', 'dim')} Would activate license key`);
        }
    }
    // Step daemon
    const daemonStep = licStepBase + 1;
    step(daemonStep, totalSteps, 'Starting daemon…');
    let daemonReady = false;
    if (!dryRun) {
        try {
            const workspace = resolveWorkspaceContext({ workspaceRoot, cwd: process.cwd(), env: process.env });
            await withSpinner('Connecting to daemon', ensureDaemonReady(workspace, { timeoutMs: DEFAULT_DAEMON_READY_TIMEOUT_MS, entrypoint: process.argv[1] }));
            daemonReady = true;
            log(`  ${c('✓', 'green')} Dashboard: ${c(dashUrl, 'cyan')}`);
        }
        catch (err) {
            log(`  ${c('✗', 'yellow')} Daemon start failed: ${err instanceof Error ? err.message : String(err)}`);
            log(`    Start manually: ${c('nexus-prime daemon', 'dim')}`);
        }
    }
    else {
        log(`  ${c('[dry-run]', 'dim')} Would start daemon → ${dashUrl}`);
    }
    // Step hire
    const hireStep = daemonStep + 1;
    step(hireStep, totalSteps, 'Hiring your first specialist…');
    if (!dryRun && daemonReady && process.env.NEXUS_AUTO_HIRE_STARTER !== '0') {
        try {
            const hireRes = await fetch(`${dashUrl}/api/synapse/hire`, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ specialistId: 'engineering.rapid-prototyper', budgetCapUsd: 1, name: 'Rapid Prototyper' }),
                signal: AbortSignal.timeout(5_000),
            });
            if (hireRes.ok) {
                const op = await hireRes.json();
                const opId = op?.operative?.id ?? op?.id ?? 'op_???';
                log(`  ${c('✓', 'green')} Operative ${c(String(opId), 'cyan')} · rapid-prototyper · hired`);
                // Fire-and-forget welcome mission
                fetch(`${dashUrl}/api/orchestrate`, {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ goal: 'Introduce yourself to this codebase — read the README and top 3 source files, then store what you learn as memories.' }),
                    signal: AbortSignal.timeout(3_000),
                }).catch(() => { });
            }
            else {
                log(`  ${c('—', 'dim')} Hire skipped (daemon warming up) — open dashboard to hire`);
            }
        }
        catch {
            log(`  ${c('—', 'dim')} Hire skipped — open ${c(`${dashUrl}/#workforce`, 'dim')} to hire manually`);
        }
    }
    else if (dryRun) {
        log(`  ${c('[dry-run]', 'dim')} Would hire engineering.rapid-prototyper`);
    }
    // Handoff: open browser + print banner
    if (!dryRun && daemonReady && (options.openDashboard ?? isNew)) {
        const target = isNew ? `${dashUrl}/welcome` : dashUrl;
        openBrowser(target);
        log(`  ${c('↗', 'cyan')} Opening ${c(target, 'cyan')} in browser…`);
    }
    printHandoffBanner(dashUrl, { isNew });
}
