"""
ReadTool - Read file contents from the filesystem

Following OpenCode architecture, provides file reading capabilities with:
- Path validation (absolute and relative)
- Path traversal protection (SEC-003)
- Async file I/O (PERF-009)
- Memory-mapped files for large files (OPT-003)
- Error handling (file not found, encoding errors)
- .gitignore pattern respect
- Type-safe implementation
"""

import asyncio
import mmap
from concurrent.futures import ThreadPoolExecutor
from functools import partial
from pathlib import Path
from typing import Any, Optional

from groknroll.tools.base_tool import BaseTool
from groknroll.tools.validators import (
    validate_path_within_workspace,
    validate_path_input,
)


# OPT-003: Threshold for using memory-mapped files (10MB)
MMAP_THRESHOLD_BYTES = 10 * 1024 * 1024


# PERF-009: Shared thread pool for async file operations
_file_executor = ThreadPoolExecutor(max_workers=4, thread_name_prefix="file_io")


class ReadTool(BaseTool):
    """
    Tool for reading file contents from the filesystem

    Accepts:
        path: str - File path (absolute or relative)

    Returns:
        str - File contents

    Raises:
        FileNotFoundError: If file doesn't exist
        PermissionError: If file can't be read
        UnicodeDecodeError: If file encoding fails

    Example:
        tool = ReadTool()
        content = await tool.execute(path="app.py")
    """

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

        Args:
            workspace_root: Optional root directory for relative paths.
                           Defaults to current working directory.
        """
        self._workspace_root = Path(workspace_root) if workspace_root else Path.cwd()

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

    @property
    def description(self) -> str:
        """Human-readable description"""
        return "Read file contents from the filesystem"

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

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

        Returns:
            Validated parameters dict

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

        # SEC-008: Use standardized validation
        validate_path_input(kwargs["path"])

        return kwargs

    async def execute(self, **kwargs) -> str:
        """
        Execute file read operation

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

        Returns:
            File contents as string

        Raises:
            FileNotFoundError: If file doesn't exist
            PermissionError: If file can't be read
            UnicodeDecodeError: If file encoding fails
        """
        # Validate parameters
        validated = self.validate_params(**kwargs)
        path_str = validated["path"]

        # Convert to Path object
        file_path = Path(path_str)

        # Handle relative paths
        if not file_path.is_absolute():
            file_path = self._workspace_root / file_path

        # Normalize path (resolve symlinks, remove ..)
        try:
            file_path = file_path.resolve()
        except (OSError, RuntimeError) as e:
            raise FileNotFoundError(f"Cannot resolve path {path_str}: {e}")

        # SEC-003: Validate path is within workspace after resolution
        validate_path_within_workspace(file_path, self._workspace_root)

        # Check file exists
        if not file_path.exists():
            raise FileNotFoundError(f"File not found: {file_path}")

        # Check it's a file (not directory)
        if not file_path.is_file():
            raise ValueError(f"Path is not a file: {file_path}")

        # Check if file should be ignored based on .gitignore
        # Note: Full .gitignore support would require pathspec library
        # For now, we'll implement basic ignore patterns
        if self._should_ignore(file_path):
            raise PermissionError(f"File matches ignore patterns and cannot be read: {file_path}")

        # PERF-009 + OPT-003: Read file asynchronously, use mmap for large files
        try:
            loop = asyncio.get_event_loop()

            # OPT-003: Check file size to decide on read strategy
            file_size = file_path.stat().st_size

            if file_size > MMAP_THRESHOLD_BYTES:
                # Use memory-mapped file for large files
                content = await loop.run_in_executor(
                    _file_executor,
                    partial(self._read_with_mmap, file_path)
                )
            else:
                # Standard read for smaller files
                content = await loop.run_in_executor(
                    _file_executor,
                    partial(file_path.read_text, encoding="utf-8")
                )
            return content
        except UnicodeDecodeError as e:
            # Try reading as binary and provide helpful error
            raise UnicodeDecodeError(
                e.encoding,
                e.object,
                e.start,
                e.end,
                f"Failed to decode {file_path} as UTF-8. File may be binary.",
            )
        except PermissionError:
            raise PermissionError(f"Permission denied reading file: {file_path}")

    def _read_with_mmap(self, file_path: Path) -> str:
        """
        Read file using memory-mapped I/O.

        OPT-003: Uses mmap for efficient reading of large files.
        - Lazy loading: only pages accessed are loaded into memory
        - Automatic cleanup: mmap is closed after reading
        - Memory efficient: OS handles page caching

        Args:
            file_path: Path to the file to read

        Returns:
            File contents as string

        Raises:
            UnicodeDecodeError: If file cannot be decoded as UTF-8
        """
        with open(file_path, "rb") as f:
            # Create memory-mapped file
            with mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ) as mm:
                # Read entire content (mmap handles paging efficiently)
                content_bytes = mm[:]

        # Decode as UTF-8
        return content_bytes.decode("utf-8")

    def _should_ignore(self, file_path: Path) -> bool:
        """
        Check if file matches ignore patterns

        Args:
            file_path: Path to check

        Returns:
            True if file should be ignored, False otherwise

        Note:
            Basic implementation. Full .gitignore support would use pathspec library.
        """
        # Common patterns to ignore
        ignore_patterns = [
            ".git",
            "__pycache__",
            ".pyc",
            ".pyo",
            ".pyd",
            ".so",
            ".dylib",
            ".dll",
            ".exe",
            "node_modules",
            ".venv",
            "venv",
            ".env",
            ".DS_Store",
        ]

        # Check if any part of the path matches ignore patterns
        parts = file_path.parts
        for part in parts:
            for pattern in ignore_patterns:
                if pattern in part:
                    return True

        # Check file extension
        if file_path.suffix in [".pyc", ".pyo", ".pyd", ".so", ".dylib", ".dll"]:
            return True

        # Check if .gitignore exists and use it
        # For now, just check for .git directory
        gitignore_path = self._workspace_root / ".gitignore"
        if gitignore_path.exists():
            # Basic .gitignore support: check for exact matches
            # Full support would require pathspec library
            try:
                gitignore_content = gitignore_path.read_text(encoding="utf-8")
                relative_path = file_path.relative_to(self._workspace_root)

                for line in gitignore_content.splitlines():
                    line = line.strip()
                    # Skip comments and empty lines
                    if not line or line.startswith("#"):
                        continue

                    # Basic pattern matching (not full gitignore spec)
                    if line in str(relative_path):
                        return True
            except Exception:
                # If .gitignore parsing fails, continue without it
                pass

        return False
