import os
import re
import glob
import asyncio
from typing import List, Optional, Dict, Any, Literal
from pydantic import BaseModel, Field
from .base import BaseTool, ToolOutput


def _read_file_sync(file_path: str) -> List[str]:
    """同步读取文件（在线程池中执行）"""
    with open(file_path, "r", encoding="utf-8") as f:
        return f.readlines()


def _write_file_sync(file_path: str, content: str) -> None:
    """同步写入文件（在线程池中执行）"""
    os.makedirs(os.path.dirname(file_path) or ".", exist_ok=True)
    with open(file_path, "w", encoding="utf-8") as f:
        f.write(content)


def _edit_file_sync(file_path: str, old_string: str, new_string: str, replace_all: bool) -> dict:
    """同步编辑文件（在线程池中执行），返回 {message, start_line}"""
    with open(file_path, "r", encoding="utf-8") as f:
        content = f.read()

    if old_string not in content:
        raise ValueError(f"old_string not found in file content.\nSearched for: {old_string[:100]}...")

    count = content.count(old_string)

    if not replace_all and count > 1:
        raise ValueError(f"old_string found {count} times. Provide more context to make it unique, or use replace_all=True.")

    # 计算 old_string 在文件中的起始行号（1-based）
    idx = content.index(old_string)
    start_line = content[:idx].count('\n') + 1

    new_content = content.replace(old_string, new_string)

    with open(file_path, "w", encoding="utf-8") as f:
        f.write(new_content)

    message = f"Replaced {count} occurrence(s)" if replace_all else "Replaced 1 occurrence"
    return {"message": message, "start_line": start_line}


# --- Read Tool ---
class ReadToolArgs(BaseModel):
    file_path: str = Field(..., description="Absolute path to the file to read")
    start_line: int = Field(
        1, description="Line number to start reading from (1-based)"
    )
    end_line: Optional[int] = Field(None, description="Line number to end reading at")


class ReadTool(BaseTool):
    name = "read"
    description = (
        "Read file content with line numbers. Use this to examine code before editing."
    )
    args_schema = ReadToolArgs

    async def run(
        self, file_path: str, start_line: int = 1, end_line: Optional[int] = None
    ) -> str:
        if not os.path.exists(file_path):
            return f"Error: File not found: {file_path}"

        # LLM 可能传字符串类型，做防御性类型转换
        start_line = int(start_line) if start_line is not None else 1
        end_line = int(end_line) if end_line is not None else None

        try:
            # 使用 asyncio.to_thread 避免阻塞事件循环
            lines = await asyncio.to_thread(_read_file_sync, file_path)

            total_lines = len(lines)
            start_index = max(0, start_line - 1)
            end_index = end_line if end_line else total_lines

            if end_line is None and total_lines > 2000:
                end_index = start_index + 2000
                truncated = True
            else:
                truncated = False

            selected_lines = lines[start_index:end_index]

            output = []
            for i, line in enumerate(selected_lines):
                output.append(f"{start_index + i + 1:6d}| {line.rstrip()}")

            result = "\n".join(output)
            if truncated:
                result += f"\n... (File truncated. Total lines: {total_lines}. Use start_line/end_line to read more)"

            return result
        except Exception as e:
            return f"Error reading file: {str(e)}"


# --- Write Tool ---
class WriteToolArgs(BaseModel):
    file_path: str = Field(..., description="Absolute path to the file to write")
    content: str = Field(..., description="Full content to write to the file")


class WriteTool(BaseTool):
    name = "write"
    description = "Write content to a file. Overwrites existing content. Create directories if needed."
    args_schema = WriteToolArgs

    async def run(self, file_path: str, content: str) -> str:
        try:
            # 使用 asyncio.to_thread 避免阻塞事件循环
            await asyncio.to_thread(_write_file_sync, file_path, content)
            return f"Successfully wrote to {file_path}"
        except Exception as e:
            return f"Error writing file: {str(e)}"


