#!/usr/bin/env node
// Copyright (c) 2026 Hellmai Ltd
// SPDX-License-Identifier: AGPL-3.0-only
/**
 * WU Cleanup Helper
 *
 * Cleans up worktree and branch after PR merge (PR-based completion workflow).
 *
 * Sequence:
 * 1) Verify PR is merged (via gh API; no merge-base fallback)
 * 2) Remove worktree (if exists)
 * 3) Delete lane branch (local + remote)
 *
 * Idempotent: Safe no-op if already cleaned.
 *
 * Usage:
 *   pnpm wu:cleanup --id WU-703
 *   pnpm wu:cleanup --artifacts
 */
import { execSync } from 'node:child_process';
import { createGitForPath, getGitForCwd } from '@lumenflow/core/git-adapter';
import { existsSync } from 'node:fs';
import path from 'node:path';
import { createWUParser, WU_OPTIONS } from '@lumenflow/core/arg-parser';
import { die } from '@lumenflow/core/error-handler';
import { cleanupWorktreeBuildArtifacts } from '@lumenflow/core/rebase-artifact-cleanup';
import { detectCurrentWorktree } from '@lumenflow/core/wu-done-validators';
import { WU_PATHS } from '@lumenflow/core/wu-paths';
import { readWU } from '@lumenflow/core/wu-yaml';
import { isGhCliAvailable } from '@lumenflow/core/wu-done-pr';
import { BOX, CLEANUP_GUARD, EXIT_CODES, FILE_SYSTEM, LOG_PREFIX, REMOTES, STRING_LITERALS, WU_STATUS, } from '@lumenflow/core/wu-constants';
// WU-2278: Import ownership validation for cross-agent protection
import { validateWorktreeOwnership } from '@lumenflow/core/worktree-ownership';
// WU-1590: Cloud cleanup helpers for branch-pr mode
import { resolveCleanupBranch, shouldSkipWorktreeChecks, isCloudManagedBranch, } from './wu-cleanup-cloud.js';
const CLEANUP_OPTIONS = {
    artifacts: {
        name: 'artifacts',
        flags: '--artifacts',
        description: 'Remove build artifacts (dist, tsbuildinfo) in current worktree',
    },
};
export const CLEANUP_GUARD_REASONS = CLEANUP_GUARD.REASONS;
export function evaluateCleanupGuards({ hasUncommittedChanges, hasUnpushedCommits, hasStamp, yamlStatus, ghAvailable, prMerged, }) {
    if (hasUncommittedChanges) {
        return {
            allowed: false,
            reason: CLEANUP_GUARD_REASONS.UNCOMMITTED_CHANGES,
        };
    }
    if (hasUnpushedCommits) {
        return { allowed: false, reason: CLEANUP_GUARD_REASONS.UNPUSHED_COMMITS };
    }
    if (yamlStatus !== WU_STATUS.DONE) {
        return { allowed: false, reason: CLEANUP_GUARD_REASONS.STATUS_NOT_DONE };
    }
    if (!hasStamp) {
        return { allowed: false, reason: CLEANUP_GUARD_REASONS.MISSING_STAMP };
    }
    if (ghAvailable && prMerged !== true) {
        return { allowed: false, reason: CLEANUP_GUARD_REASONS.PR_NOT_MERGED };
    }
    return { allowed: true, reason: null };
}
// Help text is now auto-generated by commander via createWUParser
/**
 * Verify if a PR for the given branch has been merged.
 * WU-1141: Fixed to query closed/merged PRs by adding state=all parameter.
 * The GitHub API /pulls endpoint returns only open PRs by default, so merged
 * PRs were not being found, causing false "PR not merged" errors.
 */
