# 中文注释：
# 文件：echobot/agent/tools/filesystem.py
# 说明：智能体工具层，实现文件、Shell、网络与消息等工具调用能力。

"""File System Tools - 文件系统工具
=================================================

此模块提供文件操作相关的工具，包括：

1. ReadFileTool - 读取文件
2. WriteFileTool - 写入文件
3. EditFileTool - 编辑文件（文本替换）
4. ListDirTool - 列出目录

这些工具允许 Agent 读取、修改和管理工作空间中的文件。

使用示例：
    # 读取文件
    content = await registry.execute("read_file", {"path": "test.txt"})
    
    # 写入文件
    result = await registry.execute("write_file", {"path": "new.txt", "content": "..."})
    
    # 编辑文件
    result = await registry.execute("edit_file", {"path": "file.txt", "old_text": "a", "new_text": "b"})
    
    # 列出目录
    result = await registry.execute("list_dir", {"path": "/tmp"})
"""

from pathlib import Path
from typing import Any

from echobot.agent.tools.base import Tool


def _resolve_path_with_boundary(path: str, root_dir: Path | None) -> Path:
    """
    解析用户传入路径，并执行工作区边界校验。

    规则说明：
    1. 若传入相对路径且设置了 `root_dir`，则相对 `root_dir` 解析。
    2. 若设置了 `root_dir`，最终解析出的绝对路径必须位于该目录内。
    3. 若未设置 `root_dir`，则保持原有行为（只做 expanduser + resolve）。

    这样可以避免工具被诱导读写工作区外部敏感路径。
    """
    raw = Path(path).expanduser()

    # 相对路径统一锚定到 root_dir，避免受当前进程 cwd 波动影响。
    if root_dir and not raw.is_absolute():
        raw = root_dir / raw

    resolved = raw.resolve()

    if root_dir and not resolved.is_relative_to(root_dir):
        raise ValueError(f"Path is outside workspace boundary: {path}")

    return resolved


class ReadFileTool(Tool):
    """
    读取文件工具
    
    允许 Agent 读取指定路径的文件内容。
    
    工具元数据：
    - 名称：read_file
    - 描述：Read the contents of a file at the given path.
    - 参数：path（文件路径）
    
    使用示例：
        await registry.execute("read_file", {"path": "README.md"})
    
    错误处理：
    - 文件不存在：返回错误信息
    - 权限不足：返回错误信息
    - 其他异常：返回错误信息
    """

    def __init__(self, root_dir: str | Path | None = None):
        """
        初始化读取工具。

        Args:
            root_dir: 可选工作区根目录。设置后将启用越界访问保护。
        """
        self.root_dir = Path(root_dir).expanduser().resolve() if root_dir else None
    
    @property
    def name(self) -> str:
        return "read_file"
    
    @property
    def description(self) -> str:
        return "Read the contents of a file at the given path."
    
    @property
    def parameters(self) -> dict[str, Any]:
        return {
            "type": "object",
            "properties": {
                "path": {
                    "type": "string",
                    "description": "The file path to read"
                }
            },
            "required": ["path"]
        }
    
    async def execute(self, path: str, **kwargs: Any) -> str:
        """
        读取文件内容
        
        Args:
            path: 文件路径
        
        Returns:
            str: 文件内容或错误信息
        """
        try:
            # 统一路径解析并强制工作区边界校验。
            file_path = _resolve_path_with_boundary(path, self.root_dir)
            if not file_path.exists():
                return f"Error: File not found: {path}"
            if not file_path.is_file():
                return f"Error: Not a file: {path}"

            content = file_path.read_text(encoding="utf-8")
            return content
        except ValueError as e:
            return f"Error: {str(e)}"
        except PermissionError:
            return f"Error: Permission denied: {path}"
        except Exception as e:
            return f"Error reading file: {str(e)}"


class WriteFileTool(Tool):
    """
    写入文件工具
    
    允许 Agent 创建或覆盖文件内容。
    如果父目录不存在，会自动创建。
    
    工具元数据：
    - 名称：write_file
    - 描述：Write content to a file at the given path. Creates parent directories if needed.
    - 参数：path（文件路径）、content（内容）
    
    使用示例：
        await registry.execute("write_file", {
            "path": "output.txt",
            "content": "Hello, World!"
        })
    """

    def __init__(self, root_dir: str | Path | None = None):
        """
        初始化写入工具。

        Args:
            root_dir: 可选工作区根目录。设置后将启用越界访问保护。
        """
        self.root_dir = Path(root_dir).expanduser().resolve() if root_dir else None
    
    @property
    def name(self) -> str:
        return "write_file"
    
    @property
    def description(self) -> str:
        return "Write content to a file at the given path. Creates parent directories if needed."
    
    @property
    def parameters(self) -> dict[str, Any]:
        return {
            "type": "object",
            "properties": {
                "path": {
                    "type": "string",
                    "description": "The file path to write to"
                },
                "content": {
                    "type": "string",
                    "description": "The content to write"
                }
            },
            "required": ["path", "content"]
        }
    
    async def execute(self, path: str, content: str, **kwargs: Any) -> str:
        """
        写入文件内容
        
        Args:
            path: 文件路径
            content: 要写入的内容
        
        Returns:
            str: 成功或错误信息
        """
        try:
            # 写入路径同样必须经过边界校验，避免写出工作区。
            file_path = _resolve_path_with_boundary(path, self.root_dir)
            file_path.parent.mkdir(parents=True, exist_ok=True)
            file_path.write_text(content, encoding="utf-8")
            return f"Successfully wrote {len(content)} bytes to {path}"
        except ValueError as e:
            return f"Error: {str(e)}"
        except PermissionError:
            return f"Error: Permission denied: {path}"
        except Exception as e:
            return f"Error writing file: {str(e)}"