# --- Edit Tool ---
class EditToolArgs(BaseModel):
    file_path: str = Field(..., description="Absolute path to the file to edit")
    old_string: str = Field(..., description="Exact string to match in the file")
    new_string: str = Field(..., description="String to replace the match with")
    replace_all: bool = Field(
        False, description="Replace all occurrences (default: False, requires unique match)"
    )


class EditTool(BaseTool):
    name = "edit"
    description = """Performs exact string replacements in files.

Usage:
- You must use Read tool first before editing. This tool will error if old_string is not found.
- Preserve exact indentation (tabs/spaces) from Read tool output (ignore line number prefix).
- ALWAYS prefer editing existing files. NEVER write new files unless explicitly required.
- The edit will FAIL if old_string is not unique. Provide more context or use replace_all=True.
- Use replace_all for renaming variables across the file.

Example:
  old_string: "def calculate():"
  new_string: "def calculate_total():"
"""
    args_schema = EditToolArgs

    async def run(
        self, file_path: str, old_string: str, new_string: str, replace_all: bool = False
    ) -> str:
        if not os.path.exists(file_path):
            return f"Error: File not found: {file_path}"

        try:
            # 使用 asyncio.to_thread 避免阻塞事件循环
            info = await asyncio.to_thread(
                _edit_file_sync, file_path, old_string, new_string, replace_all
            )
            return ToolOutput(
                content=f"Successfully edited {file_path} ({info['message']})",
                metadata={"start_line": info["start_line"]},
            )
        except ValueError as e:
            return f"Error: {str(e)}"
        except Exception as e:
            return f"Error editing file: {str(e)}"


# --- Glob Tool ---
GLOB_DEFAULT_MAX_RESULTS = 500


class GlobToolArgs(BaseModel):
    pattern: str = Field(
        ..., description='Glob pattern to search for files. NEVER use broad patterns like "**/*". Always use specific patterns with file extensions, e.g. "**/*.py", "src/**/*.ts", "config/*.yaml". If unsure about the directory structure'
    )
    path: str = Field(".", description="Root directory to search in. Use a specific subdirectory (e.g. 'src', 'src/core') instead of '.' when possible to narrow results.")


