// tools/git/cleanup-ops.ts — Git cleanup: worktrees, branches, remote prune, stashes (SPEC-203)
import { existsSync } from 'node:fs';
import { readdir } from 'node:fs/promises';
import { join } from 'node:path';
import { git, resolveProjectPath } from './git-helpers.js';
import { compactResult, compactError } from '../output-formatter.js';
const WORKTREES_DIR = '.claude/worktrees';
const STALE_DAYS_THRESHOLD = 7;
const MILLISECONDS_PER_DAY = 86_400_000;
// ---------------------------------------------------------------------------
// Worktree helpers
// ---------------------------------------------------------------------------
/** Parse `git worktree list --porcelain` output into {path, branch} records. */
function parseWorktreeList(output) {
    const entries = [];
    let current = {};
    for (const line of output.split('\n')) {
        if (line.startsWith('worktree ')) {
            current = { path: line.slice('worktree '.length).trim() };
        }
        else if (line.startsWith('branch ')) {
            current.branch = line.slice('branch '.length).trim().replace('refs/heads/', '');
        }
        else if (line === '' && current.path) {
            if (current.branch) {
                entries.push({ path: current.path, branch: current.branch });
            }
            current = {};
        }
    }
    return entries;
}
/** Returns the set of branches already merged into develop (or main as fallback). */
async function getMergedBranches(projectPath) {
    const bases = ['develop', 'main', 'master'];
    for (const base of bases) {
        try {
            const { stdout } = await git(projectPath, ['branch', '--merged', base]);
            return new Set(stdout
                .split('\n')
                .map((b) => b.replace(/^\*?\s+/, '').trim())
                .filter(Boolean));
        }
        catch {
            // try next base
        }
    }
    return new Set();
}
/** Remove a worktree — idempotent (ignores "not a worktree" errors). */
async function removeWorktree(projectPath, worktreePath) {
    try {
        await git(projectPath, ['worktree', 'remove', '--force', worktreePath]);
    }
    catch (err) {
        const msg = err instanceof Error ? err.message : String(err);
        // Already gone — not an error
        if (!msg.includes('not a working tree') && !msg.includes('does not exist')) {
            throw err;
        }
    }
}
async function cleanupOrphanWorktrees(projectPath, mergedBranches) {
    const removed = [];
    const errors = [];
    let worktreeOutput;
    try {
        const { stdout } = await git(projectPath, ['worktree', 'list', '--porcelain']);
        worktreeOutput = stdout;
    }
    catch {
        return { removed, errors };
    }
    const worktrees = parseWorktreeList(worktreeOutput);
    const worktreesSubdir = join(projectPath, WORKTREES_DIR);
    for (const wt of worktrees) {
        // Never touch the main worktree (same path as projectPath)
        if (wt.path === projectPath) {
            continue;
        }
        const isInWorktreesDir = wt.path.startsWith(worktreesSubdir);
        const isMergedBranch = wt.branch && mergedBranches.has(wt.branch);
        if (isInWorktreesDir || isMergedBranch) {
            // Never remove if it's the current HEAD
            try {
                const { stdout: headBranch } = await git(projectPath, [
                    'rev-parse',
                    '--abbrev-ref',
                    'HEAD',
                ]);
                if (headBranch.trim() === wt.branch) {
                    continue;
                }
            }
            catch {
                // Skip safety check failure — do not remove
                continue;
            }
            try {
                await removeWorktree(projectPath, wt.path);
                removed.push(wt.path);
            }
            catch (err) {
                errors.push(`worktree ${wt.path}: ${err instanceof Error ? err.message : String(err)}`);
            }
        }
    }
    // Also prune stale worktree admin dirs (git worktree prune)
    try {
        await git(projectPath, ['worktree', 'prune']);
    }
    catch {
        // best-effort
    }
    return { removed, errors };
}
// ---------------------------------------------------------------------------
// Branch helpers
// ---------------------------------------------------------------------------
async function cleanupGoneBranches(projectPath) {
    const removed = [];
    const errors = [];
    let vvOutput;
    try {
        const { stdout } = await git(projectPath, ['branch', '-vv']);
        vvOutput = stdout;
    }
    catch {
        return { removed, errors };
    }
    // Lines containing ": gone]" mean upstream was deleted (git branch -vv format: [origin/...: gone])
    const goneBranches = vvOutput
        .split('\n')
        .filter((line) => line.includes(': gone]'))
        .map((line) => line
        .replace(/^\*?\s+/, '')
        .split(/\s+/)[0]
        ?.trim() ?? '')
        .filter(Boolean);
    // Resolve current branch only when needed to avoid an extra git call (AC-05)
    let currentBranch = '';
    if (goneBranches.length > 0) {
        try {
            const { stdout } = await git(projectPath, ['rev-parse', '--abbrev-ref', 'HEAD']);
            currentBranch = stdout.trim();
        }
        catch {
            // ignore
        }
    }
    for (const branch of goneBranches) {
        if (branch === currentBranch) {
            continue;
        }
        try {
            await git(projectPath, ['branch', '-d', branch]);
            removed.push(branch);
        }
        catch {
            // Force-delete if branch is not fully merged (upstream gone = safe)
            try {
                await git(projectPath, ['branch', '-D', branch]);
                removed.push(branch);
            }
            catch (err) {
                errors.push(`branch ${branch}: ${err instanceof Error ? err.message : String(err)}`);
            }
        }
    }
    return { removed, errors };
}
// ---------------------------------------------------------------------------
// Stash helpers
// ---------------------------------------------------------------------------
function parseDaysOld(stashMessage) {
    // git stash list format: stash@{0}: WIP on branch: hash message
    // We detect age from the stash index timestamp via git log on the stash ref
    // Since we can't get date from the message directly, we return 0 here
    // and use git log to get the actual date per stash ref in detectStaleStashes.
    void stashMessage;
    return 0;
}
async function detectStaleStashes(projectPath) {
    const stale = [];
    let listOutput;
    try {
        const { stdout } = await git(projectPath, ['stash', 'list']);
        listOutput = stdout;
    }
    catch {
        return stale;
    }
    const lines = listOutput.split('\n').filter(Boolean);
    const now = Date.now();
    for (const line of lines) {
        const match = /^(stash@\{(\d+)\}):\s+(.+)$/.exec(line);
        if (!match) {
            continue;
        }
        const [, ref, indexStr, message] = match;
        if (!ref || !indexStr || !message) {
            continue;
        }
        const index = parseInt(indexStr, 10);
        // Get commit date for this stash ref
        let daysOld = parseDaysOld(message);
        try {
            const { stdout: dateOut } = await git(projectPath, ['log', '-1', '--format=%ct', ref]);
            const epochSeconds = parseInt(dateOut.trim(), 10);
            if (!isNaN(epochSeconds)) {
                daysOld = Math.floor((now - epochSeconds * 1000) / MILLISECONDS_PER_DAY);
            }
        }
        catch {
            // Can't determine age — skip this stash
            continue;
        }
        if (daysOld >= STALE_DAYS_THRESHOLD) {
            stale.push({ index, ref, message, daysOld });
        }
    }
    return stale;
}
async function dropStaleStashes(projectPath, stale) {
    const errors = [];
    // Drop in reverse index order so indices stay valid
    const sorted = [...stale].sort((a, b) => b.index - a.index);
    let dropped = 0;
    for (const stash of sorted) {
        try {
            await git(projectPath, ['stash', 'drop', stash.ref]);
            dropped++;
        }
        catch (err) {
            errors.push(`stash ${stash.ref}: ${err instanceof Error ? err.message : String(err)}`);
        }
    }
    return { dropped, errors };
}
// ---------------------------------------------------------------------------
// Public handler
// ---------------------------------------------------------------------------
/**
 * AC-02: Eliminate gone branches + remote prune.
 * AC-03: List stale stashes (>7d); drop if force=true.
 * AC-05: Idempotent.
 * AC-06: Never deletes active branch or current HEAD.
 * AC-07: Returns detailed report.
 */
