/**
 * commands/worktree.mjs — worktree detect + management commands (C29).
 *
 * Layer 1 (post-CLI-layering refactor, subtask 16). Returns the
 * {envelope, exitCode} tuple wrap() consumes; helper-level usage failures
 * throw WorktreeUsageError which the dispatcher converts into err()
 * envelopes.
 */
import { execFileSync, execSync, spawnSync } from 'node:child_process';
import { existsSync, mkdirSync, readdirSync, readFileSync } from 'node:fs';
import { join, resolve } from 'node:path';
import { getTaskIsolationConfig } from '../config/load.mjs';
import { detectWorktree } from '../git/worktree-detect.mjs';
import { parseFlags } from '../util/args.mjs';
import { resolveAptToolsBin } from '../util/bin-path.mjs';
import { getSharedLockDir } from '../util/project.mjs';
import { err, ok, UsageError } from '../util/result.mjs';
/**
 * Sentinel error class for helper-level usage failures inside cmdWorktree.
 * The dispatcher catches it and converts it into the standard err() envelope.
 */
class WorktreeUsageError extends UsageError {
    constructor(message) {
        super(message);
        this.name = 'WorktreeUsageError';
    }
}
export function cmdDetectWorktree(projectDir) {
    if (!projectDir)
        return err('Usage: apt-tools detect-worktree <project-dir>');
    const wt = detectWorktree(projectDir);
    return ok({
        status: 'ok',
        command: 'detect-worktree',
        ...wt,
        shared_lock_dir: getSharedLockDir(projectDir),
        recommendation: wt.is_worktree
            ? 'In a worktree — do NOT spawn nested worktrees. Use current worktree for execution. Locks coordinate via main repo.'
            : 'Not a worktree — safe to use worktree isolation if needed.',
    });
}
export function cmdWorktree(subcommand, projectDir, extraArgs) {
    if (!subcommand || !projectDir) {
        return err('Usage: apt-tools worktree <create|remove|list> <dir> [options]');
    }
    const targetDir = resolve(projectDir);
    // Validate identifiers to prevent path traversal and injection
    const SAFE_ID_PATTERN = /^[a-zA-Z0-9_-]+$/;
    const SAFE_BRANCH_PATTERN = /^[a-zA-Z0-9_\-/.]+$/;
    try {
        switch (subcommand) {
            case 'create':
                return doCreate(subcommand, targetDir, extraArgs, SAFE_ID_PATTERN, SAFE_BRANCH_PATTERN);
            case 'remove':
                return doRemove(subcommand, targetDir, extraArgs, SAFE_ID_PATTERN);
            case 'list':
                return doList(targetDir);
            default:
                return err('Unknown worktree subcommand: ' + subcommand + '. Use create, remove, or list.');
        }
    }
    catch (e) {
        if (e instanceof WorktreeUsageError)
            return err(e.message);
        throw e;
    }
}
function doCreate(_subcommand, targetDir, extraArgs, SAFE_ID_PATTERN, SAFE_BRANCH_PATTERN) {
    const flags = parseFlags(extraArgs);
    const subtaskId = flags.get('subtask');
    const taskId = flags.get('task');
    const pushOptIn = extraArgs.includes('--push');
    // C54 — --no-bootstrap opt-out. Captured here so the task-create
    // caller can forward it without us re-parsing `extraArgs` inside
    // the install block.
    const noBootstrap = extraArgs.includes('--no-bootstrap');
    // Task-mode create: isolates the whole pipeline of one task under
    // .aperant/worktrees/{scope}/{task-id}/ with branch {branch_prefix}/{task-id}.
    // Coexists with the legacy subtask-mode path below.
    if (taskId) {
        if (!SAFE_ID_PATTERN.test(taskId)) {
            throw new WorktreeUsageError('Invalid task ID: must match [a-zA-Z0-9_-]. Got: ' + taskId);
        }
        // C28 — scope propagation into worktree path. If --scope is not
        // passed, look it up from state.json so external callers keep
        // working without knowing the scope.
        let taskScope = flags.get('scope');
        if (!taskScope) {
            try {
                const sp = join(targetDir, '.aperant', 'state.json');
                if (existsSync(sp)) {
                    const st = JSON.parse(readFileSync(sp, 'utf-8'));
                    taskScope = st?.active_tasks?.[taskId]?.scope || 'project';
                }
                else {
                    taskScope = 'project';
                }
            }
            catch {
                taskScope = 'project';
            }
        }
        taskScope = (taskScope || 'project').trim() || 'project';
        if (!SAFE_ID_PATTERN.test(taskScope)) {
            throw new WorktreeUsageError('Invalid scope: must match [a-zA-Z0-9_-]. Got: ' + taskScope);
        }
        const isolation = getTaskIsolationConfig(targetDir);
        const branchPrefix = flags.get('branch-prefix') || isolation.branch_prefix || 'apt';
        const explicitBranch = flags.get('branch');
        const taskBranch = explicitBranch || `${branchPrefix}/${taskId}`;
        if (!SAFE_BRANCH_PATTERN.test(taskBranch)) {
            throw new WorktreeUsageError('Invalid branch name: must match [a-zA-Z0-9_-/.]. Got: ' + taskBranch);
        }
        // Resolve base branch: --from beats config beats default chain.
        const fromFlag = flags.get('from');
        const fromPref = fromFlag || isolation.from_branch || 'main';
        let baseBranch = fromPref;
        if (fromPref === 'current' || fromPref === 'HEAD') {
            // Explicit opt-in to chaining onto the user's current branch (AC8).
            try {
                baseBranch =
                    execSync('git rev-parse --abbrev-ref HEAD', {
                        cwd: targetDir,
                        encoding: 'utf-8',
                    }).trim() || 'HEAD';
            }
            catch {
                baseBranch = 'HEAD';
            }
        }
        else if (fromPref === 'main') {
            // Main-detection chain — local refs only, no network (per ID-02).
            const candidates = ['origin/HEAD', 'origin/main', 'origin/master', 'main', 'master'];
            let resolved = null;
            for (const ref of candidates) {
                try {
                    const out = execSync(`git rev-parse --verify ${ref}`, {
                        cwd: targetDir,
                        encoding: 'utf-8',
                        stdio: ['ignore', 'pipe', 'pipe'],
                    }).trim();
                    if (out) {
                        // origin/HEAD is a symref — resolve it to e.g. `origin/main`
                        // so the base branch we hand to `git worktree add` is usable.
                        if (ref === 'origin/HEAD') {
                            try {
                                const sym = execSync('git symbolic-ref refs/remotes/origin/HEAD', {
                                    cwd: targetDir,
                                    encoding: 'utf-8',
                                    stdio: ['ignore', 'pipe', 'pipe'],
                                }).trim();
                                resolved = sym.replace(/^refs\/remotes\//, '') || ref;
                            }
                            catch {
                                resolved = ref;
                            }
                        }
                        else {
                            resolved = ref;
                        }
                        break;
                    }
                }
                catch {
                    // Ref doesn't exist — try the next one.
                }
            }
            if (resolved) {
                baseBranch = resolved;
            }
            else {
                // Degenerate repo (no main/master ref anywhere). Fall back to
                // current HEAD so users in detached or greenfield repos still
                // get a working worktree instead of a hard error.
                try {
                    baseBranch =
                        execSync('git rev-parse --abbrev-ref HEAD', {
                            cwd: targetDir,
                            encoding: 'utf-8',
                        }).trim() || 'HEAD';
                }
                catch {
                    baseBranch = 'HEAD';
                }
            }
        }
        // Any other explicit fromPref value (e.g. `origin/main`, `feature/foo`,
        // a SHA) is used as-is and falls through to SAFE_BRANCH_PATTERN below.
        if (!SAFE_BRANCH_PATTERN.test(baseBranch)) {
            throw new WorktreeUsageError('Invalid base branch: must match [a-zA-Z0-9_-/.]. Got: ' + baseBranch);
        }
        // Nested-worktree guard — same rule as subtask mode.
        const wt = detectWorktree(targetDir);
        if (wt.is_worktree) {
            throw new WorktreeUsageError('Cannot create worktree: already inside a worktree at ' +
                wt.worktree_root +
                '. Nested worktrees are not supported.');
        }
        const worktreePath = join(targetDir, '.aperant', 'worktrees', taskScope, taskId);
        if (existsSync(worktreePath)) {
            throw new WorktreeUsageError('Worktree already exists at ' +
                worktreePath +
                '. Remove it first with: apt-tools worktree remove <dir> --task ' +
                taskId);
        }
        mkdirSync(join(targetDir, '.aperant', 'worktrees', taskScope), { recursive: true });
        try {
            execFileSync('git', ['worktree', 'add', worktreePath, '-b', taskBranch, baseBranch], {
                cwd: targetDir,
                encoding: 'utf-8',
                stdio: 'pipe',
            });
        }
        catch (e) {
            throw new WorktreeUsageError('Failed to create worktree: ' + (e.stderr || e.message).trim());
        }
        // Best-effort push: honor --push flag or config.push_on_create.
        // Never abort on failure — users may have no network / no remote.
        const shouldPush = pushOptIn || isolation.push_on_create === true;
        let remoteTracking = false;
        let pushWarning = null;
        if (shouldPush) {
            try {
                execFileSync('git', ['push', '-u', 'origin', taskBranch], {
                    cwd: worktreePath,
                    encoding: 'utf-8',
                    stdio: 'pipe',
                });
                remoteTracking = true;
            }
            catch (e) {
                pushWarning = 'push failed: ' + (e.stderr || e.message).toString().trim().split('\n')[0];
            }
        }
        // C54 — Bootstrap hook. Parallel to push_on_create: best-effort
        // side-effect that never aborts the parent. On `bootstrap_worktree:
        // true` AND no `--no-bootstrap` flag, we shell out to
        // `apt-tools worktree-install` (spawnSync) so the child can
        // detach its own install children. Failure is surfaced as
        // `install_warning` — the worktree itself was created
        // successfully either way.
        let installBlock = null;
        let installWarning = null;
        if (isolation.bootstrap_worktree === true && !noBootstrap) {
            try {
                // D-03: resolveAptToolsBin centralizes the apt-tools binary
                // resolution (env override → process.argv[1] → monorepo →
                // consumer-install → $PATH). This file no longer reads
                // process.argv[1] directly.
                const wtInstallArgs = [
                    resolveAptToolsBin(targetDir),
                    'worktree-install',
                    targetDir,
                    '--task',
                    taskId,
                    '--worktree',
                    worktreePath,
                ];
                const installRes = spawnSync(process.execPath, wtInstallArgs, {
                    encoding: 'utf-8',
                });
                if (installRes.status === 0 && installRes.stdout) {
                    try {
                        const parsed = JSON.parse(installRes.stdout);
                        if (parsed && typeof parsed === 'object') {
                            installBlock = {
                                install_status: parsed.install_status ?? null,
                                install_log_path: parsed.install_log_path ?? null,
                                install_pid: parsed.install_pid ?? null,
                                install_ecosystems: parsed.install_ecosystems ?? [],
                                install_started_at: parsed.install_started_at ?? null,
                                install_finished_at: parsed.install_finished_at ?? null,
                                install_warning: parsed.install_warning ?? null,
                                // C54 review: forward the per-ecosystem breakdown and
                                // (on pre-spawn rejections) the list of scanner
                                // rejections so the task record mirrors everything
                                // worktree-install wrote to state directly. Without
                                // this, the stdout chain drops the fields even though
                                // they already exist in state.json from the child's
                                // updateState call; forwarding keeps the parsed-stdout
                                // merge in task.mjs consistent.
                                install_per_ecosystem: parsed.install_per_ecosystem && typeof parsed.install_per_ecosystem === 'object'
                                    ? parsed.install_per_ecosystem
                                    : null,
                                install_rejections: Array.isArray(parsed.install_rejections)
                                    ? parsed.install_rejections
                                    : null,
                            };
                            if (parsed.install_warning)
                                installWarning = parsed.install_warning;
                        }
                    }
                    catch (e) {
                        installWarning = `worktree-install parse failed: ${e.message}`;
                    }
                }
                else {
                    installWarning =
                        'worktree-install failed: ' +
                            (installRes.stderr || installRes.stdout || 'unknown')
                                .toString()
                                .trim()
                                .split('\n')
                                .pop();
                }
            }
            catch (e) {
                installWarning = `worktree-install invocation error: ${e.message}`;
            }
        }
        const createOut = {
            status: 'ok',
            command: 'worktree-create',
            mode: 'task',
            task_id: taskId,
            scope: taskScope,
            worktree_path: worktreePath,
            path: worktreePath,
            branch: taskBranch,
            base_branch: baseBranch,
            remote_tracking: remoteTracking,
            push_warning: pushWarning,
            created_at: new Date().toISOString(),
        };
        if (installBlock)
            createOut.install = installBlock;
        if (installWarning)
            createOut.install_warning = installWarning;
        return ok(createOut);
    }
    // Subtask-mode (legacy) create.
    const branch = flags.get('branch');
    if (!subtaskId || !branch) {
        throw new WorktreeUsageError('Usage: apt-tools worktree create <dir> --subtask <id> --branch <name>   OR   --task <id> [--branch <name>] [--from <base>] [--push]');
    }
    // Validate inputs against safe patterns
    if (!SAFE_ID_PATTERN.test(subtaskId)) {
        throw new WorktreeUsageError('Invalid subtask ID: must match [a-zA-Z0-9_-]. Got: ' + subtaskId);
    }
    if (!SAFE_BRANCH_PATTERN.test(branch)) {
        throw new WorktreeUsageError('Invalid branch name: must match [a-zA-Z0-9_-/.]. Got: ' + branch);
    }
    // Prevent nested worktrees
    const wt = detectWorktree(targetDir);
    if (wt.is_worktree) {
        throw new WorktreeUsageError('Cannot create worktree: already inside a worktree at ' +
            wt.worktree_root +
            '. Nested worktrees are not supported.');
    }
    const worktreePath = join(targetDir, '.aperant', 'worktrees', subtaskId);
    // Check if worktree already exists
    if (existsSync(worktreePath)) {
        throw new WorktreeUsageError('Worktree already exists at ' +
            worktreePath +
            '. Remove it first with: apt-tools worktree remove <dir> --subtask ' +
            subtaskId);
    }
    // Ensure parent directory exists
    mkdirSync(join(targetDir, '.aperant', 'worktrees'), { recursive: true });
    try {
        execFileSync('git', ['worktree', 'add', worktreePath, '-b', branch], {
            cwd: targetDir,
            encoding: 'utf-8',
            stdio: 'pipe',
        });
    }
    catch (e) {
        throw new WorktreeUsageError('Failed to create worktree: ' + (e.stderr || e.message).trim());
    }
    return ok({
        status: 'ok',
        command: 'worktree-create',
        subtask_id: subtaskId,
        path: worktreePath,
        branch: branch,
        created_at: new Date().toISOString(),
    });
}
function doRemove(_subcommand, targetDir, extraArgs, SAFE_ID_PATTERN) {
    const flags = parseFlags(extraArgs);
    const subtaskId = flags.get('subtask');
    const taskId = flags.get('task');
    const force = extraArgs.includes('--force');
    const deleteBranch = extraArgs.includes('--delete-branch');
    // Task-mode remove.
    if (taskId) {
        if (!SAFE_ID_PATTERN.test(taskId)) {
            throw new WorktreeUsageError('Invalid task ID: must match [a-zA-Z0-9_-]. Got: ' + taskId);
        }
        // C28 — resolve scope for {scope}/{task-id} path. Accept --scope,
        // else read state.json, else fall back to scanning the worktree
        // dir (handles orphaned worktrees whose task record is gone).
        let taskScope = flags.get('scope') || null;
        if (!taskScope) {
            try {
                const sp = join(targetDir, '.aperant', 'state.json');
                if (existsSync(sp)) {
                    const st = JSON.parse(readFileSync(sp, 'utf-8'));
                    taskScope = st?.active_tasks?.[taskId]?.scope || null;
                }
            }
            catch {
                /* ignore */
            }
        }
        if (!taskScope) {
            // Scan scope subdirs for a match.
            try {
                const root = join(targetDir, '.aperant', 'worktrees');
                if (existsSync(root)) {
                    for (const entry of readdirSync(root, { withFileTypes: true })) {
                        if (entry.isDirectory() && existsSync(join(root, entry.name, taskId))) {
                            taskScope = entry.name;
                            break;
                        }
                    }
                }
            }
            catch {
                /* ignore */
            }
        }
        taskScope = taskScope || 'project';
        const worktreePath = join(targetDir, '.aperant', 'worktrees', taskScope, taskId);
        const args = ['worktree', 'remove', worktreePath];
        if (force)
            args.push('--force');
        try {
            execFileSync('git', args, { cwd: targetDir, encoding: 'utf-8', stdio: 'pipe' });
        }
        catch (e) {
            throw new WorktreeUsageError('Failed to remove worktree: ' + (e.stderr || e.message).trim());
        }
        let branchDeleted = false;
        if (deleteBranch) {
            const isolation = getTaskIsolationConfig(targetDir);
            const taskBranch = `${isolation.branch_prefix || 'apt'}/${taskId}`;
            try {
                execFileSync('git', ['branch', '-D', taskBranch], {
                    cwd: targetDir,
                    encoding: 'utf-8',
                    stdio: 'pipe',
                });
                branchDeleted = true;
            }
            catch {
                /* best-effort */
            }
        }
        return ok({
            status: 'ok',
            command: 'worktree-remove',
            mode: 'task',
            task_id: taskId,
            scope: taskScope,
            removed: worktreePath,
            branch_deleted: branchDeleted,
        });
    }
    if (!subtaskId) {
        throw new WorktreeUsageError('Usage: apt-tools worktree remove <dir> --subtask <id> [--force]   OR   --task <id> [--force] [--delete-branch]');
    }
    if (!SAFE_ID_PATTERN.test(subtaskId)) {
        throw new WorktreeUsageError('Invalid subtask ID: must match [a-zA-Z0-9_-]. Got: ' + subtaskId);
    }
    const worktreePath = join(targetDir, '.aperant', 'worktrees', subtaskId);
    const args = ['worktree', 'remove', worktreePath];
    if (force)
        args.push('--force');
    try {
        execFileSync('git', args, { cwd: targetDir, encoding: 'utf-8', stdio: 'pipe' });
    }
    catch (e) {
        throw new WorktreeUsageError('Failed to remove worktree: ' + (e.stderr || e.message).trim());
    }
    return ok({
        status: 'ok',
        command: 'worktree-remove',
        subtask_id: subtaskId,
        removed: worktreePath,
    });
}
function doList(targetDir) {
    let rawOutput;
    try {
        rawOutput = execSync('git worktree list --porcelain', {
            cwd: targetDir,
            encoding: 'utf-8',
            stdio: 'pipe',
        });
    }
    catch (e) {
        throw new WorktreeUsageError('Failed to list worktrees: ' + (e.stderr || e.message).trim());
    }
    // Parse porcelain output — each worktree block separated by blank line
    const worktrees = [];
    const aperantPrefix = join(targetDir, '.aperant', 'worktrees');
    const blocks = rawOutput.trim().split('\n\n');
    for (const block of blocks) {
        const lines = block.trim().split('\n');
        let wtPath = null;
        let wtBranch = null;
        for (const line of lines) {
            if (line.startsWith('worktree '))
                wtPath = line.slice('worktree '.length);
            if (line.startsWith('branch '))
                wtBranch = line.slice('branch '.length).replace('refs/heads/', '');
        }
        // Only include worktrees under .aperant/worktrees/
        if (wtPath && wtPath.startsWith(aperantPrefix)) {
            const relative = wtPath.slice(aperantPrefix.length + 1);
            const segments = relative.split(/[\\/]/).filter(Boolean);
            // C28 — two-level layout is `{scope}/{task-id}`. Legacy single-
            // level layout `{subtask-id}` is still supported for back-compat
            // on flat worktrees. We distinguish by segment count.
            if (segments.length >= 2) {
                worktrees.push({
                    scope: segments[0],
                    task_id: segments.slice(1).join('/'),
                    subtask_id: segments.slice(1).join('/'),
                    path: wtPath,
                    branch: wtBranch || 'unknown',
                });
            }
            else {
                worktrees.push({
                    subtask_id: relative,
                    path: wtPath,
                    branch: wtBranch || 'unknown',
                });
            }
        }
    }
    return ok({
        status: 'ok',
        command: 'worktree-list',
        worktrees: worktrees,
    });
}
//# sourceMappingURL=worktree.mjs.map