import { Buffer } from "node:buffer";
import { basename, extname } from "node:path";
import { createTwoFilesPatch } from "diff";
import chalk from "chalk";
import { isNotFound } from "@poe-code/config-mutations";
const REDACTED_PLACEHOLDER = "<redacted>";
const JSON_SENSITIVE_KEYS = [
    "apiKey",
    "api_key",
    "apiKeyHelper",
    "ANTHROPIC_API_KEY",
    "ANTHROPIC_CUSTOM_HEADERS"
];
const AUTH_SENSITIVE_KEYS = ["key"];
const TOML_SENSITIVE_KEYS = ["experimental_bearer_token"];
export class DryRunRecorder {
    operations = [];
    record(operation) {
        this.operations.push(operation);
    }
    drain() {
        const snapshot = this.operations;
        this.operations = [];
        return snapshot;
    }
}
export function createDryRunFileSystem(base, recorder) {
    const proxy = {
        async readFile(path, encoding) {
            if (encoding) {
                return base.readFile(path, encoding);
            }
            return base.readFile(path);
        },
        async writeFile(path, data, options) {
            const previousContent = await tryReadText(base, path);
            const nextContent = formatData(data, options?.encoding);
            recorder.record({
                type: "writeFile",
                path,
                nextContent,
                previousContent
            });
        },
        async symlink(target, path) {
            recorder.record({ type: "symlink", target, path });
        },
        async readlink(path) {
            return base.readlink(path);
        },
        async realpath(path) {
            return base.realpath(path);
        },
        async mkdir(path, options) {
            recorder.record({ type: "mkdir", path, options });
        },
        async stat(path) {
            return base.stat(path);
        },
        async lstat(path) {
            return base.lstat(path);
        },
        async rename(from, to) {
            recorder.record({ type: "rename", from, to });
        },
        async unlink(path) {
            recorder.record({ type: "unlink", path });
        },
        async readdir(path) {
            return base.readdir(path);
        }
    };
    if (typeof base.rm === "function") {
        proxy.rm = async (path, options) => {
            recorder.record({ type: "rm", path, options });
        };
    }
    if (typeof base.copyFile === "function") {
        proxy.copyFile = async (from, to) => {
            recorder.record({ type: "copyFile", from, to });
        };
    }
    if (typeof base.chmod === "function") {
        proxy.chmod = async (target, mode) => {
            recorder.record({ type: "chmod", path: target, mode });
        };
    }
    return proxy;
}
export function formatDryRunOperations(operations) {
    if (operations.length === 0) {
        return [chalk.dim("# no filesystem changes")];
    }
    const lines = [];
    for (const operation of operations) {
        const formatted = formatOperation(operation);
        if (Array.isArray(formatted)) {
            if (formatted.length === 0) {
                continue;
            }
            const [first, ...rest] = formatted;
            const indented = rest.map((line) => `  ${line}`);
            lines.push([first, ...indented].join("\n"));
        }
        else {
            lines.push(formatted);
        }
    }
    return lines;
}
function formatOperation(operation) {
    switch (operation.type) {
        case "mkdir": {
            const recursiveFlag = operation.options?.recursive ? " -p" : "";
            const command = `mkdir${recursiveFlag} ${quoteShellArgument(operation.path)}`;
            return renderOperationCommand(command, chalk.cyan, "# ensure");
        }
        case "unlink":
            return renderOperationCommand(`rm ${quoteShellArgument(operation.path)}`, chalk.red, "# delete");
        case "rm": {
            const flags = [];
            if (operation.options?.recursive) {
                flags.push("-r");
            }
            if (operation.options?.force) {
                flags.push("-f");
            }
            const flagSuffix = flags.length > 0 ? ` ${flags.join(" ")}` : "";
            return renderOperationCommand(`rm${flagSuffix} ${quoteShellArgument(operation.path)}`, chalk.red, "# delete");
        }
        case "copyFile":
            return renderOperationCommand(`cp ${quoteShellArgument(operation.from)} ${quoteShellArgument(operation.to)}`, chalk.cyan, "# copy");
        case "chmod": {
            const mode = operation.mode.toString(8);
            return renderOperationCommand(`chmod ${mode} ${quoteShellArgument(operation.path)}`, chalk.cyan, "# permissions");
        }
        case "symlink":
            return renderOperationCommand(`ln -s ${quoteShellArgument(operation.target)} ${quoteShellArgument(operation.path)}`, chalk.cyan, "# symlink");
        case "rename":
            return renderOperationCommand(`mv ${quoteShellArgument(operation.from)} ${quoteShellArgument(operation.to)}`, chalk.cyan, "# rename");
        case "writeFile": {
            return renderWriteOperation(operation);
        }
        default: {
            const neverOp = operation;
            return chalk.dim(`# unknown ${neverOp.type}`);
        }
    }
}
function renderOperationCommand(command, colorize, detail) {
    return `${colorize(command)} ${chalk.dim(detail)}`;
}
function quoteShellArgument(value) {
    if (value.length > 0 && isBareShellArgument(value)) {
        return value;
    }
    return `'${value.replaceAll("'", "'\"'\"'")}'`;
}
function isBareShellArgument(value) {
    for (const char of value) {
        if (!isBareShellArgumentChar(char)) {
            return false;
        }
    }
    return true;
}
function isBareShellArgumentChar(char) {
    const code = char.charCodeAt(0);
    return ((code >= 48 && code <= 57) ||
        (code >= 65 && code <= 90) ||
        (code >= 97 && code <= 122) ||
        char === "/" ||
        char === "." ||
        char === "_" ||
        char === "-" ||
        char === ":" ||
        char === "@" ||
        char === "%" ||
        char === "+" ||
        char === "=" ||
        char === ",");
}
function describeWriteChange(previous, next) {
    if (previous == null) {
        return "create";
    }
    if (previous === next) {
        return "noop";
    }
    return "update";
}
function renderWriteCommand(path, change) {
    const command = `cat > ${quoteShellArgument(path)}`;
    if (change === "create") {
        return renderOperationCommand(command, chalk.green, "# create");
    }
    if (change === "update") {
        return renderOperationCommand(command, chalk.yellow, "# update");
    }
    return renderOperationCommand(command, chalk.dim, "# no change");
}
function renderWriteOperation(operation) {
    const change = describeWriteChange(operation.previousContent, operation.nextContent);
    const lines = [renderWriteCommand(operation.path, change)];
    if (change === "noop") {
        return lines;
    }
    lines.push(...renderUnifiedDiff(operation.path, operation.previousContent, operation.nextContent));
    return lines;
}
export function renderUnifiedDiff(targetPath, previousContent, nextContent) {
    const sanitizedPrevious = previousContent == null
        ? null
        : redactContentForDiff(targetPath, previousContent);
    const sanitizedNext = redactContentForDiff(targetPath, nextContent);
    const oldLabel = previousContent == null ? "/dev/null" : targetPath;
    const patch = createTwoFilesPatch(oldLabel, targetPath, sanitizedPrevious ?? "", sanitizedNext, "", "", { context: 3 });
    const diffLines = patch
        .split("\n")
        .filter((line) => line.length > 0);
    const lines = [];
    for (const line of diffLines) {
        if (line.startsWith("Index:") || line.startsWith("====")) {
            continue;
        }
        if (line.startsWith("---") || line.startsWith("+++")) {
            lines.push(chalk.dim(line.trimEnd()));
            continue;
        }
        if (line.startsWith("@@")) {
            lines.push(chalk.cyan(line));
            continue;
        }
        if (line.startsWith("+")) {
            lines.push(chalk.green(line[0] ?? "+") + line.slice(1));
            continue;
        }
        if (line.startsWith("-")) {
            lines.push(chalk.red(line[0] ?? "-") + line.slice(1));
            continue;
        }
        if (line.startsWith("\\ No newline")) {
            lines.push(chalk.dim(line));
            continue;
        }
        lines.push(chalk.dim(line));
    }
    return lines;
}
function redactContentForDiff(targetPath, content) {
    const extension = extname(targetPath).toLowerCase();
    if (extension === ".json") {
        return redactJsonContent(content, basename(targetPath).toLowerCase());
    }
    if (extension === ".toml") {
        return redactTomlContent(content);
    }
    return content;
}
function redactJsonContent(content, fileName) {
    const keys = [...JSON_SENSITIVE_KEYS];
    if (fileName === "auth.json") {
        keys.push(...AUTH_SENSITIVE_KEYS);
    }
    return content
        .split("\n")
        .map((line) => redactJsonLine(line, keys))
        .join("\n");
}
function redactJsonLine(line, keys) {
    let result = line;
    for (const key of keys) {
        if (key === "apiKeyHelper") {
            result = redactJsonStringValue(result, key, redactApiKeyHelperValue);
            continue;
        }
        result = redactJsonStringValue(result, key, () => REDACTED_PLACEHOLDER);
    }
    return result;
}
function redactApiKeyHelperValue(value) {
    const echoIndex = value.indexOf("echo ");
    if (echoIndex >= 0) {
        const prefix = value.slice(0, echoIndex + "echo ".length);
        return `${prefix}${REDACTED_PLACEHOLDER}`;
    }
    return REDACTED_PLACEHOLDER;
}
function redactJsonStringValue(line, key, redact) {
    const token = `"${key}"`;
    const keyIndex = line.indexOf(token);
    if (keyIndex === -1) {
        return line;
    }
    const colonIndex = line.indexOf(":", keyIndex + token.length);
    if (colonIndex === -1) {
        return line;
    }
    const valueStart = line.indexOf("\"", colonIndex + 1);
    if (valueStart === -1) {
        return line;
    }
    const valueEnd = line.indexOf("\"", valueStart + 1);
    if (valueEnd === -1) {
        return line;
    }
    const currentValue = line.slice(valueStart + 1, valueEnd);
    const nextValue = redact(currentValue);
    return `${line.slice(0, valueStart + 1)}${nextValue}${line.slice(valueEnd)}`;
}
function redactTomlContent(content) {
    return content
        .split("\n")
        .map((line) => redactTomlLine(line))
        .join("\n");
}
function redactTomlLine(line) {
    const trimmed = line.trimStart();
    for (const key of TOML_SENSITIVE_KEYS) {
        if (!trimmed.startsWith(key)) {
            continue;
        }
        const nextChar = trimmed.charAt(key.length);
        if (nextChar && nextChar !== " " && nextChar !== "=") {
            continue;
        }
        const keyIndex = line.indexOf(key);
        const equalsIndex = line.indexOf("=", keyIndex + key.length);
        if (equalsIndex === -1) {
            return line;
        }
        const valueStart = line.indexOf("\"", equalsIndex + 1);
        if (valueStart === -1) {
            return line;
        }
        const valueEnd = line.indexOf("\"", valueStart + 1);
        if (valueEnd === -1) {
            return line;
        }
        return `${line.slice(0, valueStart + 1)}${REDACTED_PLACEHOLDER}${line.slice(valueEnd)}`;
    }
    return line;
}
async function tryReadText(base, path) {
    try {
        return await base.readFile(path, "utf8");
    }
    catch (error) {
        if (isNotFound(error)) {
            return null;
        }
        return null;
    }
}
function formatData(data, encoding = "utf8") {
    if (typeof data === "string") {
        return data;
    }
    try {
        const buffer = bufferFromView(data);
        return buffer.toString(encoding);
    }
    catch {
        return `<binary data (${data.byteLength} bytes)>`;
    }
}
function bufferFromView(view) {
    return Buffer.from(view.buffer, view.byteOffset, view.byteLength);
}
//# sourceMappingURL=dry-run.js.map