/**
 * NERF Maintenance — cleanup, skip, dry-run, and recover handlers.
 *
 * Contains: handleCleanupBranches, handleCleanupSnapshots, handleCleanupWorktrees, handleSkip, handleDryRun, handleRecover
 */
import { deriveState } from "./state.js";
import { nativeBranchList, nativeDetectMainBranch, nativeBranchListMerged, nativeBranchDelete, nativeForEachRef, nativeUpdateRef } from "./native-git-bridge.js";
export async function handleCleanupBranches(ctx, basePath) {
    let branches;
    try {
        branches = nativeBranchList(basePath, "nerf/*");
    }
    catch {
        ctx.ui.notify("No NERF branches to clean up.", "info");
        return;
    }
    const quickBranches = branches.filter((b) => b.startsWith("nerf/quick/"));
    const mainBranch = nativeDetectMainBranch(basePath);
    let merged;
    try {
        merged = nativeBranchListMerged(basePath, mainBranch, "nerf/*");
    }
    catch {
        merged = [];
    }
    const mergedNonQuick = merged.filter((b) => !b.startsWith("nerf/quick/"));
    let deletedMerged = 0;
    for (const branch of mergedNonQuick) {
        try {
            nativeBranchDelete(basePath, branch, false);
            deletedMerged++;
        }
        catch {
            /* skip branches that cannot be deleted */
        }
    }
    // Also delete stale milestone branches for completed milestones when detached
    // from any registered worktree.
    let deletedStaleMilestones = 0;
    try {
        const { listWorktrees } = await import("./worktree-manager.js");
        const { resolveMilestoneFile } = await import("./paths.js");
        const { loadFile } = await import("./files.js");
        const { parseRoadmap } = await import("./parsers-legacy.js");
        const { isMilestoneComplete } = await import("./state.js");
        const { isDbAvailable, getMilestone } = await import("./nerf-db.js");
        const attachedBranches = new Set(listWorktrees(basePath).map((wt) => wt.branch));
        const milestoneBranches = nativeBranchList(basePath, "milestone/*");
        for (const branch of milestoneBranches) {
            if (attachedBranches.has(branch))
                continue;
            const milestoneId = branch.replace(/^milestone\//, "");
            // DB-first: check milestone status directly
            if (isDbAvailable()) {
                const dbRow = getMilestone(milestoneId);
                if (dbRow) {
                    if (dbRow.status !== "complete" && dbRow.status !== "done")
                        continue;
                    // Milestone is complete per DB — proceed to delete branch
                    try {
                        nativeBranchDelete(basePath, branch, true);
                        deletedStaleMilestones++;
                    }
                    catch { /* non-fatal */ }
                    continue;
                }
            }
            // Filesystem fallback
            const roadmapPath = resolveMilestoneFile(basePath, milestoneId, "ROADMAP");
            if (!roadmapPath)
                continue;
            let roadmapContent = null;
            try {
                roadmapContent = await loadFile(roadmapPath);
            }
            catch {
                roadmapContent = null;
            }
            if (!roadmapContent)
                continue;
            if (!isMilestoneComplete(parseRoadmap(roadmapContent)))
                continue;
            try {
                nativeBranchDelete(basePath, branch, true);
                deletedStaleMilestones++;
            }
            catch {
                /* non-fatal */
            }
        }
    }
    catch {
        /* non-fatal */
    }
    const summary = [];
    if (deletedMerged > 0) {
        summary.push(`Cleaned up ${deletedMerged} merged branch${deletedMerged === 1 ? "" : "es"}.`);
    }
    if (deletedStaleMilestones > 0) {
        summary.push(`Deleted ${deletedStaleMilestones} stale milestone branch${deletedStaleMilestones === 1 ? "" : "es"}.`);
    }
    if (quickBranches.length > 0) {
        summary.push(`Skipped ${quickBranches.length} quick branch${quickBranches.length === 1 ? "" : "es"} (bnerf/quick/*).`);
    }
    if (summary.length === 0) {
        const nonQuickCount = branches.filter((b) => !b.startsWith("nerf/quick/")).length;
        ctx.ui.notify(nonQuickCount > 0
            ? `${nonQuickCount} NERF branch${nonQuickCount === 1 ? "" : "es"} found, none merged into ${mainBranch} yet.`
            : "No non-quick NERF branches to clean up.", "info");
        return;
    }
    ctx.ui.notify(summary.join(" "), "success");
}
export async function handleCleanupSnapshots(ctx, basePath) {
    let refs;
    try {
        refs = nativeForEachRef(basePath, "refs/nerf/snapshots/");
    }
    catch {
        ctx.ui.notify("No snapshot refs to clean up.", "info");
        return;
    }
    if (refs.length === 0) {
        ctx.ui.notify("No snapshot refs to clean up.", "info");
        return;
    }
    const byLabel = new Map();
    for (const ref of refs) {
        const parts = ref.split("/");
        const label = parts.slice(0, -1).join("/");
        if (!byLabel.has(label))
            byLabel.set(label, []);
        byLabel.get(label).push(ref);
    }
    let pruned = 0;
    for (const [, labelRefs] of byLabel) {
        const sorted = labelRefs.sort();
        for (const old of sorted.slice(0, -5)) {
            try {
                nativeUpdateRef(basePath, old);
                pruned++;
            }
            catch {
                /* skip individual failures */
            }
        }
    }
    ctx.ui.notify(`Pruned ${pruned} old snapshot refs. ${refs.length - pruned} remain.`, "success");
}
export async function handleCleanupWorktrees(ctx, basePath) {
    const { getAllWorktreeHealth, formatWorktreeStatusLine } = await import("./worktree-health.js");
    const { removeWorktree } = await import("./worktree-manager.js");
    const { sep } = await import("node:path");
    let statuses;
    try {
        statuses = getAllWorktreeHealth(basePath);
    }
    catch {
        ctx.ui.notify("Failed to inspect worktrees.", "error");
        return;
    }
    if (statuses.length === 0) {
        ctx.ui.notify("No NERF worktrees found.", "info");
        return;
    }
    const safeToRemove = statuses.filter(s => s.safeToRemove);
    const stale = statuses.filter(s => s.stale && !s.safeToRemove);
    const active = statuses.filter(s => !s.safeToRemove && !s.stale);
    const lines = [];
    lines.push(`${statuses.length} worktree${statuses.length === 1 ? "" : "s"} found.`);
    lines.push("");
    if (safeToRemove.length > 0) {
        lines.push(`Safe to remove (${safeToRemove.length}) — merged into main, clean:`);
        const cwd = process.cwd();
        let removed = 0;
        for (const s of safeToRemove) {
            const wt = s.worktree;
            const isCwd = wt.path === cwd || cwd.startsWith(wt.path + sep);
            if (isCwd) {
                lines.push(`  ⊘ ${wt.name}  (skipped — current working directory)`);
                continue;
            }
            try {
                removeWorktree(basePath, wt.name, { deleteBranch: true });
                lines.push(`  ✓ ${wt.name}  removed (branch ${wt.branch} deleted)`);
                removed++;
            }
            catch {
                lines.push(`  ✗ ${wt.name}  failed to remove`);
            }
        }
        if (removed > 0) {
            lines.push("");
            lines.push(`Removed ${removed} merged worktree${removed === 1 ? "" : "s"}.`);
        }
        lines.push("");
    }
    if (stale.length > 0) {
        lines.push(`Stale (${stale.length}) — no recent commits, not merged (review manually):`);
        for (const s of stale) {
            lines.push(`  ⚠ ${s.worktree.name}  ${formatWorktreeStatusLine(s)}`);
        }
        lines.push("");
    }
    if (active.length > 0) {
        lines.push(`Active (${active.length}) — in progress:`);
        for (const s of active) {
            lines.push(`  ● ${s.worktree.name}  ${formatWorktreeStatusLine(s)}`);
        }
        lines.push("");
    }
    if (safeToRemove.length === 0 && stale.length === 0) {
        lines.push("All worktrees are active — nothing to clean up.");
    }
    ctx.ui.notify(lines.join("\n"), safeToRemove.length > 0 ? "success" : "info");
}
export async function handleSkip(unitArg, ctx, basePath) {
    if (!unitArg) {
        ctx.ui.notify("Usage: /nerf skip <unit-id>  (e.g., /nerf skip execute-task/M001/S01/T03 or /nerf skip T03)", "info");
        return;
    }
    const { existsSync: fileExists, writeFileSync: writeFile, mkdirSync: mkDir, readFileSync: readFile } = await import("node:fs");
    const { join: pathJoin } = await import("node:path");
    const completedKeysFile = pathJoin(basePath, ".nerf", "completed-units.json");
    let keys = [];
    try {
        if (fileExists(completedKeysFile)) {
            keys = JSON.parse(readFile(completedKeysFile, "utf-8"));
        }
    }
    catch { /* start fresh */ }
    // Normalize: accept "execute-task/M001/S01/T03", "M001/S01/T03", or just "T03"
    let skipKey = unitArg;
    if (!skipKey.includes("execute-task") && !skipKey.includes("plan-") && !skipKey.includes("research-") && !skipKey.includes("complete-")) {
        const state = await deriveState(basePath);
        const mid = state.activeMilestone?.id;
        const sid = state.activeSlice?.id;
        if (unitArg.match(/^T\d+$/i) && mid && sid) {
            skipKey = `execute-task/${mid}/${sid}/${unitArg.toUpperCase()}`;
        }
        else if (unitArg.match(/^S\d+$/i) && mid) {
            skipKey = `plan-slice/${mid}/${unitArg.toUpperCase()}`;
        }
        else if (unitArg.includes("/")) {
            skipKey = `execute-task/${unitArg}`;
        }
    }
    if (keys.includes(skipKey)) {
        ctx.ui.notify(`Already skipped: ${skipKey}`, "info");
        return;
    }
    keys.push(skipKey);
    mkDir(pathJoin(basePath, ".nerf"), { recursive: true });
    writeFile(completedKeysFile, JSON.stringify(keys), "utf-8");
    ctx.ui.notify(`Skipped: ${skipKey}. Will not be dispatched in auto-mode.`, "success");
}
export async function handleDryRun(ctx, basePath) {
    const state = await deriveState(basePath);
    if (!state.activeMilestone) {
        ctx.ui.notify("No active milestone — nothing to dispatch.", "info");
        return;
    }
    const { getLedger, getProjectTotals, formatCost, formatTokenCount, loadLedgerFromDisk } = await import("./metrics.js");
    const { loadEffectiveNerfPreferences: loadPrefs } = await import("./preferences.js");
    const { formatDuration } = await import("../shared/format-utils.js");
    const ledger = getLedger();
    const units = ledger?.units ?? loadLedgerFromDisk(basePath)?.units ?? [];
    const prefs = loadPrefs()?.preferences;
    let nextType = "unknown";
    let nextId = "unknown";
    const mid = state.activeMilestone.id;
    const midTitle = state.activeMilestone.title;
    if (state.phase === "pre-planning") {
        nextType = "research-milestone";
        nextId = mid;
    }
    else if (state.phase === "planning" && state.activeSlice) {
        nextType = "plan-slice";
        nextId = `${mid}/${state.activeSlice.id}`;
    }
    else if (state.phase === "executing" && state.activeTask && state.activeSlice) {
        nextType = "execute-task";
        nextId = `${mid}/${state.activeSlice.id}/${state.activeTask.id}`;
    }
    else if (state.phase === "summarizing" && state.activeSlice) {
        nextType = "complete-slice";
        nextId = `${mid}/${state.activeSlice.id}`;
    }
    else if (state.phase === "completing-milestone") {
        nextType = "complete-milestone";
        nextId = mid;
    }
    else {
        nextType = state.phase;
        nextId = mid;
    }
    const sameTypeUnits = units.filter(u => u.type === nextType);
    const avgCost = sameTypeUnits.length > 0
        ? sameTypeUnits.reduce((s, u) => s + u.cost, 0) / sameTypeUnits.length
        : null;
    const avgDuration = sameTypeUnits.length > 0
        ? sameTypeUnits.reduce((s, u) => s + (u.finishedAt - u.startedAt), 0) / sameTypeUnits.length
        : null;
    const totals = units.length > 0 ? getProjectTotals(units) : null;
    const budgetRemaining = prefs?.budget_ceiling && totals
        ? prefs.budget_ceiling - totals.cost
        : null;
    const lines = [
        `Dry-run preview:`,
        ``,
        `  Next unit:     ${nextType}`,
        `  ID:            ${nextId}`,
        `  Milestone:     ${mid}: ${midTitle}`,
        `  Phase:         ${state.phase}`,
        `  Est. cost:     ${avgCost !== null ? `${formatCost(avgCost)} (avg of ${sameTypeUnits.length} similar)` : "unknown (first of this type)"}`,
        `  Est. duration: ${avgDuration !== null ? formatDuration(avgDuration) : "unknown"}`,
        `  Spent so far:  ${totals ? formatCost(totals.cost) : "$0"}`,
        `  Budget left:   ${budgetRemaining !== null ? formatCost(budgetRemaining) : "no ceiling set"}`,
    ];
    if (state.progress) {
        const p = state.progress;
        lines.push(`  Progress:      ${p.tasks?.done ?? 0}/${p.tasks?.total ?? "?"} tasks, ${p.slices?.done ?? 0}/${p.slices?.total ?? "?"} slices`);
    }
    ctx.ui.notify(lines.join("\n"), "info");
}
export async function handleCleanupProjects(args, ctx) {
    const { readdirSync, existsSync: fsExists, rmSync: fsRmSync } = await import("node:fs");
    const { join: pathJoin } = await import("node:path");
    const { readRepoMeta, externalProjectsRoot } = await import("./repo-identity.js");
    const fix = args.includes("--fix");
    const projectsDir = externalProjectsRoot();
    if (!fsExists(projectsDir)) {
        ctx.ui.notify(`No project-state directory found at ${projectsDir} — nothing to clean up.`, "info");
        return;
    }
    let hashList;
    try {
        hashList = readdirSync(projectsDir, { withFileTypes: true })
            .filter(e => e.isDirectory())
            .map(e => e.name);
    }
    catch {
        ctx.ui.notify(`Failed to read project-state directory at ${projectsDir}.`, "error");
        return;
    }
    if (hashList.length === 0) {
        ctx.ui.notify(`Project-state directory is empty (${projectsDir}) — nothing to clean up.`, "info");
        return;
    }
    const active = [];
    const orphaned = [];
    const unknown = [];
    for (const hash of hashList) {
        const dirPath = pathJoin(projectsDir, hash);
        const meta = readRepoMeta(dirPath);
        if (!meta) {
            unknown.push(hash);
            continue;
        }
        const entry = { hash, gitRoot: meta.gitRoot, remoteUrl: meta.remoteUrl };
        if (fsExists(meta.gitRoot)) {
            active.push(entry);
        }
        else {
            orphaned.push(entry);
        }
    }
    const pl = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
    const lines = [
        `${projectsDir}  ${pl(hashList.length, "project state director")}${hashList.length === 1 ? "y" : "ies"}`,
        "",
    ];
    if (active.length > 0) {
        lines.push(`Active (${active.length}) — git root present on disk:`);
        for (const e of active) {
            const remote = e.remoteUrl ? `  [${e.remoteUrl}]` : "";
            lines.push(`  + ${e.hash}  ${e.gitRoot}${remote}`);
        }
        lines.push("");
    }
    if (orphaned.length > 0) {
        lines.push(`Orphaned (${orphaned.length}) — git root no longer exists:`);
        for (const e of orphaned) {
            const remote = e.remoteUrl ? `  [${e.remoteUrl}]` : "";
            lines.push(`  - ${e.hash}  ${e.gitRoot}${remote}`);
        }
        lines.push("");
    }
    if (unknown.length > 0) {
        lines.push(`Unknown (${unknown.length}) — no metadata yet:`);
        for (const h of unknown) {
            lines.push(`  ? ${h}  (open that project in NERF once to register metadata)`);
        }
        lines.push("");
    }
    if (orphaned.length === 0) {
        lines.push("No orphaned project state — all tracked repos are still present on disk.");
        if (!fix) {
            ctx.ui.notify(lines.join("\n"), "success");
            return;
        }
    }
    if (!fix && orphaned.length > 0) {
        lines.push(`Run /nerf cleanup projects --fix to permanently delete ${pl(orphaned.length, "orphaned director")}${orphaned.length === 1 ? "y" : "ies"}.`);
        ctx.ui.notify(lines.join("\n"), "warning");
        return;
    }
    if (fix && orphaned.length > 0) {
        let removed = 0;
        const failed = [];
        for (const e of orphaned) {
            try {
                fsRmSync(pathJoin(projectsDir, e.hash), { recursive: true, force: true });
                removed++;
            }
            catch {
                failed.push(e.hash);
            }
        }
        lines.push(`Removed ${pl(removed, "orphaned director")}${removed === 1 ? "y" : "ies"}.`);
        if (failed.length > 0) {
            lines.push(`Failed to remove: ${failed.join(", ")}`);
        }
        ctx.ui.notify(lines.join("\n"), removed > 0 ? "success" : "warning");
        return;
    }
    ctx.ui.notify(lines.join("\n"), "info");
}
/**
 * `nerf recover` — Reconstruct DB hierarchy state from rendered markdown on disk.
 *
 * Deletes milestones, slices, and tasks table rows (preserves decisions,
 * requirements, artifacts, memories), re-runs `migrateHierarchyToDb()` to
 * repopulate from markdown, then calls `deriveState()` to verify sanity.
 *
 * Prints counts of recovered items and the resulting project phase.
 */
export async function handleRecover(ctx, basePath) {
    const { isDbAvailable: dbAvailable, _getAdapter, transaction: dbTransaction } = await import("./nerf-db.js");
    const { migrateHierarchyToDb } = await import("./md-importer.js");
    const { invalidateStateCache } = await import("./state.js");
    if (!dbAvailable()) {
        ctx.ui.notify("nerf recover: No database open. Run a NERF command first to initialize the DB.", "error");
        return;
    }
    try {
        // 1. Delete + re-populate inside a single transaction for atomicity
        const db = _getAdapter();
        const counts = dbTransaction(() => {
            db.exec("DELETE FROM tasks");
            db.exec("DELETE FROM slices");
            db.exec("DELETE FROM milestones");
            return migrateHierarchyToDb(basePath);
        });
        // 3. Invalidate state cache so deriveState() picks up fresh DB data
        invalidateStateCache();
        // 4. Derive state to verify sanity
        const state = await deriveState(basePath);
        // 5. Report
        const lines = [
            `nerf recover: reconstructed hierarchy from markdown`,
            `  Milestones: ${counts.milestones}`,
            `  Slices:     ${counts.slices}`,
            `  Tasks:      ${counts.tasks}`,
            ``,
            `  Phase:      ${state.phase}`,
        ];
        if (state.activeMilestone) {
            lines.push(`  Active:     ${state.activeMilestone.id}: ${state.activeMilestone.title}`);
        }
        if (state.activeSlice) {
            lines.push(`  Slice:      ${state.activeSlice.id}: ${state.activeSlice.title}`);
        }
        if (state.activeTask) {
            lines.push(`  Task:       ${state.activeTask.id}: ${state.activeTask.title}`);
        }
        process.stderr.write(`nerf-recover: recovered ${counts.milestones}M/${counts.slices}S/${counts.tasks}T hierarchy\n`);
        ctx.ui.notify(lines.join("\n"), "success");
    }
    catch (err) {
        const msg = err instanceof Error ? err.message : String(err);
        process.stderr.write(`nerf-recover: failed: ${msg}\n`);
        ctx.ui.notify(`nerf recover failed: ${msg}`, "error");
    }
}