export async function handleCleanup(projectId, config) {
    const force = config?.force ?? false;
    const errors = [];
    let projectPath;
    try {
        projectPath = await resolveProjectPath(projectId);
    }
    catch (err) {
        return compactError(err instanceof Error ? err.message : String(err));
    }
    // 1. Remote prune
    let remotePruned = false;
    try {
        await git(projectPath, ['remote', 'prune', 'origin']);
        remotePruned = true;
    }
    catch (err) {
        errors.push(`remote prune: ${err instanceof Error ? err.message : String(err)}`);
    }
    // 2. Get merged branches (used for worktree and branch cleanup)
    const mergedBranches = await getMergedBranches(projectPath);
    // 3. Remove orphan worktrees
    const { removed: worktreesRemoved, errors: wtErrors } = await cleanupOrphanWorktrees(projectPath, mergedBranches);
    errors.push(...wtErrors);
    // 4. Remove branches with gone upstream
    const { removed: branchesRemoved, errors: branchErrors } = await cleanupGoneBranches(projectPath);
    errors.push(...branchErrors);
    // 5. Detect / drop stale stashes
    const staleStashes = await detectStaleStashes(projectPath);
    let stashesDropped = 0;
    if (force && staleStashes.length > 0) {
        const { dropped, errors: stashErrors } = await dropStaleStashes(projectPath, staleStashes);
        stashesDropped = dropped;
        errors.push(...stashErrors);
    }
    const messageParts = [
        `Worktrees removed: ${worktreesRemoved.length}`,
        `Branches removed: ${branchesRemoved.length}`,
        `Remote pruned: ${remotePruned ? 'yes' : 'no'}`,
        `Stale stashes (>${STALE_DAYS_THRESHOLD}d): ${staleStashes.length}`,
        ...(force ? [`Stashes dropped: ${stashesDropped}`] : ['Use force:true to drop stale stashes']),
        ...(errors.length > 0 ? [`Errors: ${errors.length}`] : []),
    ];
    const report = {
        action: 'cleanup',
        worktreesRemoved,
        branchesRemoved,
        remotePruned,
        stalesStashes: staleStashes,
        stashesDropped,
        errors,
        message: messageParts.join(' | '),
    };
    const lines = [report.message];
    if (worktreesRemoved.length > 0) {
        lines.push('', `**Worktrees removed**: ${worktreesRemoved.join(', ')}`);
    }
    if (branchesRemoved.length > 0) {
        lines.push('', `**Branches removed**: ${branchesRemoved.join(', ')}`);
    }
    if (staleStashes.length > 0) {
        lines.push('', `**Stale stashes**: ${staleStashes.map((s) => `${s.ref} (${String(s.daysOld)}d)`).join(', ')}`);
    }
    if (errors.length > 0) {
        lines.push('', `**Errors**: ${errors.join('; ')}`);
    }
    return compactResult(lines.join('\n'));
}
// ---------------------------------------------------------------------------
// Spec-done cleanup (AC-01) — called from update-status-actions.ts
// ---------------------------------------------------------------------------
/** Checks whether a worktree path for a given spec exists in .claude/worktrees/ */
async function findSpecWorktree(projectPath, specId) {
    const worktreesPath = join(projectPath, WORKTREES_DIR);
    if (!existsSync(worktreesPath)) {
        return null;
    }
    try {
        const entries = await readdir(worktreesPath);
        const match = entries.find((e) => e.toLowerCase().includes(specId.toLowerCase()));
        return match ? join(worktreesPath, match) : null;
    }
    catch {
        return null;
    }
}
/**
 * AC-01: When a spec is marked done, clean its worktree, local branch, and remote branch.
 * Best-effort — never throws. Caller should fire-and-forget or log result.
 */
