"""File-related tools for the autonomous debugging agent.

This module provides tools for reading files and searching code in the
repository. These tools are used during the investigation phase to
gather context about failures.
"""

from __future__ import annotations

import asyncio
import logging
import re
import subprocess
from pathlib import Path
from typing import Any, Dict, List, Optional

from autodebug.llm_first.autonomous import BaseTool

logger = logging.getLogger(__name__)


class ReadFileTool(BaseTool):
    """Read file contents with validation and safety checks.

    This tool reads files from the filesystem with:
    - Path validation to prevent directory traversal
    - Size limits to avoid memory issues
    - Encoding detection for text files
    - Binary file detection
    """

    name = "read_file"
    description = """
    Read the contents of a file from the repository.

    Parameters:
    - path: File path to read (relative or absolute)
    - start_line: Optional start line (1-indexed)
    - end_line: Optional end line (1-indexed)
    - max_size_kb: Maximum file size to read (default 500KB)

    Returns: File contents as string, or error message

    Use this tool to:
    - Read source code files to understand the code
    - Read test files to understand what's being tested
    - Read configuration files that might affect behavior
    """

    def __init__(
        self,
        base_path: Optional[str] = None,
        max_size_kb: int = 500,
    ):
        """Initialize ReadFileTool.

        Args:
            base_path: Base path to resolve relative paths against.
            max_size_kb: Maximum file size in KB to read.
        """
        self.base_path = Path(base_path) if base_path else Path.cwd()
        self.max_size_kb = max_size_kb

    async def execute(
        self,
        path: str,
        start_line: Optional[int] = None,
        end_line: Optional[int] = None,
        max_size_kb: Optional[int] = None,
    ) -> str:
        """Read file contents.

        Args:
            path: File path to read.
            start_line: Optional start line (1-indexed).
            end_line: Optional end line (1-indexed).
            max_size_kb: Override maximum file size.

        Returns:
            File contents or error message.
        """
        try:
            # Resolve path
            file_path = self._resolve_path(path)

            # Validate path
            validation_error = self._validate_path(file_path)
            if validation_error:
                return f"Error: {validation_error}"

            # Check file size
            size_limit = (max_size_kb or self.max_size_kb) * 1024
            file_size = file_path.stat().st_size

            if file_size > size_limit:
                return (
                    f"Error: File too large ({file_size // 1024}KB). "
                    f"Maximum allowed: {size_limit // 1024}KB. "
                    f"Use start_line/end_line to read a portion."
                )

            # Check if binary
            if self._is_binary(file_path):
                return f"Error: {path} appears to be a binary file"

            # Read file
            content = await self._read_file(file_path)

            # Apply line filtering if specified
            if start_line or end_line:
                content = self._extract_lines(content, start_line, end_line)

            return content

        except FileNotFoundError:
            return f"Error: File not found: {path}"
        except PermissionError:
            return f"Error: Permission denied: {path}"
        except Exception as e:
            logger.error(f"Error reading file {path}: {e}")
            return f"Error reading file: {e}"

    def _resolve_path(self, path: str) -> Path:
        """Resolve path to absolute path.

        Args:
            path: Input path (relative or absolute).

        Returns:
            Resolved absolute path.
        """
        p = Path(path)
        if p.is_absolute():
            return p
        return (self.base_path / p).resolve()

    def _validate_path(self, path: Path) -> Optional[str]:
        """Validate the path is safe to read.

        Args:
            path: Path to validate.

        Returns:
            Error message if invalid, None if valid.
        """
        # Check existence
        if not path.exists():
            return f"File does not exist: {path}"

        # Check it's a file
        if not path.is_file():
            return f"Not a file: {path}"

        # Check for sensitive patterns
        sensitive_patterns = [
            r"\.env$",
            r"\.env\.",
            r"credentials",
            r"secrets?\.ya?ml$",
            r"\.pem$",
            r"\.key$",
            r"id_rsa",
            r"\.ssh/",
        ]

        path_str = str(path).lower()
        for pattern in sensitive_patterns:
            if re.search(pattern, path_str):
                return f"Cannot read sensitive file: {path.name}"

        return None

    def _is_binary(self, path: Path) -> bool:
        """Check if file is binary.

        Args:
            path: Path to check.

        Returns:
            True if file appears to be binary.
        """
        # Check by extension
        binary_extensions = {
            ".pyc", ".pyo", ".so", ".o", ".a",
            ".exe", ".dll", ".bin", ".dat",
            ".png", ".jpg", ".jpeg", ".gif", ".bmp",
            ".pdf", ".doc", ".docx", ".xls", ".xlsx",
            ".zip", ".tar", ".gz", ".bz2", ".7z",
            ".whl", ".egg",
        }

        if path.suffix.lower() in binary_extensions:
            return True

        # Check first bytes
        try:
            with open(path, "rb") as f:
                chunk = f.read(1024)
                # Check for null bytes (common in binary files)
                if b"\x00" in chunk:
                    return True
        except Exception:
            pass

        return False

    async def _read_file(self, path: Path) -> str:
        """Read file contents asynchronously.

        Args:
            path: Path to read.

        Returns:
            File contents as string.
        """
        # Run in thread pool for async compatibility
        loop = asyncio.get_event_loop()
        return await loop.run_in_executor(
            None,
            self._read_file_sync,
            path,
        )

    def _read_file_sync(self, path: Path) -> str:
        """Synchronously read file with encoding detection.

        Args:
            path: Path to read.

        Returns:
            File contents.
        """
        # Try UTF-8 first
        try:
            with open(path, encoding="utf-8") as f:
                return f.read()
        except UnicodeDecodeError:
            pass

        # Fall back to latin-1 (accepts any bytes)
        with open(path, encoding="latin-1") as f:
            return f.read()

    def _extract_lines(
        self,
        content: str,
        start_line: Optional[int],
        end_line: Optional[int],
    ) -> str:
        """Extract specific lines from content.

        Args:
            content: Full file content.
            start_line: Start line (1-indexed).
            end_line: End line (1-indexed).

        Returns:
            Extracted lines.
        """
        lines = content.splitlines()

        # Default values
        start = (start_line or 1) - 1  # Convert to 0-indexed
        end = end_line or len(lines)

        # Clamp to valid range
        start = max(0, start)
        end = min(len(lines), end)

        # Extract and add line numbers
        extracted = []
        for i, line in enumerate(lines[start:end], start=start + 1):
            extracted.append(f"{i:4d}| {line}")

        return "\n".join(extracted)