class EditFileTool(Tool):
    """
    编辑文件工具
    
    通过文本替换的方式修改文件内容。
    old_text 必须精确匹配文件中的内容。
    
    工具元数据：
    - 名称：edit_file
    - 描述：Edit a file by replacing old_text with new_text.
    - 参数：path（文件路径）、old_text（要替换的文本）、new_text（替换后的文本）
    
    使用示例：
        await registry.execute("edit_file", {
            "path": "config.py",
            "old_text": "DEBUG = True",
            "new_text": "DEBUG = False"
        })
    
    安全特性：
    - 如果匹配多次，会返回警告要求提供更精确的上下文
    """

    def __init__(self, root_dir: str | Path | None = None):
        """
        初始化编辑工具。

        Args:
            root_dir: 可选工作区根目录。设置后将启用越界访问保护。
        """
        self.root_dir = Path(root_dir).expanduser().resolve() if root_dir else None
    
    @property
    def name(self) -> str:
        return "edit_file"
    
    @property
    def description(self) -> str:
        return "Edit a file by replacing old_text with new_text. The old_text must exist exactly in the file."
    
    @property
    def parameters(self) -> dict[str, Any]:
        return {
            "type": "object",
            "properties": {
                "path": {
                    "type": "string",
                    "description": "The file path to edit"
                },
                "old_text": {
                    "type": "string",
                    "description": "The exact text to find and replace"
                },
                "new_text": {
                    "type": "string",
                    "description": "The text to replace with"
                }
            },
            "required": ["path", "old_text", "new_text"]
        }
    
    async def execute(self, path: str, old_text: str, new_text: str, **kwargs: Any) -> str:
        """
        编辑文件内容
        
        Args:
            path: 文件路径
            old_text: 要替换的精确文本
            new_text: 替换后的文本
        
        Returns:
            str: 成功或错误信息
        """
        try:
            # 编辑前先校验目标文件是否在允许范围内。
            file_path = _resolve_path_with_boundary(path, self.root_dir)
            if not file_path.exists():
                return f"Error: File not found: {path}"
            
            content = file_path.read_text(encoding="utf-8")
            
            if old_text not in content:
                return f"Error: old_text not found in file. Make sure it matches exactly."
            
            # 检查匹配次数
            count = content.count(old_text)
            if count > 1:
                return f"Warning: old_text appears {count} times. Please provide more context to make it unique."
            
            new_content = content.replace(old_text, new_text, 1)
            file_path.write_text(new_content, encoding="utf-8")

            return f"Successfully edited {path}"
        except ValueError as e:
            return f"Error: {str(e)}"
        except PermissionError:
            return f"Error: Permission denied: {path}"
        except Exception as e:
            return f"Error editing file: {str(e)}"


class ListDirTool(Tool):
    """
    列出目录工具
    
    列出指定目录的内容，显示文件和子目录。
    
    工具元数据：
    - 名称：list_dir
    - 描述：List the contents of a directory.
    - 参数：path（目录路径）
    
    使用示例：
        result = await registry.execute("list_dir", {"path": "/tmp"})
        # 输出：
        # 📁 config/
        # 📄 README.md
        # 📁 src/
    """

    def __init__(self, root_dir: str | Path | None = None):
        """
        初始化目录列表工具。

        Args:
            root_dir: 可选工作区根目录。设置后将启用越界访问保护。
        """
        self.root_dir = Path(root_dir).expanduser().resolve() if root_dir else None
    
    @property
    def name(self) -> str:
        return "list_dir"
    
    @property
    def description(self) -> str:
        return "List the contents of a directory."
    
    @property
    def parameters(self) -> dict[str, Any]:
        return {
            "type": "object",
            "properties": {
                "path": {
                    "type": "string",
                    "description": "The directory path to list"
                }
            },
            "required": ["path"]
        }
    
    async def execute(self, path: str, **kwargs: Any) -> str:
        """
        列出目录内容
        
        Args:
            path: 目录路径
        
        Returns:
            str: 目录内容列表或错误信息
        """
        try:
            # 列目录同样执行边界校验，避免暴露工作区外文件结构。
            dir_path = _resolve_path_with_boundary(path, self.root_dir)
            if not dir_path.exists():
                return f"Error: Directory not found: {path}"
            if not dir_path.is_dir():
                return f"Error: Not a directory: {path}"
            
            items = []
            for item in sorted(dir_path.iterdir()):
                prefix = "📁 " if item.is_dir() else "📄 "
                items.append(f"{prefix}{item.name}")
            
            if not items:
                return f"Directory {path} is empty"

            return "\n".join(items)
        except ValueError as e:
            return f"Error: {str(e)}"
        except PermissionError:
            return f"Error: Permission denied: {path}"
        except Exception as e:
            return f"Error listing directory: {str(e)}"
