/**
 * NERF Worktree Manager
 *
 * Creates and manages git worktrees under .nerf/worktrees/<name>/.
 * Each worktree gets its own branch (worktree/<name>) and a full
 * working copy of the project, enabling parallel work streams.
 *
 * The merge helper compares .nerf/ artifacts between a worktree and
 * the main branch, then dispatches an LLM-guided merge flow.
 *
 * Flow:
 *   1. create()  — git worktree add .nerf/worktrees/<name> -b worktree/<name>
 *   2. user works in the worktree (new plans, milestones, etc.)
 *   3. merge()   — LLM-guided reconciliation of .nerf/ artifacts back to main
 *   4. remove()  — git worktree remove + branch cleanup
 */
import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync } from "node:fs";
import { execFileSync } from "node:child_process";
import { join, resolve, sep } from "node:path";
import { NerfError, NERF_PARSE_ERROR, NERF_STALE_STATE, NERF_LOCK_HELD, NERF_GIT_ERROR, NERF_MERGE_CONFLICT } from "./errors.js";
import { logWarning } from "./workflow-logger.js";
import { nativeBranchDelete, nativeBranchExists, nativeBranchForceReset, nativeCommit, nativeDetectMainBranch, nativeDiffContent, nativeDiffNameStatus, nativeDiffNumstat, nativeGetCurrentBranch, nativeLogOneline, nativeMergeSquash, nativeWorktreeAdd, nativeWorktreeList, nativeWorktreePrune, nativeWorktreeRemove, } from "./native-git-bridge.js";
// ─── Path Helpers ──────────────────────────────────────────────────────────
function normalizePathForComparison(path) {
    const normalized = path
        .replaceAll("\\", "/")
        .replace(/^\/\/\?\//, "")
        .replace(/\/+$/, "");
    return process.platform === "win32" ? normalized.toLowerCase() : normalized;
}
// ─── resolveGitDir ─────────────────────────────────────────────────────────
/**
 * Resolve the actual git directory for a given repository path.
 *
 * In a normal repo, .git is a directory → returns `<basePath>/.git`.
 * In a worktree, .git is a file containing `gitdir: <path>` → resolves
 * and returns that path.
 *
 * This is critical for operations that reference git metadata files like
 * MERGE_HEAD, SQUASH_MSG, etc. — these live in the git directory, not
 * in the working tree root. Without this, worktree merges fail because
 * they look for MERGE_HEAD in the wrong location.
 */
export function resolveGitDir(basePath) {
    const gitPath = join(basePath, ".git");
    if (!existsSync(gitPath))
        return join(basePath, ".git");
    try {
        const content = readFileSync(gitPath, "utf-8").trim();
        if (content.startsWith("gitdir: ")) {
            return resolve(basePath, content.slice(8));
        }
    }
    catch {
        // Not a file or unreadable — fall through to default
    }
    return join(basePath, ".git");
}
export function worktreesDir(basePath) {
    return join(basePath, ".nerf", "worktrees");
}
export function worktreePath(basePath, name) {
    return join(worktreesDir(basePath), name);
}
export function worktreeBranchName(name) {
    return `worktree/${name}`;
}
// ─── Core Operations ───────────────────────────────────────────────────────
/**
 * Create a new git worktree under .nerf/worktrees/<name>/ with branch worktree/<name>.
 * The branch is created from the current HEAD of the main branch.
 *
 * @param opts.branch — override the default `worktree/<name>` branch name
 */
export function createWorktree(basePath, name, opts = {}) {
    // Validate name: alphanumeric, hyphens, underscores only
    if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
        throw new NerfError(NERF_PARSE_ERROR, `Invalid worktree name "${name}". Use only letters, numbers, hyphens, and underscores.`);
    }
    const wtPath = worktreePath(basePath, name);
    const branch = opts.branch ?? worktreeBranchName(name);
    if (existsSync(wtPath)) {
        // A valid git worktree has a .git file (not directory) containing a
        // "gitdir:" pointer.  If the directory exists but has no .git file,
        // it is a stale leftover from a prior crash — remove it so a fresh
        // worktree can be created in its place.
        const gitFilePath = join(wtPath, ".git");
        if (!existsSync(gitFilePath)) {
            logWarning("reconcile", `Removing stale worktree directory (no .git file): ${wtPath}`, { worktree: name });
            rmSync(wtPath, { recursive: true, force: true });
        }
        else {
            throw new NerfError(NERF_STALE_STATE, `Worktree "${name}" already exists at ${wtPath}`);
        }
    }
    // Ensure the .nerf/worktrees/ directory exists
    const wtDir = worktreesDir(basePath);
    mkdirSync(wtDir, { recursive: true });
    // Prune any stale worktree entries from a previous removal
    nativeWorktreePrune(basePath);
    // Use the explicit start point (e.g. integration branch) if provided,
    // otherwise fall back to the repo's detected main branch.
    const startPoint = opts.startPoint ?? nativeDetectMainBranch(basePath);
    // Check if the branch already exists (leftover from a previous worktree)
    const branchAlreadyExists = nativeBranchExists(basePath, branch);
    if (branchAlreadyExists) {
        // Check if the branch is actively used by an existing worktree.
        const worktreeEntries = nativeWorktreeList(basePath);
        const branchInUse = worktreeEntries.some(entry => entry.branch === branch);
        if (branchInUse) {
            throw new NerfError(NERF_LOCK_HELD, `Branch "${branch}" is already in use by another worktree. ` +
                `Remove the existing worktree first with /worktree remove ${name}.`);
        }
        if (opts.reuseExistingBranch) {
            // Attach worktree to the existing branch as-is (preserving commits).
            // Used when resuming auto-mode: the milestone branch has valid work
            // from prior sessions that must not be reset.
            nativeWorktreeAdd(basePath, wtPath, branch);
        }
        else {
            // Reset the stale branch to the start point, then attach worktree to it
            nativeBranchForceReset(basePath, branch, startPoint);
            nativeWorktreeAdd(basePath, wtPath, branch);
        }
    }
    else {
        nativeWorktreeAdd(basePath, wtPath, branch, true, startPoint);
    }
    return {
        name,
        path: wtPath,
        branch,
        exists: true,
    };
}
/**
 * List all NERF-managed worktrees.
 * Uses native worktree list and filters to those under .nerf/worktrees/.
 */