export async function cleanupSpecOnDone(projectPath, specId, gitBranch) {
    const result = {
        worktreeRemoved: null,
        localBranchRemoved: null,
        remoteBranchRemoved: null,
        errors: [],
    };
    // 1. Remove worktree if it exists
    const worktreePath = await findSpecWorktree(projectPath, specId);
    if (worktreePath) {
        try {
            await removeWorktree(projectPath, worktreePath);
            result.worktreeRemoved = worktreePath;
        }
        catch (err) {
            result.errors.push(`worktree: ${err instanceof Error ? err.message : String(err)}`);
        }
    }
    if (!gitBranch) {
        return result;
    }
    // 2. Remove local branch if merged
    try {
        const { stdout } = await git(projectPath, ['branch', '--merged', 'develop']);
        const merged = new Set(stdout
            .split('\n')
            .map((b) => b.replace(/^\*?\s+/, '').trim())
            .filter(Boolean));
        const isMerged = merged.has(gitBranch);
        // Also check main as fallback
        let isMergedMain = false;
        if (!isMerged) {
            try {
                const { stdout: mainOut } = await git(projectPath, ['branch', '--merged', 'main']);
                const mergedMain = new Set(mainOut
                    .split('\n')
                    .map((b) => b.replace(/^\*?\s+/, '').trim())
                    .filter(Boolean));
                isMergedMain = mergedMain.has(gitBranch);
            }
            catch {
                // ignore
            }
        }
        if (isMerged || isMergedMain) {
            // Never delete current branch
            const { stdout: headOut } = await git(projectPath, ['rev-parse', '--abbrev-ref', 'HEAD']);
            if (headOut.trim() !== gitBranch) {
                try {
                    await git(projectPath, ['branch', '-d', gitBranch]);
                    result.localBranchRemoved = gitBranch;
                }
                catch {
                    // Force-delete if needed
                    try {
                        await git(projectPath, ['branch', '-D', gitBranch]);
                        result.localBranchRemoved = gitBranch;
                    }
                    catch (err) {
                        result.errors.push(`local branch: ${err instanceof Error ? err.message : String(err)}`);
                    }
                }
            }
        }
    }
    catch (err) {
        result.errors.push(`merged check: ${err instanceof Error ? err.message : String(err)}`);
    }
    // 3. Remove remote branch if it exists
    try {
        const { stdout } = await git(projectPath, ['ls-remote', '--heads', 'origin', gitBranch]);
        if (stdout.trim()) {
            await git(projectPath, ['push', 'origin', '--delete', gitBranch]);
            result.remoteBranchRemoved = `origin/${gitBranch}`;
        }
    }
    catch (err) {
        result.errors.push(`remote branch: ${err instanceof Error ? err.message : String(err)}`);
    }
    return result;
}
//# sourceMappingURL=cleanup-ops.js.map