import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { RuntimeRegistry, resolveNexusStateDir } from './runtime-registry.js';
import { SessionDNAManager } from './session-dna.js';
import { clearBootstrapReceipt, readBootstrapReceipt } from './bootstrap/bootstrap-registry.js';
import { doctorGitWorktrees } from './worktree-health.js';
import { sweepDirectory, sweepOrphanWorktrees } from '../install/fs-purge.js';
import { enumerateNgramArchives, getRuntimeTmpRoots, getWorktreeRoots } from '../install/state-locator.js';
import { resolveWorktreeBudget, resolveRunsBudget } from '../cli/cleanup.js';
function isOlderThan(target, maxAgeMs) {
    try {
        const stat = fs.statSync(target);
        return Date.now() - stat.mtimeMs > maxAgeMs;
    }
    catch {
        return false;
    }
}
function safeUnlink(target) {
    try {
        if (fs.existsSync(target)) {
            fs.unlinkSync(target);
            return true;
        }
    }
    catch {
        // best effort
    }
    return false;
}
function safeRmDir(target) {
    try {
        if (fs.existsSync(target)) {
            fs.rmSync(target, { recursive: true, force: true });
            return true;
        }
    }
    catch {
        // best effort
    }
    return false;
}
function cleanupTmpDirEntries(rootDir, opts) {
    if (!fs.existsSync(rootDir))
        return 0;
    let cleaned = 0;
    for (const entry of fs.readdirSync(rootDir)) {
        if (opts.matchPrefix && !entry.startsWith(opts.matchPrefix))
            continue;
        const target = path.join(rootDir, entry);
        if (opts.mode === 'fresh' || isOlderThan(target, opts.maxAgeMs)) {
            if (safeRmDir(target))
                cleaned += 1;
        }
    }
    return cleaned;
}
export async function runStartupHygiene(input) {
    const mode = input.mode ?? 'stale';
    const stateDir = resolveNexusStateDir();
    const registryDir = path.join(stateDir, 'runtime-registry');
    const registryBefore = fs.existsSync(registryDir) ? fs.readdirSync(registryDir).filter((f) => f.endsWith('.json')).length : 0;
    const registry = new RuntimeRegistry(stateDir);
    registry.pruneStalePublic();
    const registryAfter = fs.existsSync(registryDir) ? fs.readdirSync(registryDir).filter((f) => f.endsWith('.json')).length : 0;
    const cleanedSessionLocks = SessionDNAManager.cleanStale(path.join(stateDir, 'sessions'));
    let clearedBootstrapReceipt = false;
    if (mode === 'fresh') {
        clearBootstrapReceipt(input.workspaceStateRoot);
        clearedBootstrapReceipt = true;
    }
    else {
        const receipt = readBootstrapReceipt(input.workspaceStateRoot);
        if (receipt === null) {
            clearBootstrapReceipt(input.workspaceStateRoot);
            clearedBootstrapReceipt = true;
        }
    }
    let worktreeDoctorOverall;
    try {
        const health = await doctorGitWorktrees(input.repoRoot);
        worktreeDoctorOverall = health.overall;
    }
    catch {
        // hygiene must not block startup
    }
    const tmpDir = os.tmpdir();
    const maxAgeMs = mode === 'fresh' ? 0 : 30 * 60 * 1000;
    let cleanedTmpEntries = 0;
    const transientRoots = [...getRuntimeTmpRoots(), ...getWorktreeRoots()];
    for (const root of transientRoots) {
        cleanedTmpEntries += cleanupTmpDirEntries(root, { mode, maxAgeMs });
    }
    cleanedTmpEntries += cleanupTmpDirEntries(tmpDir, { mode, maxAgeMs, matchPrefix: 'nexus-prime-feature-registry-' });
    cleanedTmpEntries += cleanupTmpDirEntries(tmpDir, { mode, maxAgeMs, matchPrefix: 'nexus-reset-test-' });
    if (mode === 'fresh') {
        cleanedTmpEntries += safeUnlink(path.join(stateDir, 'events.jsonl')) ? 1 : 0;
        cleanedTmpEntries += safeUnlink(path.join(stateDir, 'dashboard-state.json')) ? 1 : 0;
    }
    const worktreeBudget = resolveWorktreeBudget();
    let boundedWorktreeSweeps = 0;
    for (const root of getWorktreeRoots()) {
        try {
            const sweep = sweepDirectory(root, {
                maxAgeMs: worktreeBudget.ttlMs,
                maxCount: worktreeBudget.maxCount,
                maxBytes: worktreeBudget.maxBytes,
                dryRun: false,
                keepRoot: true,
            });
            boundedWorktreeSweeps += sweep.removed.length;
        }
        catch {
            // best-effort
        }
    }
    let orphanWorktreesRemoved = 0;
    try {
        const orphans = await sweepOrphanWorktrees(input.repoRoot, { dryRun: false });
        orphanWorktreesRemoved = orphans.ops.filter((op) => op.removed).length;
    }
    catch {
        // ignore
    }
    const runsBudget = resolveRunsBudget();
    let boundedRunsSweeps = 0;
    for (const runsRoot of [path.join(input.workspaceStateRoot, 'runs'), ...getRuntimeTmpRoots()]) {
        try {
            const sweep = sweepDirectory(runsRoot, {
                maxAgeMs: runsBudget.ttlMs,
                maxCount: runsBudget.maxCount,
                maxBytes: runsBudget.maxBytes,
                dryRun: false,
                keepRoot: true,
            });
            boundedRunsSweeps += sweep.removed.length;
        }
        catch {
            // best-effort
        }
    }
    const { ngramSidecarOrphansRemoved, ngramArchivesPruned } = pruneNgramArtifacts(stateDir);
    return {
        mode,
        cleanedRuntimeRegistryEntries: Math.max(0, registryBefore - registryAfter),
        cleanedSessionLocks,
        cleanedTmpEntries,
        clearedBootstrapReceipt,
        worktreeDoctorOverall,
        boundedWorktreeSweeps,
        orphanWorktreesRemoved,
        boundedRunsSweeps,
        ngramSidecarOrphansRemoved,
        ngramArchivesPruned,
    };
}
/**
 * Best-effort ngram cleanup at startup:
 *   - delete orphan `*.db-wal` / `*.db-shm` when the matching `.db` is missing
 *   - keep at most one `ngram-index.db.oversize.*` archive
 * The live DB is left alone — NgramIndex itself enforces WAL bounds via
 * journal_size_limit + autocheckpoint when it opens.
 */