export function listWorktrees(basePath) {
    const baseVariants = [resolve(basePath)];
    if (existsSync(basePath)) {
        baseVariants.push(realpathSync(basePath));
    }
    const seenRoots = new Set();
    const worktreeRoots = baseVariants
        .map(baseVariant => {
        const path = join(baseVariant, ".nerf", "worktrees");
        return {
            normalized: normalizePathForComparison(path),
        };
    })
        .filter(root => {
        if (seenRoots.has(root.normalized))
            return false;
        seenRoots.add(root.normalized);
        return true;
    });
    const entries = nativeWorktreeList(basePath);
    if (!entries.length)
        return [];
    const worktrees = [];
    for (const entry of entries) {
        if (entry.isBare)
            continue;
        const entryPath = entry.path;
        const branch = entry.branch;
        if (!branch)
            continue;
        const branchWorktreeName = branch.startsWith("worktree/")
            ? branch.slice("worktree/".length)
            : branch.startsWith("milestone/")
                ? branch.slice("milestone/".length)
                : null;
        const entryVariants = [resolve(entryPath)];
        if (existsSync(entryPath)) {
            entryVariants.push(realpathSync(entryPath));
        }
        const normalizedEntryVariants = [...new Set(entryVariants.map(normalizePathForComparison))];
        const matchedRoot = worktreeRoots.find(root => normalizedEntryVariants.some(entryVariant => entryVariant.startsWith(`${root.normalized}/`)));
        const matchesBranchLeaf = branchWorktreeName
            ? normalizedEntryVariants.some(entryVariant => entryVariant.split("/").pop() === branchWorktreeName)
            : false;
        // Only include worktrees under .nerf/worktrees/
        if (!matchedRoot && !matchesBranchLeaf)
            continue;
        const matchedEntryPath = normalizedEntryVariants.find(entryVariant => matchedRoot ? entryVariant.startsWith(`${matchedRoot.normalized}/`) : false);
        let name = matchedRoot ? matchedEntryPath?.slice(matchedRoot.normalized.length + 1) ?? "" : "";
        // Git on Windows can report a path form that does not map cleanly back to the
        // repo root even when the branch naming is still authoritative.
        if ((!name || name.includes("/")) && branchWorktreeName && matchesBranchLeaf) {
            name = branchWorktreeName;
        }
        if (!name || name.includes("/"))
            continue;
        const resolvedEntryPath = existsSync(entryPath) ? realpathSync(entryPath) : resolve(entryPath);
        worktrees.push({
            name,
            path: resolvedEntryPath,
            branch,
            exists: existsSync(resolvedEntryPath),
        });
    }
    return worktrees;
}
/**
 * Remove a worktree and optionally delete its branch.
 * If the process is currently inside the worktree, chdir out first.
 */
