/**
 * nexus cleanup — bound the growth of worktree + runs directories.
 *
 * This never touches durable memory. It enforces aggressive defaults because
 * transient Nexus state must never surprise users with 10GB+ growth. Heavy
 * artifacts can still be retained by raising env budgets explicitly.
 */
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { formatBytes, sweepDirectory, sweepOrphanWorktrees, } from '../install/fs-purge.js';
import { getNexusStateDir, getWorktreeRoots, getRuntimeTmpRoots, NEXUS_TMP_GLOB_PREFIXES, } from '../install/state-locator.js';
function parseIntEnv(name, fallback) {
    const raw = process.env[name];
    const parsed = Number.parseInt(raw ?? '', 10);
    return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
}
export function resolveWorktreeBudget() {
    return {
        ttlMs: parseIntEnv('NEXUS_WORKTREE_TTL_MS', 60 * 60 * 1000),
        maxCount: parseIntEnv('NEXUS_WORKTREE_MAX_COUNT', 8),
        maxBytes: parseIntEnv('NEXUS_WORKTREE_MAX_BYTES', 512 * 1024 * 1024),
    };
}
export function resolveRunsBudget() {
    return {
        ttlMs: parseIntEnv('NEXUS_RUNS_TTL_MS', 30 * 60 * 1000),
        maxCount: parseIntEnv('NEXUS_RUNS_MAX_COUNT', 16),
        maxBytes: parseIntEnv('NEXUS_RUNS_MAX_BYTES', 256 * 1024 * 1024),
    };
}
function collectLegacyTmpRoots() {
    const tmpRoot = os.tmpdir();
    let entries = [];
    try {
        entries = fs.readdirSync(tmpRoot);
    }
    catch {
        return [];
    }
    const roots = new Set();
    for (const entry of entries) {
        if (!NEXUS_TMP_GLOB_PREFIXES.some((prefix) => entry.startsWith(prefix)))
            continue;
        const fullPath = path.join(tmpRoot, entry);
        try {
            if (fs.statSync(fullPath).isDirectory())
                roots.add(fullPath);
        }
        catch {
            // ignore races with concurrently removed tmp dirs
        }
    }
    return [...roots].sort();
}
export async function runCleanup(options) {
    const dryRun = options.dryRun || !options.fix;
    const repoRoot = options.repoRoot ?? process.cwd();
    const worktreeBudget = resolveWorktreeBudget();
    const runsBudget = resolveRunsBudget();
    const report = {
        worktrees: [],
        runs: [],
        orphans: { paths: [], bytesReclaimed: 0 },
        totalBytesReclaimed: 0,
        totalEntriesRemoved: 0,
        dryRun,
    };
    const label = dryRun ? '[dry-run]' : '[cleanup]';
    console.log(`\n${label} Nexus cleanup — repo: ${repoRoot}`);
    for (const root of getWorktreeRoots()) {
        const sweep = sweepDirectory(root, {
            maxAgeMs: worktreeBudget.ttlMs,
            maxCount: worktreeBudget.maxCount,
            maxBytes: worktreeBudget.maxBytes,
            dryRun,
            keepRoot: true,
        });
        report.worktrees.push(sweep);
        report.totalBytesReclaimed += sweep.removed.reduce((s, r) => s + r.bytes, 0);
        report.totalEntriesRemoved += sweep.removed.length;
        if (sweep.entriesBefore > 0 || sweep.removed.length > 0) {
            console.log(`  worktree: ${root}`);
            console.log(`    budget: ttl=${Math.round(worktreeBudget.ttlMs / 60000)}m, max=${worktreeBudget.maxCount}, bytes=${formatBytes(worktreeBudget.maxBytes)}`);
            console.log(`    before: ${sweep.entriesBefore} entries, ${formatBytes(sweep.totalBytesBefore)}`);
            console.log(`    removed: ${sweep.removed.length} entries, ${formatBytes(sweep.removed.reduce((s, r) => s + r.bytes, 0))}`);
            for (const r of sweep.removed) {
                console.log(`      - ${r.path}  (${formatBytes(r.bytes)}, reason=${r.reason})`);
            }
        }
    }
    const runsRoots = new Set([
        path.join(getNexusStateDir(), 'runs'),
        ...getRuntimeTmpRoots(),
        ...collectLegacyTmpRoots(),
    ]);
    for (const root of runsRoots) {
        const sweep = sweepDirectory(root, {
            maxAgeMs: runsBudget.ttlMs,
            maxCount: runsBudget.maxCount,
            maxBytes: runsBudget.maxBytes,
            dryRun,
            keepRoot: true,
        });
        report.runs.push(sweep);
        report.totalBytesReclaimed += sweep.removed.reduce((s, r) => s + r.bytes, 0);
        report.totalEntriesRemoved += sweep.removed.length;
        if (sweep.entriesBefore > 0 || sweep.removed.length > 0) {
            console.log(`  runs: ${root}`);
            console.log(`    budget: ttl=${Math.round(runsBudget.ttlMs / 60000)}m, max=${runsBudget.maxCount}, bytes=${formatBytes(runsBudget.maxBytes)}`);
            console.log(`    before: ${sweep.entriesBefore} entries, ${formatBytes(sweep.totalBytesBefore)}`);
            console.log(`    removed: ${sweep.removed.length} entries, ${formatBytes(sweep.removed.reduce((s, r) => s + r.bytes, 0))}`);
        }
    }
    try {
        const orphanReport = await sweepOrphanWorktrees(repoRoot, { dryRun });
        report.orphans.paths = orphanReport.ops.filter((op) => op.kind !== 'missing').map((op) => op.path);
        report.orphans.bytesReclaimed = orphanReport.totalBytesRemoved;
        report.totalBytesReclaimed += orphanReport.totalBytesRemoved;
        report.totalEntriesRemoved += report.orphans.paths.length;
        if (report.orphans.paths.length > 0) {
            console.log(`  orphans: ${report.orphans.paths.length} worktree dir(s) not tracked by git`);
            for (const p of report.orphans.paths)
                console.log(`      - ${p}`);
        }
    }
    catch (err) {
        console.log(`  ! orphan sweep skipped: ${err instanceof Error ? err.message : String(err)}`);
    }
    console.log(`\n${label} reclaimed ${formatBytes(report.totalBytesReclaimed)} across ${report.totalEntriesRemoved} entries.`);
    if (dryRun) {
        console.log('  Preview only — re-run with --fix to apply.');
    }
    return report;
}
