/**
 * olam implode — destroy ALL local olam global install + configs so the
 * next `npm install -g @pleri/olam-cli` lands on a clean machine.
 *
 * Wipes (in order):
 *   1. Docker containers matching `olam-*` (worlds first, then stack).
 *   2. Docker images matching `olam-*:*` and `ghcr.io/pleri/olam-*:*`.
 *   3. Docker volumes matching `olam-*`.
 *   4. Docker networks matching `olam-*`.
 *   5. The `~/.olam/` tree (worlds, vault, workspaces, cache, log).
 *   6. The global npm package `@pleri/olam-cli` (last — the command
 *      uninstalls itself, so anything after this never runs).
 *
 * Default invocation is dry-run. `--yes` actually destroys after a
 * typed-confirmation prompt ("type implode to proceed"). `--include-dirty`
 * is REQUIRED if any world has uncommitted/unpushed work — the dirty list
 * is printed so the operator can see what's about to disappear.
 *
 * NEVER touches:
 *   - `~/.claude/` (Claude Code operator config — not olam's).
 *   - Any project-level `.olam/` directories.
 *   - Repos in `~/Projects/`.
 *
 * The `--keep-X` flags scope the destruction. `--keep-images` skips step 2,
 * `--keep-vault` preserves `~/.olam/auth-data/` while still wiping the rest
 * of `~/.olam/`, `--keep-npm` skips step 6, etc.
 */
import { execFileSync } from 'node:child_process';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { createInterface } from 'node:readline/promises';
import pc from 'picocolors';
import { printError, printHeader, printInfo, printSuccess, printWarning } from '../output.js';
// ────────────────────────────── Defaults ────────────────────────────────
const NPM_PACKAGE = '@pleri/olam-cli';
/** Real exec backed by `execFileSync`. Captures rc + stdout + stderr. */
const realExec = (cmd, args) => {
    try {
        const stdout = execFileSync(cmd, [...args], {
            encoding: 'utf-8',
            stdio: ['ignore', 'pipe', 'pipe'],
        });
        return { exitCode: 0, stdout, stderr: '' };
    }
    catch (err) {
        const e = err;
        return {
            exitCode: e.status ?? 1,
            stdout: typeof e.stdout === 'string' ? e.stdout : (e.stdout?.toString('utf-8') ?? ''),
            stderr: typeof e.stderr === 'string' ? e.stderr : (e.stderr?.toString('utf-8') ?? ''),
        };
    }
};
// ────────────────────────────── Survey ──────────────────────────────────
/**
 * Build a complete survey of every olam-owned resource on the machine.
 * Pure function — no destruction. Safe to call any time.
 */
