"""
工具執行器 - 提供 Agent 可用的工具

基於 fwauto_agent_sdk/tools.py，但工具定義改為 Anthropic 格式。
"""

import asyncio
import os
import re
import subprocess
import sys
from pathlib import Path
from typing import Any

import httpx


class ToolExecutor:
    """
    工具執行器

    提供 Agent 可用的工具：
    - Read: 讀取檔案
    - Write: 寫入檔案
    - Edit: 編輯檔案
    - MultiEdit: 多處編輯
    - Glob: 搜尋檔案
    - Grep: 搜尋內容
    - Bash: 執行命令
    """

    # 搜尋時排除的目錄（不會搜尋這些目錄中的檔案）
    EXCLUDED_DIRS = {
        ".git", "node_modules", ".venv", "venv", "__pycache__",
        ".tox", ".mypy_cache", ".pytest_cache", ".ruff_cache",
        "dist", ".eggs", "*.egg-info",
    }

    # 預設禁止的危險命令
    FORBIDDEN_COMMANDS = [
        "rm -rf /",
        "rm -rf /*",
        "mkfs",
        "dd if=",
        ":(){:|:&};:",
        "shutdown",
        "reboot",
        "halt",
        "poweroff",
        "format c:",
        "del /f /s /q c:",
    ]

    def __init__(
        self,
        project_root: str,
        allowed_dirs: list[str] | None = None,
        forbidden_commands: list[str] | None = None,
    ):
        """
        初始化工具執行器

        Args:
            project_root: 專案根目錄
            allowed_dirs: 允許操作的目錄（預設為 project_root）
            forbidden_commands: 額外禁止的命令
        """
        self.project_root = Path(project_root).resolve()
        self.allowed_dirs = [
            Path(d).resolve() for d in (allowed_dirs or [project_root])
        ]
        self.forbidden_commands = self.FORBIDDEN_COMMANDS + (forbidden_commands or [])

    def is_path_allowed(self, path: Path) -> bool:
        """檢查路徑是否在允許的目錄內"""
        resolved = path.resolve()
        return any(
            resolved == allowed or allowed in resolved.parents
            for allowed in self.allowed_dirs
        )

    async def execute(self, name: str, arguments: dict) -> str:
        """
        執行工具

        Args:
            name: 工具名稱
            arguments: 工具參數

        Returns:
            執行結果
        """
        # 工具名稱映射（支援多種命名）
        name_map = {
            "read_file": "Read",
            "write_file": "Write",
            "edit_file": "Edit",
            "multi_edit": "MultiEdit",
            "list_files": "Glob",
            "glob": "Glob",
            "search_in_files": "Grep",
            "grep": "Grep",
            "bash": "Bash",
            "run_command": "Bash",
        }
        normalized_name = name_map.get(name.lower(), name)

        if normalized_name == "Read":
            return self._read_file(arguments.get("file_path", ""))
        elif normalized_name == "Write":
            return self._write_file(
                arguments.get("file_path", ""),
                arguments.get("content", ""),
            )
        elif normalized_name == "Edit":
            return self._edit_file(
                arguments.get("file_path", ""),
                arguments.get("old_string", ""),
                arguments.get("new_string", ""),
            )
        elif normalized_name == "MultiEdit":
            return self._multi_edit(
                arguments.get("file_path", ""),
                arguments.get("edits", []),
            )
        elif normalized_name == "Glob":
            return self._glob_files(
                arguments.get("pattern", "*"),
                arguments.get("directory", "."),
            )
        elif normalized_name == "Grep":
            return self._grep_content(
                arguments.get("pattern", ""),
                arguments.get("path", "."),
            )
        elif normalized_name == "Bash":
            return await self._run_bash(
                arguments.get("command", ""),
                arguments.get("timeout", 60),
            )
        elif normalized_name == "WebFetch":
            return await self._web_fetch(
                arguments.get("url", ""),
                arguments.get("prompt", ""),
            )
        # WebSearch 使用 Anthropic server-side tool，不需要 client 端執行
        else:
            return f"❌ 未知工具: {name}"

    def _read_file(self, file_path: str) -> str:
        """讀取檔案"""
        if not file_path:
            return "❌ 請提供檔案路徑"

        path = (self.project_root / file_path).resolve()

        if not self.is_path_allowed(path):
            return "❌ 權限拒絕：路徑超出允許目錄"

        if not path.exists():
            return f"❌ 檔案不存在: {file_path}"

        if not path.is_file():
            return f"❌ 不是檔案: {file_path}"

        # PDF 檔案處理
        if path.suffix.lower() == ".pdf":
            return self._read_pdf(path, file_path)

        try:
            content = path.read_text(encoding="utf-8")
            lines = content.split("\n")
            numbered = "\n".join(f"{i+1:4d} | {line}" for i, line in enumerate(lines))
            return f"📄 {file_path} ({len(lines)} 行):\n\n{numbered}"
        except UnicodeDecodeError:
            return f"❌ 無法讀取（二進位檔案）: {file_path}"
        except Exception as e:
            return f"❌ 讀取失敗: {e}"

    def _read_pdf(self, path: Path, file_path: str) -> str:
        """讀取 PDF 檔案"""
        try:
            from PyPDF2 import PdfReader
        except ImportError:
            return (
                f"❌ 無法讀取 PDF: {file_path}\n"
                "需要安裝 PyPDF2: pip install PyPDF2"
            )

        try:
            reader = PdfReader(str(path))
            total_pages = len(reader.pages)
            texts = []
            for i, page in enumerate(reader.pages):
                text = page.extract_text()
                if text:
                    texts.append(f"--- 第 {i+1} 頁 ---\n{text}")

            if not texts:
                return f"❌ PDF 無法提取文字（可能是掃描圖片）: {file_path}"

            content = "\n\n".join(texts)
            # 限制輸出長度
            if len(content) > 15000:
                content = content[:15000] + f"\n\n... (內容截斷，共 {total_pages} 頁)"

            return f"📄 {file_path} (PDF, {total_pages} 頁):\n\n{content}"
        except Exception as e:
            return f"❌ PDF 讀取失敗: {e}"

    def _write_file(self, file_path: str, content: str) -> str:
        """寫入檔案"""
        if not file_path:
            return "❌ 請提供檔案路徑"

        path = (self.project_root / file_path).resolve()

        if not self.is_path_allowed(path):
            return "❌ 權限拒絕：路徑超出允許目錄"

        try:
            path.parent.mkdir(parents=True, exist_ok=True)
            path.write_text(content, encoding="utf-8")
            lines = content.count("\n") + 1
            return f"✅ 已寫入: {file_path} ({lines} 行, {len(content)} 字元)"
        except Exception as e:
            return f"❌ 寫入失敗: {e}"

    def _edit_file(self, file_path: str, old_string: str, new_string: str) -> str:
        """編輯檔案"""
        if not file_path:
            return "❌ 請提供檔案路徑"
        if not old_string:
            return "❌ 請提供要替換的字串"

        path = (self.project_root / file_path).resolve()

        if not self.is_path_allowed(path):
            return "❌ 權限拒絕：路徑超出允許目錄"

        if not path.exists():
            return f"❌ 檔案不存在: {file_path}"

        try:
            content = path.read_text(encoding="utf-8")
        except Exception as e:
            return f"❌ 讀取失敗: {e}"

        count = content.count(old_string)
        if count == 0:
            return "❌ 找不到要替換的字串"
        if count > 1:
            return f"❌ 找到 {count} 處相同字串，請提供更多上下文使其唯一"

        new_content = content.replace(old_string, new_string, 1)

        try:
            path.write_text(new_content, encoding="utf-8")
            return f"✅ 已編輯: {file_path}"
        except Exception as e:
            return f"❌ 寫入失敗: {e}"

    def _multi_edit(self, file_path: str, edits: list[dict]) -> str:
        """多處編輯"""
        if not file_path:
            return "❌ 請提供檔案路徑"
        if not edits:
            return "❌ 請提供編輯列表"

        path = (self.project_root / file_path).resolve()

        if not self.is_path_allowed(path):
            return "❌ 權限拒絕：路徑超出允許目錄"

        if not path.exists():
            return f"❌ 檔案不存在: {file_path}"

        try:
            content = path.read_text(encoding="utf-8")
        except Exception as e:
            return f"❌ 讀取失敗: {e}"

        results = []
        for i, edit in enumerate(edits):
            old_str = edit.get("old_string", "")
            new_str = edit.get("new_string", "")

            if not old_str:
                results.append(f"  {i+1}. ❌ 缺少 old_string")
                continue

            count = content.count(old_str)
            if count == 0:
                results.append(f"  {i+1}. ❌ 找不到字串")
                continue
            if count > 1:
                results.append(f"  {i+1}. ❌ 字串不唯一 ({count} 處)")
                continue

            content = content.replace(old_str, new_str, 1)
            results.append(f"  {i+1}. ✅ 已替換")

        try:
            path.write_text(content, encoding="utf-8")
            return f"✅ MultiEdit 完成 ({file_path}):\n" + "\n".join(results)
        except Exception as e:
            return f"❌ 寫入失敗: {e}"

    def _glob_files(self, pattern: str, directory: str = ".") -> str:
        """搜尋檔案"""
        path = (self.project_root / directory).resolve()

        if not self.is_path_allowed(path):
            return "❌ 權限拒絕：路徑超出允許目錄"

        if not path.exists():
            return f"❌ 目錄不存在: {directory}"

        if not path.is_dir():
            return f"❌ 不是目錄: {directory}"

        try:
            # 過濾掉排除目錄中的結果
            files = []
            for f in sorted(path.glob(pattern)):
                # 檢查路徑中是否包含排除的目錄
                parts = f.relative_to(path).parts
                if any(part in self.EXCLUDED_DIRS for part in parts):
                    continue
                files.append(f)
                if len(files) >= 200:
                    break

            if not files:
                return f"📁 沒有找到符合 '{pattern}' 的檔案"

            result = [f"📁 找到 {len(files)} 個項目:"]
            for f in files:
                try:
                    rel_path = f.relative_to(self.project_root)
                    icon = "📄" if f.is_file() else "📁"
                    result.append(f"  {icon} {rel_path}")
                except ValueError:
                    continue

            return "\n".join(result)
        except Exception as e:
            return f"❌ 搜尋失敗: {e}"

    def _walk_files(self, root_path: Path, max_files: int = 500) -> list[Path]:
        """遍歷目錄，排除 EXCLUDED_DIRS 中的目錄"""
        files = []
        for dirpath, dirnames, filenames in os.walk(root_path):
            # 排除不需要搜尋的目錄（就地修改 dirnames 讓 os.walk 跳過）
            dirnames[:] = [
                d for d in dirnames
                if d not in self.EXCLUDED_DIRS
            ]
            for filename in filenames:
                files.append(Path(dirpath) / filename)
                if len(files) >= max_files:
                    return files
        return files

    def _grep_content(self, pattern: str, path: str = ".") -> str:
        """搜尋內容"""
        if not pattern:
            return "❌ 請提供搜尋文字"

        search_path = (self.project_root / path).resolve()

        if not self.is_path_allowed(search_path):
            return "❌ 權限拒絕：路徑超出允許目錄"

        if not search_path.exists():
            return f"❌ 路徑不存在: {path}"

        try:
            regex = re.compile(pattern, re.IGNORECASE)
        except re.error as e:
            return f"❌ 無效的正則表達式: {e}"

        results = []
        files_searched = 0

        if search_path.is_file():
            files_to_search = [search_path]
        else:
            files_to_search = self._walk_files(search_path)

        for file_path in files_to_search:
            if not file_path.is_file():
                continue

            files_searched += 1
            if files_searched > 200:
                break

            try:
                content = file_path.read_text(encoding="utf-8")
            except (UnicodeDecodeError, PermissionError, OSError):
                continue

            for i, line in enumerate(content.split("\n"), 1):
                if regex.search(line):
                    rel_path = file_path.relative_to(self.project_root)
                    results.append(f"{rel_path}:{i}: {line.strip()[:80]}")

            if len(results) > 100:
                break

        if not results:
            return f"🔍 沒有找到符合 '{pattern}' 的內容"

        return f"🔍 搜尋結果 ({len(results)} 筆):\n" + "\n".join(results)

    def _decode_output(self, data: bytes) -> str:
        """
        解碼命令輸出

        由於我們在 Windows 上使用 chcp 65001 強制 UTF-8，
        優先使用 UTF-8 解碼，失敗時 fallback 到 CP950。
        """
        if not data:
            return ""

        # 優先 UTF-8（因為 chcp 65001）
        try:
            return data.decode("utf-8")
        except UnicodeDecodeError:
            pass

        # Windows fallback 到 CP950
        if sys.platform == "win32":
            try:
                return data.decode("cp950")
            except UnicodeDecodeError:
                pass

        # 最後用 latin-1（永遠不會失敗）
        return data.decode("latin-1")

    async def _run_bash(self, command: str, timeout: int = 300) -> str:
        """執行命令"""
        if not command:
            return "❌ 請提供命令"

        # 安全檢查
        for forbidden in self.forbidden_commands:
            if forbidden.lower() in command.lower():
                return f"❌ 禁止執行的命令: {command}"

        try:
            # 設定環境變數，確保輸出為 UTF-8
            env = os.environ.copy()
            if sys.platform == "win32":
                # Windows: 設定 code page 為 UTF-8
                env["PYTHONIOENCODING"] = "utf-8"
                # 在命令前加入 chcp 65001 切換到 UTF-8
                command = f"chcp 65001 >nul && {command}"

            process = await asyncio.create_subprocess_shell(
                command,
                stdout=asyncio.subprocess.PIPE,
                stderr=asyncio.subprocess.PIPE,
                cwd=str(self.project_root),
                env=env,
            )

            try:
                stdout, stderr = await asyncio.wait_for(
                    process.communicate(),
                    timeout=timeout,
                )
            except asyncio.TimeoutError:
                process.kill()
                return f"[timeout] 命令超時 ({timeout}s): {command}"

            # 解碼輸出：Windows cmd 預設是 CP950，但許多程式輸出 UTF-8
            output = self._decode_output(stdout)
            error = self._decode_output(stderr)

            result = []
            if output:
                result.append(f"[stdout]\n{output}")
            if error:
                result.append(f"[stderr]\n{error}")
            if process.returncode != 0:
                result.append(f"[exit code: {process.returncode}]")

            if not result:
                return "[OK] 命令執行成功（無輸出）"

            # 限制輸出長度
            full_result = "\n".join(result)
            return full_result[:5000] if len(full_result) > 5000 else full_result

        except Exception as e:
            return f"❌ 執行失敗: {e}"

    async def _web_fetch(self, url: str, prompt: str = "") -> str:
        """
        擷取網頁內容

        Args:
            url: 網頁 URL
            prompt: 可選的處理提示（目前僅回傳內容，不做 AI 處理）

        Returns:
            網頁內容（純文字或 Markdown）
        """
        if not url:
            return "Error: Please provide a URL"

        # 確保 URL 有協定前綴
        if not url.startswith(("http://", "https://")):
            url = "https://" + url

        try:
            async with httpx.AsyncClient(
                timeout=30.0,
                follow_redirects=True,
                headers={
                    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
                }
            ) as client:
                response = await client.get(url)
                response.raise_for_status()

                content_type = response.headers.get("content-type", "")

                # 如果是 HTML，嘗試提取主要文字
                if "text/html" in content_type:
                    html = response.text
                    text = self._html_to_text(html)
                    result = f"[WebFetch] URL: {url}\n[Content-Type: {content_type}]\n\n{text}"
                else:
                    # 其他類型直接回傳
                    result = f"[WebFetch] URL: {url}\n[Content-Type: {content_type}]\n\n{response.text}"

                # 限制輸出長度
                if len(result) > 10000:
                    result = result[:10000] + "\n\n... (content truncated, total length: " + str(len(result)) + " chars)"

                return result

        except httpx.TimeoutException:
            return f"Error: Request timeout for URL: {url}"
        except httpx.HTTPStatusError as e:
            return f"Error: HTTP {e.response.status_code} for URL: {url}"
        except httpx.RequestError as e:
            return f"Error: Failed to fetch URL: {url} - {str(e)}"
        except Exception as e:
            return f"Error: {str(e)}"

    def _html_to_text(self, html: str) -> str:
        """
        簡易 HTML 轉純文字

        Args:
            html: HTML 內容

        Returns:
            純文字內容
        """
        import re

        # 移除 script 和 style
        html = re.sub(r'<script[^>]*>.*?</script>', '', html, flags=re.DOTALL | re.IGNORECASE)
        html = re.sub(r'<style[^>]*>.*?</style>', '', html, flags=re.DOTALL | re.IGNORECASE)

        # 移除 HTML 註解
        html = re.sub(r'<!--.*?-->', '', html, flags=re.DOTALL)

        # 將特定標籤轉為換行
        html = re.sub(r'<br\s*/?>', '\n', html, flags=re.IGNORECASE)
        html = re.sub(r'</(p|div|h[1-6]|li|tr)>', '\n', html, flags=re.IGNORECASE)
        html = re.sub(r'<(p|div|h[1-6]|li|tr)[^>]*>', '\n', html, flags=re.IGNORECASE)

        # 移除所有其他 HTML 標籤
        html = re.sub(r'<[^>]+>', '', html)

        # 處理 HTML 實體
        html = html.replace('&nbsp;', ' ')
        html = html.replace('&lt;', '<')
        html = html.replace('&gt;', '>')
        html = html.replace('&amp;', '&')
        html = html.replace('&quot;', '"')
        html = html.replace('&#39;', "'")

        # 清理多餘空白
        html = re.sub(r'\n\s*\n', '\n\n', html)
        html = re.sub(r' +', ' ', html)

        return html.strip()

    # WebSearch 現在使用 Anthropic server-side tool (web_search_20250305)
    # 不再需要 client 端的 _web_search() 實作


