"""
GrepTool - Search file contents with regex patterns

Following OpenCode architecture, provides content search with:
- Regex pattern matching via Python re module
- Optional ripgrep (rg) fallback for performance
- File glob filtering
- Case-insensitive option
- .gitignore respect
- Type-safe implementation

Security Features (SEC-011):
- ReDoS protection with pattern complexity limits
- Regex operation timeout
- Pattern validation before compilation
"""

import re
import shutil
import signal
import subprocess
import threading
from functools import wraps
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional

from groknroll.tools.base_tool import BaseTool
from groknroll.tools.descriptions import get_enhanced_description


# SEC-011: ReDoS protection constants
MAX_PATTERN_LENGTH = 1000  # Maximum regex pattern length
MAX_QUANTIFIER_LIMIT = 100  # Maximum repetition in quantifiers like {n,m}
REGEX_TIMEOUT_SECONDS = 5.0  # Timeout for regex operations

# Dangerous patterns that could cause catastrophic backtracking
REDOS_PATTERNS = [
    # Nested quantifiers
    re.compile(r'\([^)]*[+*]\)[+*]'),  # (a+)+ or (a*)*
    re.compile(r'\([^)]*\{[^}]+\}\)[+*]'),  # (a{1,10})+
    # Overlapping alternations with quantifiers
    re.compile(r'\(.*\|.*\)[+*]'),  # (a|aa)+
    # Multiple adjacent quantifiers
    re.compile(r'[+*?]\s*[+*?]'),  # a++, a*?, etc (except for possessive in some engines)
]


class RegexTimeoutError(Exception):
    """Raised when regex operation times out."""
    pass


class RegexComplexityError(ValueError):
    """Raised when regex pattern is too complex."""
    pass


def validate_regex_pattern(pattern: str) -> None:
    """
    Validate regex pattern for potential ReDoS vulnerabilities.

    SEC-011: ReDoS protection - validates patterns before compilation.

    Args:
        pattern: The regex pattern to validate

    Raises:
        RegexComplexityError: If pattern is potentially dangerous
        ValueError: If pattern is invalid
    """
    # Check pattern length
    if len(pattern) > MAX_PATTERN_LENGTH:
        raise RegexComplexityError(
            f"Regex pattern exceeds maximum length of {MAX_PATTERN_LENGTH} characters. "
            f"Pattern length: {len(pattern)}"
        )

    # Check for dangerous patterns that could cause catastrophic backtracking
    for dangerous_pattern in REDOS_PATTERNS:
        if dangerous_pattern.search(pattern):
            raise RegexComplexityError(
                f"Regex pattern contains potentially dangerous constructs that could cause "
                f"catastrophic backtracking (ReDoS). Please simplify the pattern."
            )

    # Check for excessive quantifiers
    quantifier_pattern = re.compile(r'\{(\d+)(?:,(\d+))?\}')
    for match in quantifier_pattern.finditer(pattern):
        min_val = int(match.group(1))
        max_val = int(match.group(2)) if match.group(2) else min_val

        if max_val > MAX_QUANTIFIER_LIMIT:
            raise RegexComplexityError(
                f"Regex quantifier exceeds maximum limit of {MAX_QUANTIFIER_LIMIT}. "
                f"Found: {{{min_val},{max_val}}}"
            )

    # Validate pattern compiles correctly
    try:
        re.compile(pattern)
    except re.error as e:
        raise ValueError(f"Invalid regex pattern: {e}")


def regex_with_timeout(func: Callable) -> Callable:
    """
    Decorator to add timeout to regex operations.

    SEC-011: Prevents long-running regex operations.

    Args:
        func: Function to wrap

    Returns:
        Wrapped function with timeout
    """
    @wraps(func)
    def wrapper(*args, **kwargs):
        result = [None]
        exception = [None]

        def target():
            try:
                result[0] = func(*args, **kwargs)
            except Exception as e:
                exception[0] = e

        thread = threading.Thread(target=target)
        thread.daemon = True
        thread.start()
        thread.join(timeout=REGEX_TIMEOUT_SECONDS)

        if thread.is_alive():
            raise RegexTimeoutError(
                f"Regex operation timed out after {REGEX_TIMEOUT_SECONDS} seconds. "
                f"The pattern may be too complex."
            )

        if exception[0]:
            raise exception[0]

        return result[0]

    return wrapper