export function surveyOlam(opts = {}) {
    const exec = opts.exec ?? realExec;
    const home = opts.olamHome ?? path.join(os.homedir(), '.olam');
    // Docker resources — group by prefix. We use --format to keep output
    // parseable; --filter name= matches as a substring so we tighten with
    // a startsWith check after the fact.
    const containers = listDocker(exec, ['ps', '-a', '--format', '{{.Names}}']).filter((n) => n.startsWith('olam-'));
    const images = listDocker(exec, ['images', '--format', '{{.Repository}}:{{.Tag}}']).filter((ref) => ref.startsWith('olam-') ||
        ref.startsWith('ghcr.io/pleri/olam-') ||
        ref.startsWith('ghcr.io/pleri/olam'));
    const volumes = listDocker(exec, ['volume', 'ls', '--format', '{{.Name}}']).filter((n) => n.startsWith('olam-'));
    const networks = listDocker(exec, ['network', 'ls', '--format', '{{.Name}}']).filter((n) => n.startsWith('olam-') && n !== 'olam-host-cp-internal-default');
    // Worlds — each ~/.olam/worlds/<id>/ subdir, with size + dirty check.
    const worlds = [];
    const worldsDir = path.join(home, 'worlds');
    if (fs.existsSync(worldsDir)) {
        for (const entry of fs.readdirSync(worldsDir, { withFileTypes: true })) {
            if (!entry.isDirectory())
                continue;
            const wPath = path.join(worldsDir, entry.name);
            const bytes = dirSize(wPath);
            const { dirty, note } = inspectWorldGitState(wPath);
            worlds.push({ id: entry.name, bytes, dirty, note, path: wPath });
        }
    }
    // ~/.olam/* size minus worlds (which we count separately so reviewers
    // can see how much data is in the vault / cache vs in worlds).
    const homeBytesNonWorld = fs.existsSync(home)
        ? Math.max(0, dirSize(home) - worlds.reduce((acc, w) => acc + w.bytes, 0))
        : 0;
    // npm: is the package installed globally? Where?
    const npmRoot = exec('npm', ['root', '-g']);
    const npmGlobalRoot = npmRoot.exitCode === 0 ? npmRoot.stdout.trim() : null;
    const npmInstalled = npmGlobalRoot !== null && fs.existsSync(path.join(npmGlobalRoot, NPM_PACKAGE));
    return {
        containers,
        images,
        volumes,
        networks,
        worlds,
        homeBytesNonWorld,
        npmInstalled,
        npmGlobalRoot,
    };
}
function listDocker(exec, args) {
    const r = exec('docker', args);
    if (r.exitCode !== 0)
        return [];
    return r.stdout
        .split('\n')
        .map((s) => s.trim())
        .filter((s) => s.length > 0);
}
function dirSize(p) {
    let total = 0;
    try {
        const stack = [p];
        while (stack.length > 0) {
            const cur = stack.pop();
            let entries;
            try {
                entries = fs.readdirSync(cur, { withFileTypes: true });
            }
            catch {
                continue;
            }
            for (const e of entries) {
                const full = path.join(cur, e.name);
                try {
                    if (e.isDirectory())
                        stack.push(full);
                    else if (e.isFile())
                        total += fs.statSync(full).size;
                }
                catch {
                    // Skip unreadable entries.
                }
            }
        }
    }
    catch {
        // Ignored.
    }
    return total;
}
function inspectWorldGitState(worldPath) {
    // A world is "dirty" if any of its repos has uncommitted or unpushed
    // changes. Cheap check: look for repo subdirs with .git, run a
    // `git status --porcelain` + `git rev-list @{upstream}..HEAD` per repo.
    const repoNotes = [];
    let dirty = false;
    try {
        for (const entry of fs.readdirSync(worldPath, { withFileTypes: true })) {
            if (!entry.isDirectory())
                continue;
            const repo = path.join(worldPath, entry.name);
            const gitDir = path.join(repo, '.git');
            if (!fs.existsSync(gitDir))
                continue;
            try {
                const status = execFileSync('git', ['status', '--porcelain'], {
                    cwd: repo,
                    encoding: 'utf-8',
                    stdio: ['ignore', 'pipe', 'ignore'],
                }).trim();
                if (status.length > 0) {
                    dirty = true;
                    repoNotes.push(`${entry.name}: uncommitted`);
                    continue;
                }
            }
            catch {
                // Skip repos that aren't real worktrees.
                continue;
            }
            try {
                const ahead = execFileSync('git', ['rev-list', '--count', '@{upstream}..HEAD'], { cwd: repo, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
                if (ahead && ahead !== '0') {
                    dirty = true;
                    repoNotes.push(`${entry.name}: ${ahead} unpushed`);
                }
            }
            catch {
                // No upstream — count as dirty so unpushed local commits aren't
                // silently destroyed.
                dirty = true;
                repoNotes.push(`${entry.name}: no-upstream`);
            }
        }
    }
    catch {
        // Worldwise read failure — surface as a note but don't claim dirty.
    }
    return { dirty, note: repoNotes.join('; ') };
}
// ────────────────────────────── Render ──────────────────────────────────
export function renderSurvey(survey, opts) {
    if (opts.json) {
        return JSON.stringify({
            containers: survey.containers,
            images: survey.images,
            volumes: survey.volumes,
            networks: survey.networks,
            worlds: survey.worlds.map((w) => ({
                id: w.id,
                bytes: w.bytes,
                dirty: w.dirty,
                note: w.note,
            })),
            homeBytesNonWorld: survey.homeBytesNonWorld,
            npmInstalled: survey.npmInstalled,
            npmGlobalRoot: survey.npmGlobalRoot,
        }, null, 2);
    }
    const lines = [];
    const dirtyWorlds = survey.worlds.filter((w) => w.dirty);
    const cleanWorlds = survey.worlds.filter((w) => !w.dirty);
    const worldBytes = survey.worlds.reduce((acc, w) => acc + w.bytes, 0);
    lines.push(pc.bold(pc.red('— olam implode survey —')));
    lines.push('');
    lines.push(`  containers (olam-*):   ${survey.containers.length}`);
    lines.push(`  images:                ${survey.images.length}`);
    lines.push(`  volumes:               ${survey.volumes.length}`);
    lines.push(`  networks:              ${survey.networks.length}`);
    lines.push(`  worlds:                ${survey.worlds.length}  (${dirtyWorlds.length} dirty, ${cleanWorlds.length} clean)`);
    lines.push(`  ~/.olam/* (non-world): ${formatBytes(survey.homeBytesNonWorld)}`);
    lines.push(`  worlds total:          ${formatBytes(worldBytes)}`);
    lines.push(`  npm package installed: ${survey.npmInstalled ? `yes (at ${survey.npmGlobalRoot}/${NPM_PACKAGE})` : 'no'}`);
    if (dirtyWorlds.length > 0) {
        lines.push('');
        lines.push(pc.yellow(`  ⚠ dirty worlds (${dirtyWorlds.length}) — pass --include-dirty to destroy:`));
        for (const w of dirtyWorlds) {
            lines.push(`    - ${w.id}  ${formatBytes(w.bytes)}  (${w.note})`);
        }
    }
    lines.push('');
    lines.push(pc.dim('  Run with --yes to actually destroy. Default is dry-run.'));
    return lines.join('\n');
}
function formatBytes(n) {
    if (n < 1024)
        return `${n} B`;
    if (n < 1024 * 1024)
        return `${(n / 1024).toFixed(1)} KB`;
    if (n < 1024 * 1024 * 1024)
        return `${(n / 1024 / 1024).toFixed(1)} MB`;
    return `${(n / 1024 / 1024 / 1024).toFixed(2)} GB`;
}
export async function executeImplode(survey, opts) {
    const exec = opts.exec ?? realExec;
    const home = opts.olamHome ?? path.join(os.homedir(), '.olam');
    const removed = [];
    const skipped = [];
    const errors = [];
    // Step 1: stop + remove containers (worlds first so the host-cp
    // socket-proxy chain stays alive long enough to kill them gracefully).
    if (!opts.keepWorlds) {
        const sortedContainers = [...survey.containers].sort((a, b) => {
            const aWorld = isWorldContainer(a);
            const bWorld = isWorldContainer(b);
            if (aWorld && !bWorld)
                return -1;
            if (!aWorld && bWorld)
                return 1;
            return 0;
        });
        for (const name of sortedContainers) {
            const r = exec('docker', ['rm', '-f', name]);
            if (r.exitCode === 0)
                removed.push(`container:${name}`);
            else
                errors.push(`container:${name}: ${r.stderr.trim()}`);
        }
    }
    else {
        for (const c of survey.containers)
            skipped.push(`container:${c}`);
    }
    // Step 2: images.
    if (!opts.keepImages) {
        for (const ref of survey.images) {
            // Skip images that are still in use by a kept container.
            const r = exec('docker', ['rmi', '-f', ref]);
            if (r.exitCode === 0)
                removed.push(`image:${ref}`);
            else
                errors.push(`image:${ref}: ${r.stderr.trim()}`);
        }
    }
    else {
        for (const i of survey.images)
            skipped.push(`image:${i}`);
    }
    // Step 3: volumes.
    if (!opts.keepVolumes && !opts.keepVault) {
        for (const v of survey.volumes) {
            const r = exec('docker', ['volume', 'rm', '-f', v]);
            if (r.exitCode === 0)
                removed.push(`volume:${v}`);
            else
                errors.push(`volume:${v}: ${r.stderr.trim()}`);
        }
    }
    else if (opts.keepVault) {
        // Vault preservation: keep olam-auth-data + olam-mcp-auth-data, drop the rest.
        for (const v of survey.volumes) {
            if (v === 'olam-auth-data' || v === 'olam-mcp-auth-data') {
                skipped.push(`volume:${v} (--keep-vault)`);
                continue;
            }
            const r = exec('docker', ['volume', 'rm', '-f', v]);
            if (r.exitCode === 0)
                removed.push(`volume:${v}`);
            else
                errors.push(`volume:${v}: ${r.stderr.trim()}`);
        }
    }
    else {
        for (const v of survey.volumes)
            skipped.push(`volume:${v}`);
    }
    // Step 4: networks.
    if (!opts.keepNetworks) {
        for (const n of survey.networks) {
            const r = exec('docker', ['network', 'rm', n]);
            if (r.exitCode === 0)
                removed.push(`network:${n}`);
            else
                errors.push(`network:${n}: ${r.stderr.trim()}`);
        }
    }
    else {
        for (const n of survey.networks)
            skipped.push(`network:${n}`);
    }
    // Step 5: ~/.olam/ tree. Selective when --keep-vault.
    if (fs.existsSync(home)) {
        if (opts.keepVault) {
            // Wipe everything under ~/.olam/ except auth-data.
            for (const entry of fs.readdirSync(home, { withFileTypes: true })) {
                if (entry.name === 'auth-data') {
                    skipped.push(`fs:~/.olam/auth-data (--keep-vault)`);
                    continue;
                }
                const target = path.join(home, entry.name);
                try {
                    fs.rmSync(target, { recursive: true, force: true });
                    removed.push(`fs:${target}`);
                }
                catch (err) {
                    errors.push(`fs:${target}: ${err.message}`);
                }
            }
        }
        else {
            try {
                fs.rmSync(home, { recursive: true, force: true });
                removed.push(`fs:${home}`);
            }
            catch (err) {
                errors.push(`fs:${home}: ${err.message}`);
            }
        }
    }
    // Step 6: npm package — last, since it removes this command itself.
    if (!opts.keepNpm && survey.npmInstalled) {
        const r = exec('npm', ['uninstall', '-g', NPM_PACKAGE]);
        if (r.exitCode === 0)
            removed.push(`npm:${NPM_PACKAGE}`);
        else
            errors.push(`npm:${NPM_PACKAGE}: ${r.stderr.trim()}`);
    }
    else if (opts.keepNpm) {
        skipped.push(`npm:${NPM_PACKAGE}`);
    }
    return { removed, skipped, errors };
}
function isWorldContainer(name) {
    // World containers look like `olam-<world-id>-devbox` or `olam-<world-id>-cp`.
    // Stack containers are exactly `olam-host-cp`, `olam-auth`, `olam-mcp-auth`,
    // `olam-docker-socket-proxy`.
    const stack = new Set(['olam-host-cp', 'olam-auth', 'olam-mcp-auth', 'olam-docker-socket-proxy']);
    return !stack.has(name);
}
// ────────────────────────────── Confirm prompt ───────────────────────────
async function readConfirmation() {
    const rl = createInterface({ input: process.stdin, output: process.stdout });
    const answer = await rl.question('Type "implode" to proceed: ');
    rl.close();
    return answer.trim();
}
// ────────────────────────────── Command wiring ───────────────────────────
export function registerImplode(program) {
    program
        .command('implode')
        .description('Destroy ALL local olam install + configs (containers, images, volumes, ~/.olam/, npm package). Default is dry-run.')
        .option('-y, --yes', 'Actually destroy (default is dry-run)', false)
        .option('--include-dirty', 'Also destroy worlds with uncommitted/unpushed work', false)
        .option('--keep-worlds', 'Skip world container destruction', false)
        .option('--keep-images', 'Skip docker image removal', false)
        .option('--keep-vault', 'Preserve auth-service credentials (~/.olam/auth-data + olam-*-data volumes)', false)
        .option('--keep-npm', 'Skip global npm package uninstall', false)
        .option('--keep-volumes', 'Skip docker volume removal', false)
        .option('--keep-networks', 'Skip docker network removal', false)
        .option('--json', 'Emit machine-readable JSON survey', false)
        .action(async (raw) => {
        const opts = {
            yes: raw.yes === true,
            includeDirty: raw.includeDirty === true,
            keepWorlds: raw.keepWorlds === true,
            keepImages: raw.keepImages === true,
            keepVault: raw.keepVault === true,
            keepNpm: raw.keepNpm === true,
            keepVolumes: raw.keepVolumes === true,
            keepNetworks: raw.keepNetworks === true,
            json: raw.json === true,
        };
        const exitCode = await runImplode(opts);
        process.exitCode = exitCode;
    });
}
export async function runImplode(opts) {
    const survey = surveyOlam({ exec: opts.exec, olamHome: opts.olamHome });
    if (opts.json && !opts.yes) {
        process.stdout.write(renderSurvey(survey, opts) + '\n');
        return 0;
    }
    if (!opts.yes) {
        process.stdout.write(renderSurvey(survey, opts) + '\n');
        return 0;
    }
    // --yes path: hard checks before destruction.
    const dirty = survey.worlds.filter((w) => w.dirty);
    if (dirty.length > 0 && !opts.includeDirty && !opts.keepWorlds) {
        printError(`Refusing to destroy ${dirty.length} world(s) with uncommitted/unpushed work. Pass --include-dirty to override or --keep-worlds to skip world destruction.`);
        for (const w of dirty) {
            process.stderr.write(`  - ${w.id} (${w.note})\n`);
        }
        return 2;
    }
    process.stdout.write(renderSurvey(survey, opts) + '\n');
    printWarning('This will permanently destroy the resources listed above.');
    printInfo('Project files', '~/Projects/* and ~/.claude/ are NOT touched.');
    const answer = opts.confirmAnswer ?? (await readConfirmation());
    if (answer !== 'implode') {
        printError(`Confirmation failed (got "${answer}", expected "implode"). Aborting.`);
        return 3;
    }
    printHeader('Imploding olam');
    const result = await executeImplode(survey, opts);
    for (const r of result.removed)
        printSuccess(`removed ${r}`);
    for (const s of result.skipped)
        printInfo('skipped', s);
    for (const e of result.errors)
        printError(e);
    printHeader('Implode complete');
    printInfo('Removed', String(result.removed.length));
    printInfo('Skipped', String(result.skipped.length));
    printInfo('Errors', String(result.errors.length));
    process.stdout.write('\n');
    process.stdout.write(`To reinstall a fresh copy:\n`);
    process.stdout.write(`  ${pc.cyan(`npm install -g ${NPM_PACKAGE}`)}\n`);
    process.stdout.write(`  ${pc.cyan('olam init')}\n`);
    process.stdout.write(`  ${pc.cyan('olam auth login')}\n`);
    return result.errors.length > 0 ? 1 : 0;
}
//# sourceMappingURL=implode.js.map