export function removeWorktree(basePath, name, opts = {}) {
    let wtPath = worktreePath(basePath, name);
    const branch = opts.branch ?? worktreeBranchName(name);
    const { deleteBranch = true, force = true } = opts;
    // Resolve the ACTUAL worktree path from git's worktree list.
    // The computed path may differ when .nerf/ is (or was) a symlink to an
    // external state directory — git resolves symlinks at worktree creation
    // time, so its registered path points to the resolved external location.
    // If syncStateToProjectRoot later creates a real .nerf/ directory that
    // shadows the symlink, the computed path diverges from git's record.
    try {
        const entries = nativeWorktreeList(basePath);
        const entry = entries.find(e => e.branch === branch);
        if (entry?.path) {
            wtPath = entry.path;
        }
    }
    catch { /* fall back to computed path */ }
    const resolvedWtPath = existsSync(wtPath) ? realpathSync(wtPath) : wtPath;
    // If we're inside the worktree, move out first — git can't remove an in-use directory
    const cwd = process.cwd();
    const resolvedCwd = existsSync(cwd) ? realpathSync(cwd) : cwd;
    if (resolvedCwd === resolvedWtPath || resolvedCwd.startsWith(resolvedWtPath + sep)) {
        process.chdir(basePath);
    }
    if (!existsSync(wtPath)) {
        nativeWorktreePrune(basePath);
        if (deleteBranch) {
            try {
                nativeBranchDelete(basePath, branch, true);
            }
            catch { /* branch may not exist */ }
        }
        return;
    }
    // Submodule safety (#2337): detect submodules with uncommitted changes
    // before force-removing the worktree. Force removal destroys all uncommitted
    // state, which is especially destructive for submodule directories.
    let hasSubmoduleChanges = false;
    const gitmodulesPath = join(resolvedWtPath, ".gitmodules");
    if (existsSync(gitmodulesPath)) {
        try {
            const submoduleStatus = execFileSync("git", ["submodule", "status"], { cwd: resolvedWtPath, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" }).trim();
            // Lines starting with '+' indicate uncommitted submodule changes
            hasSubmoduleChanges = submoduleStatus.split("\n").some((line) => line.startsWith("+") || line.startsWith("-"));
            if (hasSubmoduleChanges) {
                // Stash submodule changes so they are not lost during force removal.
                // The stash is created in the worktree before it's torn down.
                try {
                    execFileSync("git", ["stash", "push", "-m", "nerf: auto-stash submodule changes before worktree teardown"], { cwd: resolvedWtPath, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" });
                    logWarning("reconcile", `Stashed uncommitted submodule changes before worktree teardown`, { worktree: name, path: resolvedWtPath });
                }
                catch {
                    // Stash failed — warn the user that submodule changes may be lost
                    logWarning("reconcile", `Submodule changes detected — stash failed, changes may be lost during force removal`, { worktree: name, path: resolvedWtPath });
                }
            }
        }
        catch {
            // submodule status failed — proceed with normal removal
        }
    }
    // Remove worktree: try non-force first when submodules have changes,
    // falling back to force only after submodule state has been preserved.
    const useForce = hasSubmoduleChanges ? false : force;
    try {
        nativeWorktreeRemove(basePath, resolvedWtPath, useForce);
    }
    catch { /* may fail */ }
    // If the directory is still there (e.g. locked), try harder with force
    if (existsSync(resolvedWtPath)) {
        try {
            nativeWorktreeRemove(basePath, resolvedWtPath, true);
        }
        catch { /* may fail */ }
    }
    // Prune stale entries so git knows the worktree is gone
    nativeWorktreePrune(basePath);
    if (deleteBranch) {
        try {
            nativeBranchDelete(basePath, branch, true);
        }
        catch { /* branch may not exist */ }
    }
}
/** Paths to skip in all worktree diffs (internal/runtime artifacts). */
const SKIP_PATHS = [".nerf/worktrees/", ".nerf/runtime/", ".nerf/activity/"];
const SKIP_EXACT = [".nerf/STATE.md", ".nerf/auto.lock", ".nerf/metrics.json"];
function shouldSkipPath(filePath) {
    if (SKIP_PATHS.some(p => filePath.startsWith(p)))
        return true;
    if (SKIP_EXACT.includes(filePath))
        return true;
    return false;
}
function parseDiffNameStatus(entries) {
    const added = [];
    const modified = [];
    const removed = [];
    for (const { status, path } of entries) {
        if (shouldSkipPath(path))
            continue;
        switch (status) {
            case "A":
                added.push(path);
                break;
            case "M":
                modified.push(path);
                break;
            case "D":
                removed.push(path);
                break;
            default:
                // Renames, copies — treat as modified
                if (status?.startsWith("R") || status?.startsWith("C")) {
                    modified.push(path);
                }
        }
    }
    return { added, modified, removed };
}
/**
 * Diff the .nerf/ directory between the worktree branch and main branch.
 * Returns a summary of added, modified, and removed NERF artifacts.
 */
export function diffWorktreeNerf(basePath, name) {
    const branch = worktreeBranchName(name);
    const mainBranch = nativeDetectMainBranch(basePath);
    const entries = nativeDiffNameStatus(basePath, mainBranch, branch, ".nerf/", true);
    return parseDiffNameStatus(entries);
}
/**
 * Diff ALL files between the worktree branch and main branch.
 * Uses direct diff (no merge-base) to show what will actually change
 * on main when the merge is applied. If both branches have identical
 * content, this correctly returns an empty diff.
 */
export function diffWorktreeAll(basePath, name) {
    const branch = worktreeBranchName(name);
    const mainBranch = nativeDetectMainBranch(basePath);
    const entries = nativeDiffNameStatus(basePath, mainBranch, branch);
    return parseDiffNameStatus(entries);
}
/**
 * Get per-file line addition/deletion stats for what will change on main.
 * Uses direct diff (not merge-base) so the preview matches the actual merge outcome.
 */
export function diffWorktreeNumstat(basePath, name) {
    const branch = worktreeBranchName(name);
    const mainBranch = nativeDetectMainBranch(basePath);
    const rawStats = nativeDiffNumstat(basePath, mainBranch, branch);
    const stats = [];
    for (const entry of rawStats) {
        if (shouldSkipPath(entry.path))
            continue;
        stats.push({ file: entry.path, added: entry.added, removed: entry.removed });
    }
    return stats;
}
/**
 * Get the full diff content for .nerf/ between the worktree branch and main.
 * Returns the raw unified diff for LLM consumption.
 */
export function getWorktreeNerfDiff(basePath, name) {
    const branch = worktreeBranchName(name);
    const mainBranch = nativeDetectMainBranch(basePath);
    return nativeDiffContent(basePath, mainBranch, branch, ".nerf/", undefined, true);
}
/**
 * Get the full diff content for non-.nerf/ files between the worktree branch and main.
 * Returns the raw unified diff for LLM consumption.
 */
export function getWorktreeCodeDiff(basePath, name) {
    const branch = worktreeBranchName(name);
    const mainBranch = nativeDetectMainBranch(basePath);
    return nativeDiffContent(basePath, mainBranch, branch, undefined, ".nerf/", true);
}
/**
 * Get commit log for the worktree branch since it diverged from main.
 */
export function getWorktreeLog(basePath, name) {
    const branch = worktreeBranchName(name);
    const mainBranch = nativeDetectMainBranch(basePath);
    const entries = nativeLogOneline(basePath, mainBranch, branch);
    return entries.map(e => `${e.sha} ${e.message}`).join("\n");
}
/**
 * Merge the worktree branch into main using squash merge.
 * Must be called from the main working tree (not the worktree itself).
 * Returns the merge commit message.
 */
export function mergeWorktreeToMain(basePath, name, commitMessage) {
    const branch = worktreeBranchName(name);
    const mainBranch = nativeDetectMainBranch(basePath);
    const current = nativeGetCurrentBranch(basePath);
    if (current !== mainBranch) {
        throw new NerfError(NERF_GIT_ERROR, `Must be on ${mainBranch} to merge. Currently on ${current}.`);
    }
    const result = nativeMergeSquash(basePath, branch);
    if (!result.success) {
        throw new NerfError(NERF_MERGE_CONFLICT, `Merge conflicts detected in: ${result.conflicts.join(", ")}`);
    }
    nativeCommit(basePath, commitMessage);
    return commitMessage;
}