# ============================================================================
# 工具定義（Anthropic 格式）
# ============================================================================

TOOL_DEFINITIONS = [
    {
        "name": "Read",
        "description": "讀取檔案內容。支援純文字檔案和 PDF 檔案。在編輯檔案前，請先使用此工具了解檔案內容。支援相對路徑（相對於專案目錄）和絕對路徑。",
        "input_schema": {
            "type": "object",
            "properties": {
                "file_path": {
                    "type": "string",
                    "description": "檔案路徑（相對於專案目錄或絕對路徑），例如 'src/main.c' 或 'docs/datasheet.pdf'"
                }
            },
            "required": ["file_path"]
        }
    },
    {
        "name": "Write",
        "description": "寫入檔案內容（建立新檔案或覆寫現有檔案）",
        "input_schema": {
            "type": "object",
            "properties": {
                "file_path": {
                    "type": "string",
                    "description": "檔案路徑（相對於專案目錄）"
                },
                "content": {
                    "type": "string",
                    "description": "要寫入的檔案內容"
                }
            },
            "required": ["file_path", "content"]
        }
    },
    {
        "name": "Edit",
        "description": "編輯檔案，將指定的舊字串替換為新字串。舊字串必須是唯一的。",
        "input_schema": {
            "type": "object",
            "properties": {
                "file_path": {
                    "type": "string",
                    "description": "檔案路徑（相對於專案目錄）"
                },
                "old_string": {
                    "type": "string",
                    "description": "要被替換的字串（必須唯一存在於檔案中）"
                },
                "new_string": {
                    "type": "string",
                    "description": "替換後的新字串"
                }
            },
            "required": ["file_path", "old_string", "new_string"]
        }
    },
    {
        "name": "Glob",
        "description": "快速搜尋符合 glob 模式的檔案。支援遞迴搜尋，例如 '**/*.py' 搜尋所有子目錄中的 Python 檔案、'**/*.h' 搜尋所有標頭檔。當你需要了解專案結構或尋找特定類型的檔案時，優先使用此工具。會自動排除 .git、node_modules 等目錄。",
        "input_schema": {
            "type": "object",
            "properties": {
                "pattern": {
                    "type": "string",
                    "description": "Glob 模式。使用 '**/' 前綴進行遞迴搜尋，例如 '**/*.py'、'**/*.c'、'**/Makefile'"
                },
                "directory": {
                    "type": "string",
                    "description": "搜尋的起始目錄（相對於專案目錄），預設為專案根目錄"
                }
            },
            "required": ["pattern"]
        }
    },
    {
        "name": "Grep",
        "description": "在檔案或整個目錄樹中搜尋內容（支援正則表達式）。指定目錄時會遞迴搜尋所有子目錄中的檔案。會自動排除 .git、node_modules 等目錄。適合用來搜尋函式定義、變數引用、include 語句等。",
        "input_schema": {
            "type": "object",
            "properties": {
                "pattern": {
                    "type": "string",
                    "description": "搜尋模式（正則表達式），例如 '#include.*Arduino' 或 'void setup'"
                },
                "path": {
                    "type": "string",
                    "description": "搜尋路徑（單一檔案或目錄）。指定目錄時會遞迴搜尋。預設為專案根目錄"
                }
            },
            "required": ["pattern"]
        }
    },
    {
        "name": "Bash",
        "description": "執行 shell 命令。用於編譯、測試、燒錄、安裝依賴等操作。支援長時間執行的命令。",
        "input_schema": {
            "type": "object",
            "properties": {
                "command": {
                    "type": "string",
                    "description": "要執行的 shell 命令"
                },
                "timeout": {
                    "type": "integer",
                    "description": "超時時間（秒），預設 300（5分鐘），燒錄等操作可設更長"
                }
            },
            "required": ["command"]
        }
    },
    {
        "name": "WebFetch",
        "description": "擷取網頁內容。可用於獲取網頁資訊、API 文件、文章等。會自動將 HTML 轉換為純文字。",
        "input_schema": {
            "type": "object",
            "properties": {
                "url": {
                    "type": "string",
                    "description": "要擷取的網頁 URL（例如 https://example.com）"
                },
                "prompt": {
                    "type": "string",
                    "description": "可選的處理提示，描述你想從網頁中提取什麼資訊"
                }
            },
            "required": ["url"]
        }
    },
    # WebSearch 使用 Anthropic server-side tool (web_search_20250305)
    # 定義在 agent.py 的 _get_tools() 中，不在此處
]
