/**
 * process-cleanup — stop every Nexus-owned process without touching anything else.
 *
 * Wraps lockfile cleanup and adds a process-table sweep for legacy launch
 * shapes. Older installs can be started by npx/npm, by MCP clients as
 * `node .../dist/index.js`, or by dashboard/daemon commands that never wrote a
 * current lockfile. Uninstall must catch all of those, otherwise users think
 * Nexus was removed while stale workers keep writing state.
 */
import * as fs from 'fs';
import { spawnSync } from 'child_process';
import { scanAllLockfiles, isProcessAlive } from '../daemon/lock.js';
import { getNexusStateDir } from './state-locator.js';
export function killProcessTree(pid) {
    if (!Number.isInteger(pid) || pid <= 0) {
        return;
    }
    if (process.platform === 'win32') {
        spawnSync('taskkill', ['/PID', String(pid), '/T', '/F'], {
            stdio: 'ignore',
            windowsHide: true,
            timeout: 5_000,
        });
        return;
    }
    try {
        process.kill(-pid, 'SIGTERM');
    }
    catch {
        try {
            process.kill(pid, 'SIGTERM');
        }
        catch {
            return;
        }
    }
}
function sleep(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
}
function normalizeCommand(command) {
    return command.replace(/\s+/g, ' ').trim();
}
function looksLikeNexusEntrypoint(command) {
    const normalized = normalizeCommand(command);
    return /\bnexus-prime(?:\s|$)/.test(normalized)
        || /(?:^|[\s"'])npx(?:\.cmd)?(?:\s+--yes)?\s+nexus-prime(?:\s|$)/.test(normalized)
        || /(?:^|[\s"'])npm(?:\.cmd)?\s+(?:exec|x)\s+(?:--\s+)?nexus-prime(?:\s|$)/.test(normalized)
        || /[\\/]nexus-prime[\\/](?:dist[\\/])?(?:index|cli)\.js(?:\s|$)/.test(normalized)
        || /[\\/]node_modules[\\/]\.bin[\\/]nexus-prime(?:\s|$)/.test(normalized);
}
function isBenignNexusCommand(command) {
    const normalized = normalizeCommand(command);
    return /\b(npm|pnpm|yarn)\s+(?:install|add|remove|uninstall|view|info|show)\b/.test(normalized)
        || /\b(vitest|jest|tsc|tsx)\b.*\bsrc[\\/]install[\\/]process-cleanup\.ts\b/.test(normalized);
}
function isNexusProcessCommand(command) {
    const normalized = normalizeCommand(command);
    if (!looksLikeNexusEntrypoint(normalized) || isBenignNexusCommand(normalized))
        return false;
    // Runtime surfaces that can keep writing state / holding dashboard ports.
    if (/\b(mcp|daemon\s+__serve|dashboard|server|serve|start|orchestrate|bootstrap)\b/.test(normalized)) {
        return true;
    }
    // MCP clients often launch the compiled entrypoint without a visible subcommand.
    return /[\\/]nexus-prime[\\/]dist[\\/](?:index|cli)\.js(?:\s|$)/.test(normalized)
        || /[\\/]node_modules[\\/]\.bin[\\/]nexus-prime(?:\s|$)/.test(normalized);
}
function parseProcessLines(stdout) {
    const candidates = [];
    for (const line of (stdout ?? '').split(/\r?\n/)) {
        const match = line.match(/^\s*(\d+)\s+(.+)$/) ?? line.match(/^\s*(\d+)\t(.+)$/);
        if (!match)
            continue;
        const pid = Number(match[1]);
        const command = match[2];
        if (pid === process.pid || pid === process.ppid || !Number.isInteger(pid) || pid <= 1)
            continue;
        if (!isNexusProcessCommand(command))
            continue;
        candidates.push({ pid, command });
    }
    return candidates;
}
export function listNexusProcessPids() {
    if (process.platform === 'win32') {
        const result = spawnSync('powershell.exe', [
            '-NoProfile',
            '-Command',
            "Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -match 'nexus-prime|nexus-prime.*dist|node_modules.*nexus-prime' } | ForEach-Object { \"$($_.ProcessId)`t$($_.CommandLine)\" }",
        ], {
            encoding: 'utf8',
            timeout: 5_000,
            windowsHide: true,
        });
        return result.status === 0 ? parseProcessLines(result.stdout ?? '') : [];
    }
    const result = spawnSync('ps', ['-axo', 'pid=,command='], {
        encoding: 'utf8',
        timeout: 5_000,
    });
    if (result.status !== 0) {
        return [];
    }
    return parseProcessLines(result.stdout ?? '');
}
async function stopPid(pid, options) {
    if (options.dryRun) {
        return { stopped: true };
    }
    if (!options.alive(pid)) {
        return { stopped: true };
    }
    try {
        options.killTree(pid);
    }
    catch (err) {
        return { stopped: false, error: err instanceof Error ? err.message : String(err) };
    }
    for (let attempt = 0; attempt < 20 && options.alive(pid); attempt += 1) {
        await options.sleepFn(100);
    }
    if (options.alive(pid)) {
        try {
            process.kill(pid, 'SIGKILL');
        }
        catch {
            // Best effort fallback; final liveness check below decides status.
        }
        for (let attempt = 0; attempt < 10 && options.alive(pid); attempt += 1) {
            await options.sleepFn(100);
        }
    }
    return options.alive(pid) ? { stopped: false, error: 'process did not stop' } : { stopped: true };
}
export async function stopAllNexusProcesses(options = {}) {
    const dryRun = options.dryRun ?? false;
    const stateDir = options.stateRoot ?? getNexusStateDir();
    const alive = options.isProcessAliveFn ?? isProcessAlive;
    const killTree = options.killProcessTreeFn ?? killProcessTree;
    const listProcessPids = options.listProcessPidsFn ?? listNexusProcessPids;
    const sleepFn = options.sleepFn ?? sleep;
    const errors = [];
    const stoppedPids = new Set();
    let daemonsKilled = 0;
    let lockfilesRemoved = 0;
    try {
        const scans = scanAllLockfiles(stateDir);
        for (const scan of scans) {
            if (alive(scan.lock.pid)) {
                const result = await stopPid(scan.lock.pid, { dryRun, alive, killTree, sleepFn });
                if (result.stopped) {
                    daemonsKilled += 1;
                    stoppedPids.add(scan.lock.pid);
                }
                else {
                    errors.push(`daemon pid ${scan.lock.pid} did not stop${result.error ? `: ${result.error}` : ''}`);
                    continue;
                }
            }
            if (dryRun) {
                lockfilesRemoved += 1;
                continue;
            }
            try {
                fs.unlinkSync(scan.lockPath);
                lockfilesRemoved += 1;
            }
            catch (err) {
                errors.push(`unlink ${scan.lockPath}: ${err instanceof Error ? err.message : String(err)}`);
            }
        }
    }
    catch (err) {
        errors.push(`scan-lockfiles failed: ${err instanceof Error ? err.message : String(err)}`);
    }
    try {
        const candidates = listProcessPids()
            .filter((candidate) => !stoppedPids.has(candidate.pid))
            .sort((a, b) => b.pid - a.pid);
        for (const candidate of candidates) {
            if (!alive(candidate.pid))
                continue;
            const result = await stopPid(candidate.pid, { dryRun, alive, killTree, sleepFn });
            if (result.stopped) {
                daemonsKilled += 1;
                stoppedPids.add(candidate.pid);
            }
            else {
                errors.push(`process pid ${candidate.pid} did not stop${result.error ? `: ${result.error}` : ''}`);
            }
        }
    }
    catch (err) {
        errors.push(`scan-processes failed: ${err instanceof Error ? err.message : String(err)}`);
    }
    return { daemonsKilled, lockfilesRemoved, errors, dryRun };
}