export async function verifyPRMerged(laneBranch) {
    if (!isGhCliAvailable()) {
        return { merged: null, method: 'gh_unavailable' };
    }
    let ghResult;
    try {
        // WU-1141: Add state=all to include closed/merged PRs
        // Without this parameter, the API only returns open PRs,
        // causing merged PRs to not be found and verification to fail
        ghResult = execSync(`gh api "repos/:owner/:repo/pulls?state=all" -q '.[] | select(.head.ref == "${laneBranch}") | .merged'`, { encoding: FILE_SYSTEM.UTF8 }).trim();
    }
    catch {
        ghResult = STRING_LITERALS.EMPTY;
    }
    if (ghResult === 'true') {
        return { merged: true, method: 'gh_api' };
    }
    if (ghResult === 'false') {
        return { merged: false, method: 'gh_api' };
    }
    return { merged: null, method: 'gh_api' };
}
async function removeWorktree(worktreePath) {
    if (!existsSync(worktreePath)) {
        console.log(`[wu-cleanup] ✓ Worktree already removed: ${worktreePath}`);
        return;
    }
    console.log(`[wu-cleanup] Removing worktree: ${worktreePath}`);
    await getGitForCwd().worktreeRemove(worktreePath, { force: true });
    console.log('[wu-cleanup] ✓ Worktree removed');
}
async function deleteBranch(laneBranch) {
    // Delete local branch
    const localExists = await getGitForCwd().branchExists(laneBranch);
    if (localExists) {
        console.log(`[wu-cleanup] Deleting local branch: ${laneBranch}`);
        await getGitForCwd().deleteBranch(laneBranch, { force: true });
        console.log('[wu-cleanup] ✓ Local branch deleted');
    }
    else {
        console.log(`[wu-cleanup] ✓ Local branch already deleted: ${laneBranch}`);
    }
    // Delete remote branch
    const remoteExists = await getGitForCwd().raw([
        'ls-remote',
        '--heads',
        REMOTES.ORIGIN,
        laneBranch,
    ]);
    if (remoteExists) {
        console.log(`[wu-cleanup] Deleting remote branch: ${laneBranch}`);
        await getGitForCwd().raw(['push', REMOTES.ORIGIN, '--delete', laneBranch]);
        console.log('[wu-cleanup] ✓ Remote branch deleted');
    }
    else {
        console.log(`[wu-cleanup] ✓ Remote branch already deleted: ${laneBranch}`);
    }
}
async function cleanupArtifactsInWorktree() {
    const worktreePath = detectCurrentWorktree();
    if (!worktreePath) {
        die(`${LOG_PREFIX.CLEANUP} Not in a worktree.\n\n` +
            'Run this command from inside a worktree:\n' +
            '  cd worktrees/<lane>-wu-xxx\n' +
            '  pnpm wu:cleanup --artifacts\n');
    }
    console.log(`${LOG_PREFIX.CLEANUP} Cleaning build artifacts in ${worktreePath}`);
    const result = await cleanupWorktreeBuildArtifacts(worktreePath);
    if (result.removedCount === 0) {
        console.log(`${LOG_PREFIX.CLEANUP} ✓ No build artifacts found`);
        return;
    }
    if (result.distDirectories.length > 0) {
        console.log(`${LOG_PREFIX.CLEANUP} Removed dist directories:`);
        for (const dir of result.distDirectories) {
            console.log(`  - ${dir}`);
        }
    }
    if (result.tsbuildinfoFiles.length > 0) {
        console.log(`${LOG_PREFIX.CLEANUP} Removed tsbuildinfo files:`);
        for (const file of result.tsbuildinfoFiles) {
            console.log(`  - ${file}`);
        }
    }
    console.log(`${LOG_PREFIX.CLEANUP} ✓ Build artifact cleanup complete`);
}
async function hasUncommittedChanges(worktreePath) {
    if (!existsSync(worktreePath)) {
        return false;
    }
    const git = createGitForPath(worktreePath);
    const status = await git.getStatus();
    return status.length > 0;
}
async function hasUnpushedCommits(worktreePath) {
    if (!existsSync(worktreePath)) {
        return false;
    }
    const git = createGitForPath(worktreePath);
    try {
        // WU-2513: Compare against origin/<branch> explicitly instead of @{u}
        // The upstream tracking ref may point to origin/main (set by wu:claim),
        // not origin/<lane-branch>, causing false "unpushed commits" reports
        // even when the lane branch is fully pushed.
        const currentBranch = (await git.raw(['rev-parse', '--abbrev-ref', 'HEAD'])).trim();
        const remoteBranch = `origin/${currentBranch}`;
        try {
            // Fetch the remote branch to ensure we have the latest ref
            await git.raw(['fetch', 'origin', currentBranch]);
        }
        catch {
            // Remote branch may not exist — treat as unpushed
            return true;
        }
        const unpushed = (await git.raw(['log', `${remoteBranch}..HEAD`, '--oneline'])).trim();
        return unpushed.length > 0;
    }
    catch {
        // If we can't determine push status, allow cleanup to proceed
        // rather than blocking on ambiguous state
        return false;
    }
}
function hasStampFile(wuId) {
    const stampPath = path.join(process.cwd(), WU_PATHS.STAMP(wuId));
    return existsSync(stampPath);
}
export async function main() {
    const args = createWUParser({
        name: 'wu-cleanup',
        description: 'Clean up worktree and branch after PR merge (PR-based completion workflow)',
        options: [WU_OPTIONS.id, CLEANUP_OPTIONS.artifacts],
        required: [],
        allowPositionalId: true,
    });
    if (args.artifacts) {
        await cleanupArtifactsInWorktree();
        return;
    }
    if (!args.id) {
        die(`${LOG_PREFIX.CLEANUP} Missing required --id (or use --artifacts).`);
    }
    const id = args.id.toUpperCase();
    const wu = readWU(WU_PATHS.WU(id), id);
    // Use kebab-case lane naming (match wu-claim.ts logic)
    const laneK = wu.lane
        ?.toLowerCase()
        .trim()
        .replace(/[^a-z0-9]+/g, '-')
        .replace(/^-+|-+$/g, '') || 'unknown';
    // args.id is already "wu-706", don't double-prepend "wu-"
    const idK = args.id.toLowerCase();
    // WU-1590: Resolve cleanup branch from claimed_branch (cloud) or lane-derived fallback
    const laneBranch = resolveCleanupBranch({
        claimed_branch: wu.claimed_branch,
        lane: wu.lane || '',
        id: idK,
    });
    const worktreePath = path.join('worktrees', `${laneK}-${idK}`);
    const absoluteWorktreePath = path.resolve(worktreePath);
    console.log(`[wu-cleanup] Cleaning up ${args.id} (${wu.title})`);
    console.log(`[wu-cleanup] Lane: ${wu.lane}`);
    console.log(`[wu-cleanup] Branch: ${laneBranch}`);
    console.log(`[wu-cleanup] Worktree: ${worktreePath}`);
    console.log();
    // WU-1590: Determine if worktree checks should be skipped (branch-pr mode)
    const skipWorktree = shouldSkipWorktreeChecks(wu);
    // WU-2278: Validate worktree ownership before cleanup
    // Prevents cross-agent worktree deletion (skip for branch-pr: no worktree)
    if (!skipWorktree) {
        const ownershipResult = validateWorktreeOwnership({ worktreePath, wuId: id });
        if (!ownershipResult.valid) {
            console.error();
            console.error(BOX.TOP);
            console.error('║  OWNERSHIP VALIDATION FAILED');
            console.error(BOX.MID);
            console.error(`║  ${ownershipResult.error}`);
            console.error('║');
            console.error("║  This prevents accidentally deleting another agent's worktree.");
            console.error(BOX.BOT);
            process.exit(EXIT_CODES.ERROR);
        }
    }
    const cleanupCheck = {
        // WU-1590: Skip worktree-specific checks in branch-pr mode (no worktree to check)
        hasUncommittedChanges: skipWorktree ? false : await hasUncommittedChanges(absoluteWorktreePath),
        hasUnpushedCommits: skipWorktree ? false : await hasUnpushedCommits(absoluteWorktreePath),
        hasStamp: hasStampFile(id),
        yamlStatus: wu.status,
        ghAvailable: isGhCliAvailable(),
        prMerged: null,
    };
    if (cleanupCheck.ghAvailable) {
        console.log(`${LOG_PREFIX.CLEANUP} ${CLEANUP_GUARD.PR_CHECK.START}`);
        const { merged, method } = await verifyPRMerged(laneBranch);
        cleanupCheck.prMerged = merged;
        console.log(`${LOG_PREFIX.CLEANUP} ${CLEANUP_GUARD.PR_CHECK.RESULT} ${method}`);
        console.log();
    }
    const guardResult = evaluateCleanupGuards(cleanupCheck);
    if (!guardResult.allowed) {
        const { reason } = guardResult;
        console.error();
        console.error(BOX.TOP);
        console.error(`${BOX.SIDE}  ${CLEANUP_GUARD.TITLES.BLOCKED}`);
        console.error(BOX.MID);
        console.error(`${BOX.SIDE}  ${CLEANUP_GUARD.MESSAGES[reason]}`);
        console.error(`${BOX.SIDE}`);
        console.error(`${BOX.SIDE}  ${CLEANUP_GUARD.TITLES.NEXT_STEPS}`);
        const steps = CLEANUP_GUARD.NEXT_STEPS[reason] || CLEANUP_GUARD.NEXT_STEPS.DEFAULT;
        for (const step of steps) {
            const line = step.appendId ? `${step.text} ${args.id}` : step.text;
            console.error(`${BOX.SIDE}  ${line}`);
        }
        console.error(BOX.BOT);
        process.exit(EXIT_CODES.ERROR);
    }
    // 2. Remove worktree (skip in branch-pr mode: no worktree exists)
    if (!skipWorktree) {
        await removeWorktree(worktreePath);
        console.log();
    }
    else {
        console.log('[wu-cleanup] Branch-PR mode: no worktree to remove');
    }
    // 3. Delete branch (local + remote)
    // WU-1590: Skip branch deletion for cloud-managed branches (claude/*, codex/*, etc.)
    if (await isCloudManagedBranch(laneBranch)) {
        console.log(`[wu-cleanup] Skipping branch deletion: ${laneBranch} is cloud-managed`);
    }
    else {
        await deleteBranch(laneBranch);
    }
    console.log();
    console.log(BOX.TOP);
    console.log('║  CLEANUP COMPLETE');
    console.log(BOX.MID);
    console.log(`║  WU: ${args.id}`);
    console.log('║  Worktree removed');
    console.log('║  Branch deleted (local + remote)');
    console.log(BOX.BOT);
}
// WU-1181: Use import.meta.main instead of process.argv[1] comparison
// The old pattern fails with pnpm symlinks because process.argv[1] is the symlink
// path but import.meta.url resolves to the real path - they never match
import { runCLI } from './cli-entry-point.js';
if (import.meta.main) {
    void runCLI(main);
}
//# sourceMappingURL=wu-cleanup.js.map