/**
 * Task isolation backends for subagent execution.
 *
 * Provides filesystem isolation via git worktrees or FUSE overlays
 * so concurrent subagents don't stomp on each other's files.
 * Changes are captured as patches and merged back to the main repo.
 */
import { execFile as execFileCb } from "node:child_process";
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { promisify } from "node:util";
const execFile = promisify(execFileCb);
// ============================================================================
// Directory helpers
// ============================================================================
function encodeCwd(cwd) {
    return cwd.replace(/\//g, "--");
}
const nerfHome = process.env.NERF_HOME || path.join(os.homedir(), ".nerf");
function getIsolationBaseDir(cwd, taskId) {
    return path.join(nerfHome, "wt", encodeCwd(cwd), taskId);
}
// Track active isolation dirs for cleanup on exit
const activeIsolations = new Set();
let exitHandlerRegistered = false;
function registerExitHandler() {
    if (exitHandlerRegistered)
        return;
    exitHandlerRegistered = true;
    const cleanup = () => {
        for (const dir of activeIsolations) {
            try {
                // Best-effort sync cleanup: remove git worktree
                const { execFileSync } = require("node:child_process");
                try {
                    execFileSync("git", ["worktree", "remove", "--force", dir], {
                        stdio: "ignore",
                        timeout: 5000,
                    });
                }
                catch {
                    // Worktree may not exist (FUSE mode), just rm
                }
                fs.rmSync(dir, { recursive: true, force: true });
            }
            catch {
                // Best effort
            }
        }
    };
    process.on("exit", cleanup);
}
// ============================================================================
// Git helpers
// ============================================================================
async function git(args, cwd) {
    // M3: reserve 50 MB only for binary diff calls; all other git ops use 2 MB
    const hasBinary = args.includes("--binary");
    const { stdout } = await execFile("git", args, {
        cwd,
        maxBuffer: hasBinary ? 50 * 1024 * 1024 : 2 * 1024 * 1024,
    });
    return stdout;
}
async function gitSilent(args, cwd) {
    try {
        return await git(args, cwd);
    }
    catch {
        return "";
    }
}
// ============================================================================
// Baseline: capture and apply dirty state
// ============================================================================
async function captureBaseline(repoRoot) {
    // Staged changes
    const stagedDiff = await gitSilent(["diff", "--cached", "--binary"], repoRoot);
    // Unstaged changes (tracked files only)
    const unstagedDiff = await gitSilent(["diff", "--binary"], repoRoot);
    // Untracked files
    const untrackedOutput = await gitSilent(["ls-files", "--others", "--exclude-standard", "-z"], repoRoot);
    const untrackedPaths = untrackedOutput
        .split("\0")
        .filter((p) => p.length > 0);
    const untrackedFiles = [];
    for (const relativePath of untrackedPaths) {
        const fullPath = path.join(repoRoot, relativePath);
        try {
            const stat = fs.statSync(fullPath);
            if (stat.isFile() && stat.size < 10 * 1024 * 1024) {
                // Skip files > 10MB
                untrackedFiles.push({
                    relativePath,
                    content: fs.readFileSync(fullPath),
                });
            }
        }
        catch {
            // Skip unreadable files
        }
    }
    return { stagedDiff, unstagedDiff, untrackedFiles };
}
async function applyBaseline(worktreeDir, baseline) {
    // Apply staged diff
    if (baseline.stagedDiff.trim()) {
        const patchPath = path.join(worktreeDir, ".nerf-staged.patch");
        fs.writeFileSync(patchPath, baseline.stagedDiff);
        try {
            await git(["apply", "--binary", patchPath], worktreeDir);
            await git(["add", "-A"], worktreeDir);
        }
        catch {
            // Non-fatal: staged diff may not apply cleanly
        }
        finally {
            fs.unlinkSync(patchPath);
        }
    }
    // Apply unstaged diff on top
    if (baseline.unstagedDiff.trim()) {
        const patchPath = path.join(worktreeDir, ".nerf-unstaged.patch");
        fs.writeFileSync(patchPath, baseline.unstagedDiff);
        try {
            await git(["apply", "--binary", patchPath], worktreeDir);
        }
        catch {
            // Non-fatal: unstaged diff may not apply cleanly
        }
        finally {
            fs.unlinkSync(patchPath);
        }
    }
    // Copy untracked files
    for (const file of baseline.untrackedFiles) {
        const dest = path.join(worktreeDir, file.relativePath);
        const destDir = path.dirname(dest);
        fs.mkdirSync(destDir, { recursive: true });
        fs.writeFileSync(dest, file.content);
    }
    // Commit the baseline state so captureDeltaPatch can diff against it
    // without accidentally including the parent's dirty state in the delta.
    await gitSilent(["add", "-A"], worktreeDir);
    await gitSilent(["commit", "--allow-empty", "-m", "nerf: baseline snapshot"], worktreeDir);
}
// ============================================================================
// Delta capture
// ============================================================================
async function captureDeltaPatch(isolationDir) {
    const patches = [];
    // Add all changes (tracked + untracked) to index for diffing
    await gitSilent(["add", "-A"], isolationDir);
    // Capture the full diff against HEAD
    const diff = await gitSilent(["diff", "--cached", "--binary", "HEAD"], isolationDir);
    if (diff.trim()) {
        patches.push({
            path: path.join(isolationDir, "delta.patch"),
            content: diff,
        });
    }
    return patches;
}
// ============================================================================
// Worktree backend
// ============================================================================
export async function createWorktreeIsolation(repoRoot, taskId) {
    const worktreeDir = getIsolationBaseDir(repoRoot, taskId);
    registerExitHandler();
    activeIsolations.add(worktreeDir);
    // Create parent directories
    fs.mkdirSync(path.dirname(worktreeDir), { recursive: true });
    // Remove stale worktree if it exists
    try {
        await git(["worktree", "remove", "--force", worktreeDir], repoRoot);
    }
    catch {
        // Doesn't exist, that's fine
    }
    // Also clean up any leftover directory
    fs.rmSync(worktreeDir, { recursive: true, force: true });
    // Create the worktree
    await git(["worktree", "add", "--detach", worktreeDir, "HEAD"], repoRoot);
    // Capture and apply the parent's dirty state
    const baseline = await captureBaseline(repoRoot);
    await applyBaseline(worktreeDir, baseline);
    return {
        workDir: worktreeDir,
        async captureDelta() {
            return captureDeltaPatch(worktreeDir);
        },
        async cleanup() {
            activeIsolations.delete(worktreeDir);
            try {
                await Promise.race([
                    git(["worktree", "remove", "--force", worktreeDir], repoRoot),
                    new Promise((_, reject) => setTimeout(() => reject(new Error("Worktree cleanup timed out")), 10_000)),
                ]);
            }
            catch {
                try {
                    fs.rmSync(worktreeDir, { recursive: true, force: true });
                }
                catch { /* best effort */ }
            }
        },
    };
}
// ============================================================================
// FUSE overlay backend (Linux only)
// ============================================================================
async function findBinary(name) {
    try {
        const { stdout } = await execFile("which", [name]);
        const p = stdout.trim();
        return p || null;
    }
    catch {
        return null;
    }
}
export async function createFuseOverlayIsolation(repoRoot, taskId) {
    const baseDir = getIsolationBaseDir(repoRoot, taskId);
    const upperDir = path.join(baseDir, "upper");
    const workDir = path.join(baseDir, "work");
    const mergedDir = path.join(baseDir, "merged");
    // Check for fuse-overlayfs
    const fuseBin = await findBinary("fuse-overlayfs");
    if (!fuseBin) {
        // Fall back to worktree
        return createWorktreeIsolation(repoRoot, taskId);
    }
    registerExitHandler();
    activeIsolations.add(baseDir);
    // Clean up any stale mount/directory
    fs.rmSync(baseDir, { recursive: true, force: true });
    // Create directory structure
    fs.mkdirSync(upperDir, { recursive: true });
    fs.mkdirSync(workDir, { recursive: true });
    fs.mkdirSync(mergedDir, { recursive: true });
    // Mount the overlay
    await execFile(fuseBin, [
        "-o",
        `lowerdir=${repoRoot},upperdir=${upperDir},workdir=${workDir}`,
        mergedDir,
    ]);
    // Capture the parent's dirty file set so we can exclude them from the delta.
    // Upper dir will contain both parent-dirty files (visible through overlay) and
    // subagent-written files — we only want the latter.
    const parentDirtyFiles = new Set();
    const parentStatus = await gitSilent(["status", "--porcelain", "-z"], repoRoot);
    for (const entry of parentStatus.split("\0").filter(Boolean)) {
        // Porcelain format: XY filename (skip 3-char prefix)
        const filePath = entry.slice(3);
        if (filePath)
            parentDirtyFiles.add(filePath);
    }
    return {
        workDir: mergedDir,
        async captureDelta() {
            // Generate patches from upper dir (files actually written by the subagent).
            // Exclude files that were already dirty in the parent repo.
            const patches = [];
            const diffs = [];
            const walk = (dir, prefix) => {
                for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
                    const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
                    if (entry.isDirectory()) {
                        walk(path.join(dir, entry.name), rel);
                    }
                    else if (entry.isFile() && !parentDirtyFiles.has(rel)) {
                        // This file was written by the subagent, not inherited from parent
                        diffs.push(rel);
                    }
                }
            };
            walk(upperDir, "");
            if (diffs.length > 0) {
                // Use git diff in the merged dir (which has the .git) for only subagent files
                const diff = await gitSilent(["diff", "--binary", "HEAD", "--", ...diffs], mergedDir);
                if (diff.trim()) {
                    patches.push({
                        path: path.join(mergedDir, "delta.patch"),
                        content: diff,
                    });
                }
            }
            return patches;
        },
        async cleanup() {
            activeIsolations.delete(baseDir);
            try {
                // Unmount
                const fusermount = (await findBinary("fusermount")) || "fusermount";
                await execFile(fusermount, ["-u", mergedDir]);
            }
            catch {
                // Try fusermount3 as fallback
                try {
                    await execFile("fusermount3", ["-u", mergedDir]);
                }
                catch {
                    // Best effort
                }
            }
            // Remove all dirs
            fs.rmSync(baseDir, { recursive: true, force: true });
        },
    };
}
// ============================================================================
// Unified creation
// ============================================================================
export async function createIsolation(repoRoot, taskId, mode) {
    switch (mode) {
        case "fuse-overlay":
            return createFuseOverlayIsolation(repoRoot, taskId);
        case "worktree":
            return createWorktreeIsolation(repoRoot, taskId);
        default:
            throw new Error(`Isolation mode "${mode}" requires no isolation environment`);
    }
}
// ============================================================================
// Patch merge
// ============================================================================
export async function mergeDeltaPatches(repoRoot, patches) {
    if (patches.length === 0) {
        return { success: true, appliedPatches: [], failedPatches: [] };
    }
    // Combine all patches into one
    const combined = patches.map((p) => p.content).join("\n");
    const patchFile = path.join(os.tmpdir(), `nerf-merge-${Date.now()}.patch`);
    const appliedPatches = [];
    const failedPatches = [];
    try {
        fs.writeFileSync(patchFile, combined);
        // Dry run first
        try {
            await git(["apply", "--check", "--binary", patchFile], repoRoot);
        }
        catch (err) {
            // Dry run failed — patches conflict
            for (const p of patches)
                failedPatches.push(p.path);
            return {
                success: false,
                appliedPatches,
                failedPatches,
                error: `Patch conflict: ${err instanceof Error ? err.message : String(err)}`,
            };
        }
        // Apply for real
        await git(["apply", "--binary", patchFile], repoRoot);
        for (const p of patches)
            appliedPatches.push(p.path);
        return { success: true, appliedPatches, failedPatches };
    }
    finally {
        try {
            fs.unlinkSync(patchFile);
        }
        catch {
            // Best effort
        }
    }
}
// ============================================================================
// Settings reader (reads directly from settings file)
// ============================================================================
export function readIsolationMode() {
    try {
        const { getAgentDir } = require("@nerf/pi-coding-agent");
        const settingsPath = path.join(getAgentDir(), "settings.json");
        if (!fs.existsSync(settingsPath))
            return "none";
        const settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
        const mode = settings?.taskIsolation?.mode;
        if (mode === "worktree" || mode === "fuse-overlay")
            return mode;
        return "none";
    }
    catch {
        return "none";
    }
}
