"""Search and pattern matching tools.

Provides grep-like functionality with:
- Path traversal prevention (all paths resolve within repo_root)
- Encoding fallbacks (UTF-8 -> Latin-1)
- Size limits (1MB max)
- Binary file detection and skipping
- Hidden/non-code directory filtering
"""

from __future__ import annotations

import re
from pathlib import Path
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from .executor import ToolResult

# Constants
MAX_FILE_SIZE = 1024 * 1024  # 1MB
SKIP_DIRS = frozenset({
    ".git",
    ".hg",
    ".svn",
    "node_modules",
    "__pycache__",
    ".venv",
    "venv",
    ".env",
    ".tox",
    ".pytest_cache",
    ".mypy_cache",
    ".ruff_cache",
    "dist",
    "build",
    ".eggs",
    "*.egg-info",
})


def _get_tool_result() -> type:
    """Import ToolResult lazily to avoid circular imports."""
    from .executor import ToolResult
    return ToolResult


def _resolve_safe_path(path: str | Path, repo_root: Path) -> Path | str:
    """Resolve path safely within repo_root.

    Args:
        path: The path to resolve (absolute or relative)
        repo_root: The repository root that all paths must resolve within

    Returns:
        Resolved Path if safe, or error string if path traversal detected
    """
    try:
        repo_root = repo_root.resolve()

        # Handle both absolute and relative paths
        path_obj = Path(path)
        if path_obj.is_absolute():
            resolved = path_obj.resolve()
        else:
            resolved = (repo_root / path_obj).resolve()

        # Security check: ensure path is within repo_root
        try:
            resolved.relative_to(repo_root)
        except ValueError:
            return f"Error: Path traversal detected: {path}"

        return resolved
    except Exception as e:
        return f"Error: Invalid path '{path}': {e}"


def _is_binary_file(file_path: Path) -> bool:
    """Check if file is binary by reading first 8KB.

    Args:
        file_path: Path to the file to check

    Returns:
        True if file appears to be binary, False otherwise
    """
    try:
        chunk_size = 8192
        with open(file_path, "rb") as f:
            chunk = f.read(chunk_size)
        # Check for null bytes (common indicator of binary)
        return b"\x00" in chunk
    except Exception:
        return True  # Assume binary if we can't read it


def _should_skip_dir(dir_name: str) -> bool:
    """Check if directory should be skipped.

    Args:
        dir_name: Name of the directory

    Returns:
        True if directory should be skipped
    """
    # Skip hidden directories
    if dir_name.startswith("."):
        return True
    # Skip known non-code directories
    return dir_name in SKIP_DIRS


def _read_file_with_fallback(file_path: Path) -> str | None:
    """Read file with UTF-8 -> Latin-1 encoding fallback.

    Args:
        file_path: Path to the file to read

    Returns:
        File contents as string, or None if file cannot be read
    """
    try:
        return file_path.read_text(encoding="utf-8")
    except UnicodeDecodeError:
        try:
            return file_path.read_text(encoding="latin-1")
        except Exception:
            return None
    except Exception:
        return None


async def grep(
    pattern: str,
    repo_root: Path,
    path: str = ".",
    max_results: int = 100,
) -> "ToolResult":
    """Search for regex pattern in files.

    Searches recursively through files in the specified path, returning
    matches as a list of "file:line:content" strings. Respects path safety,
    skips binary files, hidden directories, and common non-code directories.

    Args:
        pattern: Regular expression pattern to search for
        repo_root: Repository root directory for path safety validation
        path: Path to search (relative to repo_root), defaults to "."
        max_results: Maximum number of matches to return (default: 100)

    Returns:
        ToolResult with success=True and data=list[str] of matches in
        "file:line:content" format, or success=False and error on failure.

    Example:
        >>> result = await grep(r"def\\s+\\w+", Path("/repo"), "src")
        >>> if result.success:
        ...     for match in result.data:
        ...         print(match)
        src/main.py:10:def hello():
        src/utils.py:5:def helper():
    """
    ToolResult = _get_tool_result()

    # Validate regex pattern
    try:
        regex = re.compile(pattern)
    except re.error as e:
        return ToolResult(success=False, error=f"Invalid regex pattern: {pattern}")

    # Resolve and validate path
    resolved = _resolve_safe_path(path, repo_root)
    if isinstance(resolved, str):
        return ToolResult(success=False, error=resolved.replace("Error: ", ""))

    # Check path exists
    if not resolved.exists():
        return ToolResult(success=False, error=f"Path not found: {path}")

    repo_root_resolved = repo_root.resolve()
    matches: list[str] = []

    # Handle single file
    if resolved.is_file():
        # Check file size
        try:
            if resolved.stat().st_size > MAX_FILE_SIZE:
                return ToolResult(success=False, error=f"File too large: {path}")
        except OSError:
            return ToolResult(success=False, error=f"Cannot access file: {path}")

        # Check if binary
        if _is_binary_file(resolved):
            return ToolResult(success=True, data=[])

        # Search file
        content = _read_file_with_fallback(resolved)
        if content is None:
            return ToolResult(success=False, error=f"Cannot read file: {path}")

        try:
            rel_path_str = str(resolved.relative_to(repo_root_resolved))
        except ValueError:
            rel_path_str = resolved.name

        for line_num, line in enumerate(content.splitlines(), 1):
            if regex.search(line):
                matches.append(f"{rel_path_str}:{line_num}:{line}")
                if len(matches) >= max_results:
                    break

        return ToolResult(success=True, data=matches)

    # Handle directory - recursive search
    if resolved.is_dir():
        for file_path in resolved.rglob("*"):
            # Check result limit
            if len(matches) >= max_results:
                break

            # Skip directories
            if not file_path.is_file():
                continue

            # Skip files in excluded directories
            try:
                rel_to_search = file_path.relative_to(resolved)
                parts = rel_to_search.parts
                if any(_should_skip_dir(part) for part in parts[:-1]):
                    continue
            except ValueError:
                continue

            # Skip large files
            try:
                if file_path.stat().st_size > MAX_FILE_SIZE:
                    continue
            except OSError:
                continue

            # Skip binary files
            if _is_binary_file(file_path):
                continue

            # Read and search file
            content = _read_file_with_fallback(file_path)
            if content is None:
                continue

            try:
                rel_path_str = str(file_path.relative_to(repo_root_resolved))
            except ValueError:
                rel_path_str = file_path.name

            for line_num, line in enumerate(content.splitlines(), 1):
                if regex.search(line):
                    matches.append(f"{rel_path_str}:{line_num}:{line}")
                    if len(matches) >= max_results:
                        break

        return ToolResult(success=True, data=matches)

    return ToolResult(success=False, error=f"Invalid path type: {path}")