class SearchCodeTool(BaseTool):
    """Search codebase for patterns using ripgrep.

    This tool searches the codebase for patterns using ripgrep (rg),
    which is fast and respects .gitignore. Falls back to grep if
    ripgrep is not available.
    """

    name = "search_code"
    description = """
    Search the codebase for a pattern using regex.

    Parameters:
    - pattern: Regex pattern to search for
    - file_glob: Optional file pattern (e.g., "*.py", "test_*.py")
    - path: Optional directory to search in
    - max_results: Maximum number of results (default 50)
    - context_lines: Lines of context around matches (default 2)
    - ignore_case: Case-insensitive search (default False)

    Returns: List of matches with file, line, and content

    Use this tool to:
    - Find function/class definitions
    - Find usages of a function or variable
    - Find similar patterns in the codebase
    - Locate where exceptions are raised
    """

    def __init__(
        self,
        base_path: Optional[str] = None,
        max_results: int = 50,
    ):
        """Initialize SearchCodeTool.

        Args:
            base_path: Base path for search.
            max_results: Default maximum results.
        """
        self.base_path = Path(base_path) if base_path else Path.cwd()
        self.max_results = max_results
        self._use_ripgrep = self._check_ripgrep()

    def _check_ripgrep(self) -> bool:
        """Check if ripgrep is available."""
        try:
            subprocess.run(
                ["rg", "--version"],
                capture_output=True,
                check=True,
            )
            return True
        except (subprocess.CalledProcessError, FileNotFoundError):
            return False

    async def execute(
        self,
        pattern: str,
        file_glob: Optional[str] = None,
        path: Optional[str] = None,
        max_results: Optional[int] = None,
        context_lines: int = 2,
        ignore_case: bool = False,
    ) -> List[Dict[str, Any]]:
        """Search codebase for pattern.

        Args:
            pattern: Regex pattern to search.
            file_glob: Optional file pattern filter.
            path: Optional subdirectory to search.
            max_results: Maximum results to return.
            context_lines: Lines of context around matches.
            ignore_case: Case-insensitive search.

        Returns:
            List of match dictionaries.
        """
        try:
            # Validate pattern
            if not pattern or not pattern.strip():
                return [{"error": "Empty pattern provided"}]

            # Determine search path
            search_path = self.base_path
            if path:
                search_path = self._resolve_path(path)
                if not search_path.exists():
                    return [{"error": f"Path does not exist: {path}"}]

            # Build and run search command
            if self._use_ripgrep:
                results = await self._search_ripgrep(
                    pattern=pattern,
                    search_path=search_path,
                    file_glob=file_glob,
                    max_results=max_results or self.max_results,
                    context_lines=context_lines,
                    ignore_case=ignore_case,
                )
            else:
                results = await self._search_grep(
                    pattern=pattern,
                    search_path=search_path,
                    file_glob=file_glob,
                    max_results=max_results or self.max_results,
                    ignore_case=ignore_case,
                )

            return results

        except Exception as e:
            logger.error(f"Search error: {e}")
            return [{"error": f"Search failed: {e}"}]

    def _resolve_path(self, path: str) -> Path:
        """Resolve path to absolute path."""
        p = Path(path)
        if p.is_absolute():
            return p
        return (self.base_path / p).resolve()

    async def _search_ripgrep(
        self,
        pattern: str,
        search_path: Path,
        file_glob: Optional[str],
        max_results: int,
        context_lines: int,
        ignore_case: bool,
    ) -> List[Dict[str, Any]]:
        """Search using ripgrep.

        Args:
            pattern: Regex pattern.
            search_path: Directory to search.
            file_glob: Optional file filter.
            max_results: Maximum results.
            context_lines: Context lines around matches.
            ignore_case: Case-insensitive search.

        Returns:
            List of match dictionaries.
        """
        cmd = ["rg", "--json"]

        if ignore_case:
            cmd.append("-i")

        if context_lines > 0:
            cmd.extend(["-C", str(context_lines)])

        if file_glob:
            cmd.extend(["-g", file_glob])

        cmd.extend([
            "--max-count", str(max_results),
            pattern,
            str(search_path),
        ])

        # Run in thread pool
        loop = asyncio.get_event_loop()
        result = await loop.run_in_executor(
            None,
            lambda: subprocess.run(
                cmd,
                capture_output=True,
                text=True,
                timeout=30,
            ),
        )

        return self._parse_ripgrep_json(result.stdout, max_results)

    def _parse_ripgrep_json(
        self,
        output: str,
        max_results: int,
    ) -> List[Dict[str, Any]]:
        """Parse ripgrep JSON output.

        Args:
            output: Raw JSON lines output.
            max_results: Maximum results to return.

        Returns:
            List of match dictionaries.
        """
        import json

        results = []
        current_match = None
        context_before = []
        context_after = []

        for line in output.strip().split("\n"):
            if not line:
                continue

            try:
                data = json.loads(line)
                msg_type = data.get("type")

                if msg_type == "match":
                    # Save previous match if exists
                    if current_match:
                        current_match["context_before"] = context_before[-3:]
                        current_match["context_after"] = context_after[:3]
                        results.append(current_match)
                        if len(results) >= max_results:
                            break

                    # Start new match
                    match_data = data.get("data", {})
                    path = match_data.get("path", {}).get("text", "")
                    line_number = match_data.get("line_number", 0)
                    lines = match_data.get("lines", {}).get("text", "")

                    current_match = {
                        "file": path,
                        "line": line_number,
                        "content": lines.rstrip(),
                        "context_before": [],
                        "context_after": [],
                    }
                    context_before = []
                    context_after = []

                elif msg_type == "context":
                    ctx_data = data.get("data", {})
                    ctx_line = ctx_data.get("lines", {}).get("text", "").rstrip()
                    ctx_num = ctx_data.get("line_number", 0)

                    if current_match:
                        if ctx_num < current_match["line"]:
                            context_before.append(f"{ctx_num}: {ctx_line}")
                        else:
                            context_after.append(f"{ctx_num}: {ctx_line}")

            except json.JSONDecodeError:
                continue

        # Don't forget the last match
        if current_match and len(results) < max_results:
            current_match["context_before"] = context_before[-3:]
            current_match["context_after"] = context_after[:3]
            results.append(current_match)

        return results

    async def _search_grep(
        self,
        pattern: str,
        search_path: Path,
        file_glob: Optional[str],
        max_results: int,
        ignore_case: bool,
    ) -> List[Dict[str, Any]]:
        """Fallback search using grep.

        Args:
            pattern: Regex pattern.
            search_path: Directory to search.
            file_glob: Optional file filter.
            max_results: Maximum results.
            ignore_case: Case-insensitive search.

        Returns:
            List of match dictionaries.
        """
        cmd = ["grep", "-r", "-n", "--include"]

        if file_glob:
            cmd.append(file_glob)
        else:
            cmd.append("*.py")  # Default to Python files

        if ignore_case:
            cmd.append("-i")

        cmd.extend([pattern, str(search_path)])

        # Run in thread pool
        loop = asyncio.get_event_loop()
        result = await loop.run_in_executor(
            None,
            lambda: subprocess.run(
                cmd,
                capture_output=True,
                text=True,
                timeout=30,
            ),
        )

        return self._parse_grep_output(result.stdout, max_results)

    def _parse_grep_output(
        self,
        output: str,
        max_results: int,
    ) -> List[Dict[str, Any]]:
        """Parse grep output.

        Args:
            output: Raw grep output.
            max_results: Maximum results.

        Returns:
            List of match dictionaries.
        """
        results = []

        for line in output.strip().split("\n"):
            if not line:
                continue

            # Parse grep output: file:line:content
            match = re.match(r"^(.+?):(\d+):(.*)$", line)
            if match:
                results.append({
                    "file": match.group(1),
                    "line": int(match.group(2)),
                    "content": match.group(3),
                    "context_before": [],
                    "context_after": [],
                })

                if len(results) >= max_results:
                    break

        return results