function pruneNgramArtifacts(stateDir) {
    let ngramSidecarOrphansRemoved = 0;
    let ngramArchivesPruned = 0;
    if (!fs.existsSync(stateDir)) {
        return { ngramSidecarOrphansRemoved, ngramArchivesPruned };
    }
    // Orphan sidecars: foo.db-wal / foo.db-shm with no foo.db.
    try {
        for (const entry of fs.readdirSync(stateDir)) {
            if (!entry.endsWith('.db-wal') && !entry.endsWith('.db-shm'))
                continue;
            const dbBase = entry.replace(/-(?:wal|shm)$/, '');
            if (fs.existsSync(path.join(stateDir, dbBase)))
                continue;
            if (safeUnlink(path.join(stateDir, entry)))
                ngramSidecarOrphansRemoved += 1;
        }
    }
    catch {
        // best-effort
    }
    // Archive cap: keep newest, drop the rest.
    try {
        const archives = enumerateNgramArchives(stateDir).sort((a, b) => b.modifiedAt - a.modifiedAt);
        for (const archive of archives.slice(1)) {
            if (safeUnlink(archive.path))
                ngramArchivesPruned += 1;
        }
    }
    catch {
        // best-effort
    }
    return { ngramSidecarOrphansRemoved, ngramArchivesPruned };
}
