import { randomUUID } from "node:crypto";
import { promises as fs } from "node:fs";
import path from "node:path";
import { runShellCommand, } from "./builtins/shellCommandRunner.js";
function toDisplayPath(targetPath) {
    return targetPath.replace(/\\/g, "/");
}
async function pathExists(targetPath) {
    try {
        await fs.access(targetPath);
        return true;
    }
    catch {
        return false;
    }
}
function countOccurrences(content, search) {
    if (!search) {
        return 0;
    }
    let count = 0;
    let index = content.indexOf(search);
    while (index >= 0) {
        count += 1;
        index = content.indexOf(search, index + search.length);
    }
    return count;
}
function readStringArg(args, names, label) {
    for (const name of names) {
        const value = args[name];
        if (typeof value === "string") {
            return value;
        }
    }
    throw new Error(`${label} is required`);
}
function readOptionalStringArg(args, names) {
    for (const name of names) {
        const value = args[name];
        if (typeof value === "string" && value.trim()) {
            return value;
        }
    }
    return undefined;
}
function readOptionalPositiveIntegerArg(args, names, fallback) {
    for (const name of names) {
        const value = args[name];
        const numeric = typeof value === "number"
            ? value
            : typeof value === "string"
                ? Number(value)
                : NaN;
        if (Number.isFinite(numeric) && numeric > 0) {
            return Math.floor(numeric);
        }
    }
    return fallback;
}
function ensureInsideWorkspace(workspaceRoot, targetPath) {
    const resolvedTargetPath = path.resolve(targetPath);
    const relativePath = path.relative(workspaceRoot, resolvedTargetPath);
    if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
        throw new Error(`Target path is outside workspace: ${resolvedTargetPath}`);
    }
    return resolvedTargetPath;
}
async function resolveWorkspaceDirectory(workingDirectory, rawCwd) {
    const requestedRoot = path.resolve(String(workingDirectory || "").trim());
    const workspaceRoot = await fs.realpath(requestedRoot);
    const workspaceStat = await fs.stat(workspaceRoot);
    if (!workspaceStat.isDirectory()) {
        throw new Error(`Workspace path is not a directory: ${requestedRoot}`);
    }
    if (!rawCwd?.trim()) {
        return { workspaceRoot, cwd: workspaceRoot };
    }
    const requestedCwd = rawCwd.trim();
    const candidatePath = path.isAbsolute(requestedCwd)
        ? path.resolve(requestedCwd)
        : path.resolve(requestedRoot, requestedCwd);
    const requestedRelativePath = path.relative(requestedRoot, candidatePath);
    if (requestedRelativePath.startsWith("..") ||
        path.isAbsolute(requestedRelativePath)) {
        throw new Error(`Command cwd is outside workspace: ${candidatePath}`);
    }
    const realCwd = ensureInsideWorkspace(workspaceRoot, await fs.realpath(candidatePath));
    const cwdStat = await fs.stat(realCwd);
    if (!cwdStat.isDirectory()) {
        throw new Error(`Command cwd is not a directory: ${realCwd}`);
    }
    return { workspaceRoot, cwd: realCwd };
}
async function ensureWritableTarget(workspaceRoot, targetPath) {
    const realParentPath = await fs.realpath(path.dirname(targetPath));
    ensureInsideWorkspace(workspaceRoot, realParentPath);
    try {
        const targetLinkStat = await fs.lstat(targetPath);
        if (targetLinkStat.isSymbolicLink()) {
            throw new Error(`Refusing to write through symbolic link: ${targetPath}`);
        }
    }
    catch (error) {
        const errorCode = typeof error === "object" && error !== null
            ? Reflect.get(error, "code")
            : undefined;
        if (errorCode !== "ENOENT") {
            throw error;
        }
    }
}
/**
 * 解析内置文件工具目标路径。
 * 主流程：同时校验请求路径与 realpath 工作区边界，防止绝对路径、`..` 与符号链接目录穿透。
 */
async function resolveWorkspaceTarget(workingDirectory, rawFilePath) {
    const requestedRoot = path.resolve(String(workingDirectory || "").trim());
    const workspaceRoot = await fs.realpath(requestedRoot);
    const workspaceStat = await fs.stat(workspaceRoot);
    if (!workspaceStat.isDirectory()) {
        throw new Error(`Workspace path is not a directory: ${requestedRoot}`);
    }
    const normalizedFilePath = rawFilePath.trim();
    if (!normalizedFilePath) {
        throw new Error("file_path is required");
    }
    const requestedPath = path.isAbsolute(normalizedFilePath)
        ? path.resolve(normalizedFilePath)
        : path.resolve(requestedRoot, normalizedFilePath);
    const requestedRelativePath = path.relative(requestedRoot, requestedPath);
    if (requestedRelativePath.startsWith("..") ||
        path.isAbsolute(requestedRelativePath)) {
        throw new Error(`Target path is outside workspace: ${requestedPath}`);
    }
    return {
        workspaceRoot,
        requestedPath,
        targetPath: ensureInsideWorkspace(workspaceRoot, path.join(workspaceRoot, requestedRelativePath)),
    };
}
/**
 * 原子写入完整文件内容。
 * 主流程：创建父目录 -> 校验 realpath 边界和符号链接 -> 写同目录临时文件 -> rename 覆盖目标。
 */
