import fs from "fs/promises";
import os from "os";
import path from "path";
import process from "process";
import diff_match_patch from "diff-match-patch";
import { resolvePath } from "./resolvePath.js";
import { resolveDir } from "./resolveDir.js";
import { expandPath } from "./expandPath.js";
import { formatDiff } from "../utils/diff.js";
export { resolvePath } from "./resolvePath.js";
/**
 * Print a colored, line-based diff of `oldText` vs `newText` to
 * stdout. Deletions are red, insertions are green, unchanged context
 * is dimmed. Used both as a standalone Agency tool (`std::fs::printDiff`)
 * and as the side effect of `edit(..., printDiff: true)`.
 */
export function _printDiff(oldText, newText) {
    console.log(formatDiff(oldText, newText));
}
export async function _multiedit(dir, filename, edits, printDiff = false) {
    const full = await resolvePath(dir, filename);
    let contents = await fs.readFile(full, "utf8");
    const original = contents;
    let total = 0;
    for (let i = 0; i < edits.length; i++) {
        const { oldText, newText, replaceAll } = edits[i];
        if (!oldText) {
            throw new Error(`multiedit: edit #${i + 1} has empty oldText`);
        }
        if (replaceAll) {
            if (contents.indexOf(oldText) === -1) {
                throw new Error(`multiedit: edit #${i + 1} oldText not found in ${filename}`);
            }
            let count = 0;
            contents = contents.replaceAll(oldText, () => {
                count++;
                return newText;
            });
            total += count;
        }
        else {
            const first = contents.indexOf(oldText);
            if (first === -1) {
                throw new Error(`multiedit: edit #${i + 1} oldText not found in ${filename}`);
            }
            const second = contents.indexOf(oldText, first + oldText.length);
            if (second !== -1) {
                throw new Error(`multiedit: edit #${i + 1} oldText appears multiple times in ${filename}. Provide more context or set replaceAll.`);
            }
            contents =
                contents.slice(0, first) +
                    newText +
                    contents.slice(first + oldText.length);
            total += 1;
        }
    }
    await fs.writeFile(full, contents, "utf8");
    if (printDiff && original !== contents) {
        _printDiff(original, contents);
    }
    return { replacements: total, path: filename, edits: edits.length };
}
export async function _applyPatch(patch, allowedPaths) {
    const files = parseUnifiedDiff(patch);
    const touched = [];
    for (const f of files) {
        // Resolve via `resolveDir` (cwd-anchored — fs ops are
        // process-cwd-relative, not module-dir-relative). This expands
        // `~` and runs the allow-list check in one place.
        const full = await resolveDir(f.path, allowedPaths ?? [], "cwd");
        let original = "";
        if (f.isNew) {
            original = "";
        }
        else {
            original = await fs.readFile(full, "utf8");
        }
        const updated = applyHunks(original, f.hunks, f.path);
        await fs.mkdir(path.dirname(full), { recursive: true });
        await fs.writeFile(full, updated, "utf8");
        touched.push(f.path);
    }
    return { applied: files.length, files: touched };
}
function parseUnifiedDiff(patch) {
    const lines = patch.split("\n");
    const files = [];
    let current = null;
    let hunk = null;
    for (let i = 0; i < lines.length; i++) {
        const line = lines[i];
        if (line.startsWith("--- ")) {
            if (current)
                files.push(current);
            const nextLine = lines[i + 1] || "";
            if (!nextLine.startsWith("+++ ")) {
                throw new Error("applyPatch: malformed diff, missing +++ after ---");
            }
            const newFile = firstToken(nextLine.slice(4));
            const oldFile = firstToken(line.slice(4));
            const target = stripPathPrefix(newFile === "/dev/null" ? oldFile : newFile);
            current = {
                path: target,
                isNew: oldFile === "/dev/null" || oldFile.endsWith("/dev/null"),
                hunks: [],
            };
            hunk = null;
            i++;
        }
        else if (line.startsWith("@@")) {
            if (!current)
                throw new Error("applyPatch: hunk before file header");
            const m = line.match(/@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/);
            if (!m)
                throw new Error(`applyPatch: malformed hunk header: ${line}`);
            hunk = {
                oldStart: parseInt(m[1], 10),
                oldLines: m[2] ? parseInt(m[2], 10) : 1,
                newStart: parseInt(m[3], 10),
                newLines: m[4] ? parseInt(m[4], 10) : 1,
                lines: [],
            };
            current.hunks.push(hunk);
        }
        else if (hunk && (line.startsWith(" ") || line.startsWith("+") || line.startsWith("-"))) {
            hunk.lines.push(line);
        }
    }
    if (current)
        files.push(current);
    return files;
}
function stripPathPrefix(p) {
    if (p.startsWith("a/") || p.startsWith("b/"))
        return p.slice(2);
    return p;
}
function firstToken(s) {
    const m = s.match(/^\S+/);
    return m ? m[0] : "";
}
function applyHunks(original, hunks, filePath) {
    const dmp = new diff_match_patch();
    const allPatches = [];
    for (const h of hunks) {
        const before = [];
        const after = [];
        for (const line of h.lines) {
            const tag = line[0];
            const content = line.slice(1);
            if (tag === " ") {
                before.push(content);
                after.push(content);
            }
            else if (tag === "-") {
                before.push(content);
            }
            else if (tag === "+") {
                after.push(content);
            }
        }
        allPatches.push(...dmp.patch_make(before.join("\n"), after.join("\n")));
    }
    const [updated, applied] = dmp.patch_apply(allPatches, original);
    const firstFailed = applied.findIndex((ok) => !ok);
    if (firstFailed !== -1) {
        throw new Error(`applyPatch: hunk #${firstFailed + 1} could not be applied to ${filePath}; the surrounding context does not match the current file contents`);
    }
    return updated;
}
export async function _mkdir(dir, allowedPaths) {
    const full = await resolveDir(dir, allowedPaths ?? [], "cwd");
    await fs.mkdir(full, { recursive: true });
}
export async function _copy(src, dest, allowedPaths) {
    const srcFull = await resolveDir(src, allowedPaths ?? [], "cwd");
    const destFull = await resolveDir(dest, allowedPaths ?? [], "cwd");
    await fs.cp(srcFull, destFull, { recursive: true });
}
export async function _move(src, dest, allowedPaths) {
    const srcFull = await resolveDir(src, allowedPaths ?? [], "cwd");
    const destFull = await resolveDir(dest, allowedPaths ?? [], "cwd");
    await rejectDangerousPath(src, "move", "source");
    try {
        await fs.rename(srcFull, destFull);
    }
    catch (e) {
        if (e?.code === "EXDEV") {
            await fs.cp(srcFull, destFull, { recursive: true });
            await fs.rm(srcFull, { recursive: true, force: true });
            return;
        }
        throw e;
    }
}
export async function _remove(target, allowedPaths) {
    const full = await resolveDir(target, allowedPaths ?? [], "cwd");
    await rejectDangerousPath(target, "remove", "target");
    await fs.rm(full, { recursive: true, force: true });
}
export async function rejectDangerousPath(p, op, role) {
    const trimmed = p.trim();
    if (trimmed === "") {
        throw new Error(`${op}: ${role} must not be empty`);
    }
    // Expand `~` first so the home / top-level checks below are
    // performed against the actual target, not the literal `~/foo`.
    const lexical = path.resolve(process.cwd(), expandPath(trimmed));
    const [real, homeReal, cwdReal] = await Promise.all([
        realpathOrResolve(lexical),
        realpathOrResolve(os.homedir()),
        realpathOrResolve(process.cwd()),
    ]);
    for (const candidate of new Set([lexical, real])) {
        const root = path.parse(candidate).root;
        if (samePath(candidate, root)) {
            throw new Error(`${op}: refusing to use the filesystem root as ${role} (got '${p}')`);
        }
        if (homeReal && samePath(candidate, homeReal)) {
            throw new Error(`${op}: refusing to use the home directory as ${role} (got '${p}')`);
        }
        const segments = candidate
            .slice(root.length)
            .split(path.sep)
            .filter((s) => s.length > 0);
        if (segments.length <= 1) {
            throw new Error(`${op}: refusing to use the top-level path '${candidate}' as ${role} (got '${p}'); operations on a single segment under root could destroy critical system directories`);
        }
        if (samePath(cwdReal, candidate) ||
            cwdStartsWith(cwdReal, candidate + path.sep)) {
            throw new Error(`${op}: refusing to use the current working directory or one of its ancestors '${candidate}' as ${role} (got '${p}')`);
        }
    }
}
async function realpathOrResolve(p) {
    try {
        return await fs.realpath(p);
    }
    catch {
        return p;
    }
}
function samePath(a, b) {
    if (process.platform === "win32") {
        return a.toLowerCase() === b.toLowerCase();
    }
    return a === b;
}
function cwdStartsWith(cwd, prefix) {
    if (process.platform === "win32") {
        return cwd.toLowerCase().startsWith(prefix.toLowerCase());
    }
    return cwd.startsWith(prefix);
}