class GrepMatch:
    """
    Result of a grep match

    Attributes:
        file: Path to the file containing the match
        line: Line number (1-indexed)
        content: Content of the matching line
        match: The matched text
    """

    def __init__(self, file: Path, line: int, content: str, match: str):
        self.file = file
        self.line = line
        self.content = content
        self.match = match

    def to_dict(self) -> Dict[str, Any]:
        """Convert match to dictionary"""
        return {
            "file": str(self.file),
            "line": self.line,
            "content": self.content,
            "match": self.match,
        }

    def __str__(self) -> str:
        """String representation"""
        return f"{self.file}:{self.line}:{self.content}"


class GrepTool(BaseTool):
    """
    Tool for searching file contents with regex patterns

    Accepts:
        pattern: str - Regex pattern to search for
        glob: str (optional) - Glob pattern to filter files (e.g., "*.py")
        case_insensitive: bool (optional) - Case-insensitive search (default: False)
        max_results: int (optional) - Maximum number of results (default: 100)

    Returns:
        List[GrepMatch] - List of matches with file, line, content

    Raises:
        ValueError: If pattern is empty or invalid regex

    Example:
        tool = GrepTool()
        results = await tool.execute(pattern="def.*test", glob="*.py")
        for match in results:
            print(f"{match.file}:{match.line}: {match.content}")
    """

    def __init__(self, workspace_root: Optional[str] = None):
        """
        Initialize GrepTool

        Args:
            workspace_root: Optional root directory for searching.
                           Defaults to current working directory.
        """
        self._workspace_root = Path(workspace_root) if workspace_root else Path.cwd()
        self._has_ripgrep = shutil.which("rg") is not None

    @property
    def name(self) -> str:
        """Tool identifier"""
        return "grep"

    @property
    def description(self) -> str:
        """Human-readable description"""
        return get_enhanced_description("grep", "Search file contents with regex patterns")

    def validate_params(self, **kwargs) -> dict[str, Any]:
        """
        Validate parameters before execution

        Args:
            **kwargs: Must contain 'pattern' parameter

        Returns:
            Validated parameters dict

        Raises:
            ValueError: If required parameters are missing or invalid
        """
        if "pattern" not in kwargs:
            raise ValueError("pattern parameter is required")

        pattern = kwargs["pattern"]
        if not isinstance(pattern, str):
            raise ValueError(f"pattern must be a string, got {type(pattern).__name__}")

        if not pattern.strip():
            raise ValueError("pattern cannot be empty")

        # SEC-011: Validate pattern for ReDoS vulnerabilities and validity
        validate_regex_pattern(pattern)

        # Validate glob if provided
        if "glob" in kwargs and kwargs["glob"] is not None:
            glob = kwargs["glob"]
            if not isinstance(glob, str):
                raise ValueError(f"glob must be a string or None, got {type(glob).__name__}")

        # Validate case_insensitive if provided
        if "case_insensitive" in kwargs:
            case_insensitive = kwargs["case_insensitive"]
            if not isinstance(case_insensitive, bool):
                raise ValueError(
                    f"case_insensitive must be a boolean, got {type(case_insensitive).__name__}"
                )

        # Validate max_results if provided
        if "max_results" in kwargs:
            max_results = kwargs["max_results"]
            if not isinstance(max_results, int):
                raise ValueError(
                    f"max_results must be an integer, got {type(max_results).__name__}"
                )
            if max_results <= 0:
                raise ValueError(f"max_results must be positive, got {max_results}")

        return kwargs

    async def execute(self, **kwargs) -> List[GrepMatch]:
        """
        Execute grep search

        Args:
            **kwargs: Must contain 'pattern' parameter

        Returns:
            List[GrepMatch] with search results

        Raises:
            ValueError: If pattern is invalid
        """
        # Validate parameters
        validated = self.validate_params(**kwargs)
        pattern = validated["pattern"]
        glob = validated.get("glob")
        case_insensitive = validated.get("case_insensitive", False)
        max_results = validated.get("max_results", 100)

        # Try ripgrep first if available, otherwise use Python regex
        if self._has_ripgrep:
            return await self._grep_with_ripgrep(pattern, glob, case_insensitive, max_results)
        else:
            return await self._grep_with_python(pattern, glob, case_insensitive, max_results)

    async def _grep_with_ripgrep(
        self, pattern: str, glob: Optional[str], case_insensitive: bool, max_results: int
    ) -> List[GrepMatch]:
        """Use ripgrep for searching"""
        cmd = ["rg", "--line-number", "--no-heading", "--with-filename"]

        if case_insensitive:
            cmd.append("--ignore-case")

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

        cmd.extend(["--max-count", str(max_results)])
        cmd.append(pattern)
        cmd.append(str(self._workspace_root))

        try:
            result = subprocess.run(
                cmd,
                capture_output=True,
                text=True,
                timeout=30,
            )

            matches = []
            for line in result.stdout.splitlines()[:max_results]:
                # Format: file:line:content
                parts = line.split(":", 2)
                if len(parts) >= 3:
                    file_path = Path(parts[0])
                    line_num = int(parts[1])
                    content = parts[2]

                    matches.append(
                        GrepMatch(
                            file=file_path,
                            line=line_num,
                            content=content,
                            match=pattern,
                        )
                    )

            return matches

        except (subprocess.TimeoutExpired, subprocess.CalledProcessError, ValueError):
            # Fall back to Python regex if ripgrep fails
            return await self._grep_with_python(pattern, glob, case_insensitive, max_results)

    async def _grep_with_python(
        self, pattern: str, glob: Optional[str], case_insensitive: bool, max_results: int
    ) -> List[GrepMatch]:
        """
        Use Python regex for searching with streaming reads.

        PERF-004: Uses line-by-line streaming instead of loading entire files.
        SEC-011: Uses validated patterns with timeout protection.
        """
        flags = re.IGNORECASE if case_insensitive else 0

        # SEC-011: Compile pattern (already validated in validate_params)
        try:
            regex = re.compile(pattern, flags)
        except re.error as e:
            raise ValueError(f"Failed to compile regex pattern: {e}")

        matches = []

        # Determine which files to search
        if glob:
            files = list(self._workspace_root.glob(f"**/{glob}"))
        else:
            files = list(self._workspace_root.glob("**/*"))

        # Filter out directories and ignored paths
        files = [f for f in files if f.is_file() and not self._should_ignore(f)]

        # Search each file using streaming with timeout tracking
        start_time = __import__('time').time()
        total_timeout = REGEX_TIMEOUT_SECONDS * 10  # Allow more time for multi-file search

        for file_path in files:
            if len(matches) >= max_results:
                break

            # SEC-011: Check total elapsed time
            if __import__('time').time() - start_time > total_timeout:
                raise RegexTimeoutError(
                    f"Total search time exceeded {total_timeout} seconds. "
                    f"Found {len(matches)} matches before timeout."
                )

            file_matches = await self._search_file_streaming(
                file_path, regex, max_results - len(matches)
            )
            matches.extend(file_matches)

        return matches

    async def _search_file_streaming(
        self, file_path: Path, regex: re.Pattern, max_matches: int
    ) -> List[GrepMatch]:
        """
        Search a single file using streaming line-by-line reads.

        PERF-004: Avoids loading entire file into memory.
        Memory usage is O(max_line_length) instead of O(file_size).

        SEC-011: Uses timeout protection per line to prevent ReDoS.

        Args:
            file_path: Path to file to search
            regex: Compiled regex pattern
            max_matches: Maximum matches to return (early termination)

        Returns:
            List of matches found in file
        """
        matches = []
        lines_processed = 0
        max_lines_per_file = 100000  # Limit lines per file to prevent hanging

        try:
            # Use buffered line-by-line reading for memory efficiency
            with open(file_path, 'r', encoding='utf-8', errors='replace') as f:
                for line_num, line in enumerate(f, start=1):
                    if len(matches) >= max_matches:
                        break  # Early termination

                    # SEC-011: Limit lines processed per file
                    lines_processed += 1
                    if lines_processed > max_lines_per_file:
                        break

                    # Remove trailing newline for cleaner output
                    line = line.rstrip('\n\r')

                    # SEC-011: Skip very long lines that could cause ReDoS
                    if len(line) > 10000:
                        continue

                    try:
                        match = regex.search(line)
                        if match:
                            matches.append(
                                GrepMatch(
                                    file=file_path,
                                    line=line_num,
                                    content=line,
                                    match=match.group(0),
                                )
                            )
                    except (RecursionError, MemoryError):
                        # SEC-011: Pattern caused excessive resource usage on this line
                        continue

        except (UnicodeDecodeError, PermissionError, OSError):
            # Skip files that can't be read
            pass

        return matches

    def _should_ignore(self, file_path: Path) -> bool:
        """Check if file should be ignored based on common patterns"""
        ignore_patterns = {
            ".git",
            "__pycache__",
            "node_modules",
            ".venv",
            "venv",
            ".env",
            ".pytest_cache",
            ".mypy_cache",
            ".ruff_cache",
            "dist",
            "build",
            "*.pyc",
            "*.pyo",
            "*.so",
            "*.dylib",
        }

        path_str = str(file_path)
        for pattern in ignore_patterns:
            if pattern in path_str:
                return True

        return False