async function writeFileAtomically(workingDirectory, filePath, content) {
    const target = await resolveWorkspaceTarget(workingDirectory, filePath);
    const parentPath = path.dirname(target.targetPath);
    const createdParentDirectory = !(await pathExists(parentPath));
    await fs.mkdir(parentPath, { recursive: true });
    await ensureWritableTarget(target.workspaceRoot, target.targetPath);
    const temporaryPath = path.join(parentPath, `.${path.basename(target.targetPath)}.${process.pid}.${randomUUID()}.tmp`);
    try {
        await fs.writeFile(temporaryPath, content, "utf-8");
        await fs.rename(temporaryPath, target.targetPath);
    }
    catch (error) {
        await fs.rm(temporaryPath, { force: true });
        throw error;
    }
    return {
        workspaceRoot: target.workspaceRoot,
        targetPath: target.targetPath,
        bytes: Buffer.byteLength(content, "utf-8"),
        createdParentDirectory,
    };
}
async function readFileInsideWorkspace(workingDirectory, filePath) {
    const target = await resolveWorkspaceTarget(workingDirectory, filePath);
    const realTargetPath = ensureInsideWorkspace(target.workspaceRoot, await fs.realpath(target.targetPath));
    const stat = await fs.stat(realTargetPath);
    if (!stat.isFile()) {
        throw new Error(`Path is not a file: ${realTargetPath}`);
    }
    return {
        workspaceRoot: target.workspaceRoot,
        targetPath: realTargetPath,
        content: await fs.readFile(realTargetPath, "utf-8"),
    };
}
const MAX_COMMAND_TIMEOUT_MS = 10 * 60_000;
const DEFAULT_COMMAND_TIMEOUT_MS = MAX_COMMAND_TIMEOUT_MS;
const DEFAULT_COMMAND_INACTIVITY_TIMEOUT_MS = 60_000;
const MAX_COMMAND_INACTIVITY_TIMEOUT_MS = 10 * 60_000;
function writeFileTool(workingDirectory) {
    return {
        kind: "builtin",
        toolName: "write_file",
        exposedName: "write_file",
        description: "Create or overwrite a UTF-8 text file inside the current workspace. Parent directories are created automatically.",
        inputSchema: {
            type: "object",
            properties: {
                file_path: {
                    type: "string",
                    description: "Workspace-relative file path to write.",
                },
                content: {
                    type: "string",
                    description: "Complete UTF-8 file content.",
                },
            },
            required: ["file_path", "content"],
            additionalProperties: false,
        },
        async execute(args) {
            const filePath = readStringArg(args, ["file_path", "filePath"], "file_path");
            const content = readStringArg(args, ["content"], "content");
            const result = await writeFileAtomically(workingDirectory, filePath, content);
            return {
                ok: true,
                tool: "write_file",
                workspacePath: toDisplayPath(result.workspaceRoot),
                filePath: toDisplayPath(result.targetPath),
                bytes: result.bytes,
                createdParentDirectory: result.createdParentDirectory,
            };
        },
    };
}
function editFileTool(workingDirectory) {
    return {
        kind: "builtin",
        toolName: "edit_file",
        exposedName: "edit_file",
        description: "Replace exactly one occurrence of old_string with new_string in a UTF-8 workspace text file.",
        inputSchema: {
            type: "object",
            properties: {
                file_path: {
                    type: "string",
                    description: "Workspace-relative file path to edit.",
                },
                old_string: {
                    type: "string",
                    description: "Existing text to replace. Must occur exactly once.",
                },
                new_string: { type: "string", description: "Replacement text." },
            },
            required: ["file_path", "old_string", "new_string"],
            additionalProperties: false,
        },
        async execute(args) {
            const filePath = readStringArg(args, ["file_path", "filePath"], "file_path");
            const oldString = readStringArg(args, ["old_string", "oldString"], "old_string");
            const newString = readStringArg(args, ["new_string", "newString"], "new_string");
            if (!oldString) {
                throw new Error("old_string must not be empty");
            }
            const current = await readFileInsideWorkspace(workingDirectory, filePath);
            const occurrences = countOccurrences(current.content, oldString);
            if (occurrences === 0) {
                throw new Error("old_string was not found in file");
            }
            if (occurrences > 1) {
                throw new Error(`old_string occurs ${occurrences} times; provide a unique match`);
            }
            const updatedContent = current.content.replace(oldString, newString);
            const result = await writeFileAtomically(workingDirectory, filePath, updatedContent);
            return {
                ok: true,
                tool: "edit_file",
                workspacePath: toDisplayPath(result.workspaceRoot),
                filePath: toDisplayPath(result.targetPath),
                replacements: 1,
                bytes: result.bytes,
            };
        },
    };
}
function readFileTool(workingDirectory) {
    return {
        kind: "builtin",
        toolName: "read_file",
        exposedName: "read_file",
        description: "Read a UTF-8 text file inside the current workspace.",
        inputSchema: {
            type: "object",
            properties: {
                file_path: {
                    type: "string",
                    description: "Workspace-relative file path to read.",
                },
            },
            required: ["file_path"],
            additionalProperties: false,
        },
        async execute(args) {
            const filePath = readStringArg(args, ["file_path", "filePath"], "file_path");
            const result = await readFileInsideWorkspace(workingDirectory, filePath);
            return {
                ok: true,
                tool: "read_file",
                workspacePath: toDisplayPath(result.workspaceRoot),
                filePath: toDisplayPath(result.targetPath),
                content: result.content,
            };
        },
    };
}
function bashTool(workingDirectory) {
    return {
        kind: "builtin",
        toolName: "Bash",
        exposedName: "Bash",
        description: "Execute a shell command in the current workspace. Use for local diagnostics, builds, tests, and OS commands. The command runs with a bounded timeout and returns stdout, stderr, and exit code.",
        inputSchema: {
            type: "object",
            properties: {
                command: { type: "string", description: "Shell command to execute." },
                cwd: {
                    type: "string",
                    description: "Optional workspace-relative working directory. Defaults to the workspace root.",
                },
                timeout_ms: {
                    type: "number",
                    description: "Optional timeout in milliseconds. Defaults to 600000 and is capped at 600000.",
                },
                inactivity_timeout_ms: {
                    type: "number",
                    description: "Optional no-output timeout in milliseconds. Defaults to 60000 and is capped at 600000.",
                },
                description: {
                    type: "string",
                    description: "Optional short reason for running the command.",
                },
            },
            required: ["command"],
            additionalProperties: false,
        },
        async execute(args, context) {
            const command = readStringArg(args, ["command"], "command").trim();
            if (!command) {
                throw new Error("command must not be empty");
            }
            const requestedTimeoutMs = readOptionalPositiveIntegerArg(args, ["timeout_ms", "timeoutMs"], DEFAULT_COMMAND_TIMEOUT_MS);
            const timeoutMs = Math.min(requestedTimeoutMs, MAX_COMMAND_TIMEOUT_MS);
            const requestedInactivityTimeoutMs = readOptionalPositiveIntegerArg(args, ["inactivity_timeout_ms", "inactivityTimeoutMs"], DEFAULT_COMMAND_INACTIVITY_TIMEOUT_MS);
            const inactivityTimeoutMs = Math.min(requestedInactivityTimeoutMs, MAX_COMMAND_INACTIVITY_TIMEOUT_MS);
            const cwdArg = readOptionalStringArg(args, ["cwd"]);
            const resolved = await resolveWorkspaceDirectory(workingDirectory, cwdArg);
            const result = await runShellCommand({
                commandText: command,
                workspaceRoot: resolved.workspaceRoot,
                cwd: resolved.cwd,
                timeoutMs,
                inactivityTimeoutMs,
                onOutput: context?.onOutput,
            });
            return {
                ok: result.exitCode === 0 && !result.timedOut,
                tool: "Bash",
                command,
                cwd: toDisplayPath(resolved.cwd),
                exitCode: result.exitCode,
                signal: result.signal,
                timedOut: result.timedOut,
                inactivityTimedOut: result.inactivityTimedOut,
                timeoutMs,
                inactivityTimeoutMs,
                durationMs: result.durationMs,
                outputSpilled: result.outputSpilled,
                outputFilePath: result.outputFilePath,
                outputLineCount: result.outputLineCount,
                commandFilePath: result.commandFilePath,
                commandLineCount: result.commandLineCount,
                stdout: result.stdout,
                stderr: result.stderr,
            };
        },
    };
}
export function createBuiltInFileTools(workingDirectory) {
    return [
        readFileTool(workingDirectory),
        writeFileTool(workingDirectory),
        editFileTool(workingDirectory),
        bashTool(workingDirectory),
    ];
}