class GlobTool(BaseTool):
    name = "glob"
    description = """Find files matching a glob pattern.

Important:
- NEVER use "**/*" — always include a file extension or specific directory
- Before globbing, consider using bash "ls" to understand the directory structure first, then use a targeted pattern
- Good: "**/*.py", "src/**/*.ts", "config/*.yaml"
- Bad: "**/*", "*"
- If results are truncated, narrow your search with a more specific pattern or path
- Common noise directories (__pycache__, .git, node_modules, etc.) are excluded automatically
"""
    args_schema = GlobToolArgs

    # 默认排除的目录和文件模式
    EXCLUDED_DIRS = {
        # Python
        "__pycache__", ".mypy_cache", ".pytest_cache", ".tox", ".venv", "venv",
        ".eggs", "egg-info", ".pytype", ".pyre",
        # Node.js
        "node_modules", ".npm", ".yarn", ".pnp",
        # Java / Kotlin / Scala
        "target", ".gradle", ".mvn", "build",
        # Go
        "vendor",
        # Rust
        # ("target" already included above)
        # C / C++
        "CMakeFiles",
        # .NET / C#
        "bin", "obj", ".nuget",
        # Ruby
        ".bundle",
        # IDE / Editor
        ".idea", ".vscode", ".vs", ".eclipse",
        # VCS
        ".git", ".svn", ".hg",
        # OS
        ".DS_Store", "Thumbs.db",
        # Misc
        ".cache", ".tmp", ".temp", "dist", ".next", ".nuxt", ".output",
        ".turbo", ".parcel-cache", ".webpack", ".rollup.cache",
        "coverage", ".nyc_output", "htmlcov",
        "__snapshots__",
    }

    EXCLUDED_EXTENSIONS = {
        # Compiled / bytecode
        ".pyc", ".pyo", ".class", ".o", ".obj", ".a", ".lib", ".so", ".dylib", ".dll",
        ".exe", ".out", ".jar", ".war", ".ear",
        # Package archives
        ".whl", ".egg", ".gem", ".nupkg",
        # Source maps
        ".map",
        # Lock files (usually not useful for exploration)
        ".lock",
    }

    async def run(self, pattern: str, path: str = ".") -> str:
        try:
            full_pattern = os.path.join(path, pattern)

            files = []
            returned = 0
            scanned = 0
            start = asyncio.get_running_loop().time()
            max_seconds = 3.0

            budget_hit = False
            # Use iglob to avoid materializing huge lists in memory.
            for f in glob.iglob(full_pattern, recursive=True):
                scanned += 1
                if scanned % 1000 == 0:
                    await asyncio.sleep(0)  # Yield control to event loop

                # Budget guards
                if asyncio.get_running_loop().time() - start > max_seconds:
                    budget_hit = True
                    break
                if scanned > 200_000:
                    budget_hit = True
                    break
                if returned >= GLOB_DEFAULT_MAX_RESULTS:
                    budget_hit = True
                    break

                if any(part in self.EXCLUDED_DIRS for part in f.split(os.sep)):
                    continue
                if any(f.endswith(ext) for ext in self.EXCLUDED_EXTENSIONS):
                    continue

                files.append(f)
                returned += 1

            output = "\n".join(files)

            # If we didn't exhaust the iterator, treat as truncated.
            if budget_hit:
                output += (
                    f"\n... (truncated, scanned={scanned}, returned={returned}; "
                    "use a more specific pattern/path)"
                )

            return output
        except Exception as e:
            return f"Error executing glob: {str(e)}"


# --- Grep Tool (Enhanced) ---
class GrepToolArgs(BaseModel):
    pattern: str = Field(..., description="Regular expression pattern to search for")
    path: str = Field(".", description="File or directory to search in")
    glob_pattern: Optional[str] = Field(
        None,
        description="Glob pattern to filter files (e.g. '*.py', '**/*.tsx')"
    )
    file_type: Optional[str] = Field(
        None,
        description="File type to search (e.g. 'py', 'js', 'rust'). More efficient than glob for standard types."
    )
    output_mode: Literal["content", "files_with_matches", "count"] = Field(
        "files_with_matches",
        description="Output mode: 'content' shows matching lines, 'files_with_matches' shows file paths, 'count' shows match counts"
    )
    context_before: int = Field(
        0, alias="-B",
        description="Number of lines to show before each match (like grep -B)"
    )
    context_after: int = Field(
        0, alias="-A",
        description="Number of lines to show after each match (like grep -A)"
    )
    context: int = Field(
        0, alias="-C",
        description="Number of lines to show before and after each match (like grep -C)"
    )
    case_insensitive: bool = Field(
        False, alias="-i",
        description="Case insensitive search"
    )
    show_line_numbers: bool = Field(
        True, alias="-n",
        description="Show line numbers in output (for content mode)"
    )
    max_results: int = Field(
        100,
        description="Maximum number of results to return"
    )
    multiline: bool = Field(
        False,
        description="Enable multiline mode where . matches newlines"
    )


class GrepTool(BaseTool):
    """增强版 Grep 工具，支持上下文行显示"""

    name = "grep"
    description = """A powerful search tool for finding patterns in files.

Usage:
- Supports full regex syntax (e.g., "log.*Error", "function\\s+\\w+")
- Filter files with glob parameter (e.g., "*.js", "**/*.tsx") or type parameter (e.g., "js", "py")
- Output modes: "content" shows matching lines, "files_with_matches" shows only file paths (default), "count" shows match counts
- Use -A/-B/-C for context lines around matches
- Use multiline=True for patterns that span multiple lines

Examples:
- Search for function definitions: pattern="def \\w+\\(", path="src/", glob_pattern="*.py"
- Find imports with context: pattern="^import", output_mode="content", context_after=2
- Count occurrences: pattern="TODO", output_mode="count"
"""
    args_schema = GrepToolArgs

    # 文件类型映射
    FILE_TYPE_EXTENSIONS = {
        "py": ["*.py"],
        "python": ["*.py"],
        "js": ["*.js", "*.jsx", "*.mjs"],
        "javascript": ["*.js", "*.jsx", "*.mjs"],
        "ts": ["*.ts", "*.tsx"],
        "typescript": ["*.ts", "*.tsx"],
        "rust": ["*.rs"],
        "go": ["*.go"],
        "java": ["*.java"],
        "c": ["*.c", "*.h"],
        "cpp": ["*.cpp", "*.hpp", "*.cc", "*.hh", "*.cxx"],
        "ruby": ["*.rb"],
        "php": ["*.php"],
        "swift": ["*.swift"],
        "kotlin": ["*.kt", "*.kts"],
        "scala": ["*.scala"],
        "html": ["*.html", "*.htm"],
        "css": ["*.css", "*.scss", "*.sass", "*.less"],
        "json": ["*.json"],
        "yaml": ["*.yaml", "*.yml"],
        "xml": ["*.xml"],
        "md": ["*.md", "*.markdown"],
        "sql": ["*.sql"],
        "sh": ["*.sh", "*.bash"],
        "dockerfile": ["Dockerfile", "*.dockerfile"],
    }

    async def run(
        self,
        pattern: str,
        path: str = ".",
        glob_pattern: Optional[str] = None,
        file_type: Optional[str] = None,
        output_mode: str = "files_with_matches",
        context_before: int = 0,
        context_after: int = 0,
        context: int = 0,
        case_insensitive: bool = False,
        show_line_numbers: bool = True,
        max_results: int = 100,
        multiline: bool = False,
        **kwargs,  # 处理别名参数
    ) -> str:
        # 处理别名参数
        context_before = kwargs.get("-B", context_before)
        context_after = kwargs.get("-A", context_after)
        context = kwargs.get("-C", context)
        case_insensitive = kwargs.get("-i", case_insensitive)
        show_line_numbers = kwargs.get("-n", show_line_numbers)

        # 如果设置了 -C，则同时设置 -A 和 -B
        if context > 0:
            context_before = context
            context_after = context

        try:
            # 编译正则表达式
            flags = re.IGNORECASE if case_insensitive else 0
            if multiline:
                flags |= re.MULTILINE | re.DOTALL

            try:
                regex = re.compile(pattern, flags)
            except re.error as e:
                return f"Error: Invalid regex pattern: {e}"

            # 获取要搜索的文件列表
            files_to_search = self._get_files(path, glob_pattern, file_type)

            if not files_to_search:
                return "No files found matching the criteria."

            # 执行搜索
            results = []
            total_matches = 0

            for file_path in files_to_search:
                if total_matches >= max_results:
                    break

                file_results = self._search_file(
                    file_path, regex, output_mode,
                    context_before, context_after,
                    show_line_numbers, multiline
                )

                if file_results:
                    if output_mode == "count":
                        results.append(f"{file_path}: {file_results}")
                        total_matches += file_results
                    elif output_mode == "files_with_matches":
                        results.append(file_path)
                        total_matches += 1
                    else:  # content
                        results.append(f"=== {file_path} ===\n{file_results}")
                        total_matches += file_results.count("\n") + 1

            if not results:
                return f"No matches found for pattern: {pattern}"

            # 格式化输出
            if output_mode == "count":
                output = "\n".join(results[:max_results])
                output += f"\n\nTotal: {total_matches} matches"
            elif output_mode == "files_with_matches":
                output = "\n".join(results[:max_results])
                if len(results) > max_results:
                    output += f"\n... ({len(results) - max_results} more files)"
            else:
                output = "\n\n".join(results[:max_results])
                if len(results) > max_results:
                    output += f"\n\n... ({len(results) - max_results} more files with matches)"

            return output

        except Exception as e:
            return f"Error executing grep: {str(e)}"

    def _get_files(
        self,
        path: str,
        glob_pattern: Optional[str],
        file_type: Optional[str],
    ) -> List[str]:
        """获取要搜索的文件列表"""
        files = []

        # 如果 path 是文件，直接返回
        if os.path.isfile(path):
            return [path]

        # 确定 glob 模式
        patterns = []
        if glob_pattern:
            patterns.append(glob_pattern)
        elif file_type and file_type.lower() in self.FILE_TYPE_EXTENSIONS:
            patterns.extend(self.FILE_TYPE_EXTENSIONS[file_type.lower()])
        else:
            patterns.append("**/*")

        # 搜索文件
        for pattern in patterns:
            full_pattern = os.path.join(path, pattern)
            matched = glob.glob(full_pattern, recursive=True)
            for f in matched:
                if os.path.isfile(f) and f not in files:
                    # 跳过二进制文件和隐藏目录
                    if not self._is_binary(f) and "/.git/" not in f:
                        files.append(f)

        return sorted(files)

    def _is_binary(self, file_path: str) -> bool:
        """检查文件是否为二进制文件"""
        try:
            with open(file_path, "rb") as f:
                chunk = f.read(1024)
                # 检查是否包含 null 字节
                if b"\x00" in chunk:
                    return True
                # 检查非文本字符比例
                text_chars = bytearray({7, 8, 9, 10, 12, 13, 27} | set(range(0x20, 0x100)))
                non_text = sum(1 for byte in chunk if byte not in text_chars)
                return non_text / len(chunk) > 0.3 if chunk else False
        except:
            return True

    def _search_file(
        self,
        file_path: str,
        regex: re.Pattern,
        output_mode: str,
        context_before: int,
        context_after: int,
        show_line_numbers: bool,
        multiline: bool,
    ):
        """在单个文件中搜索"""
        try:
            with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
                content = f.read()
                lines = content.split("\n")
        except Exception:
            return None

        if output_mode == "count":
            matches = regex.findall(content)
            return len(matches) if matches else None

        if output_mode == "files_with_matches":
            return True if regex.search(content) else None

        # content 模式 - 显示匹配行和上下文
        matching_lines = set()

        if multiline:
            # 多行模式：找到所有匹配的位置
            for match in regex.finditer(content):
                start_pos = match.start()
                end_pos = match.end()
                # 计算起始和结束行号
                start_line = content[:start_pos].count("\n")
                end_line = content[:end_pos].count("\n")
                for line_num in range(start_line, end_line + 1):
                    matching_lines.add(line_num)
        else:
            # 单行模式
            for i, line in enumerate(lines):
                if regex.search(line):
                    matching_lines.add(i)

        if not matching_lines:
            return None

        # 添加上下文行
        lines_to_show = set()
        for line_num in matching_lines:
            start = max(0, line_num - context_before)
            end = min(len(lines), line_num + context_after + 1)
            for i in range(start, end):
                lines_to_show.add(i)

        # 格式化输出
        output_lines = []
        sorted_lines = sorted(lines_to_show)
        prev_line = -2

        for line_num in sorted_lines:
            # 添加分隔符（如果行号不连续）
            if prev_line >= 0 and line_num > prev_line + 1:
                output_lines.append("--")

            # 标记匹配行
            marker = ">" if line_num in matching_lines else " "

            if show_line_numbers:
                output_lines.append(f"{marker}{line_num + 1:6d}| {lines[line_num]}")
            else:
                output_lines.append(f"{marker} {lines[line_num]}")

            prev_line = line_num

        return "\n".join(output_lines)
