"""
EditTool - Edit files with precise text replacements

Following OpenCode architecture, provides file editing capabilities with:
- Exact text matching and replacement
- Ambiguity detection (multiple matches)
- Optional replace-all mode
- Unified diff generation
- Preserves file encoding
- Type-safe implementation
"""

from pathlib import Path
from typing import Any, Optional, Dict
import difflib

from groknroll.tools.base_tool import BaseTool


class EditResult:
    """
    Result of an edit operation

    Attributes:
        success: Whether the edit succeeded
        path: Path to the edited file
        replacements: Number of replacements made
        diff: Unified diff showing changes
        old_content: Original file content
        new_content: Modified file content
    """

    def __init__(
        self,
        success: bool,
        path: Path,
        replacements: int,
        diff: str,
        old_content: str,
        new_content: str,
    ):
        self.success = success
        self.path = path
        self.replacements = replacements
        self.diff = diff
        self.old_content = old_content
        self.new_content = new_content

    def to_dict(self) -> Dict[str, Any]:
        """Convert result to dictionary"""
        return {
            "success": self.success,
            "path": str(self.path),
            "replacements": self.replacements,
            "diff": self.diff,
        }

    def __str__(self) -> str:
        """String representation"""
        return f"Edited {self.path} ({self.replacements} replacement{'s' if self.replacements != 1 else ''})"


class EditTool(BaseTool):
    """
    Tool for editing files with precise text replacements

    Accepts:
        path: str - File path (absolute or relative)
        old: str - Text to find and replace
        new: str - Replacement text
        replace_all: bool (optional) - Replace all occurrences (default: False)

    Returns:
        EditResult - Edit operation result with diff

    Raises:
        FileNotFoundError: If file doesn't exist
        ValueError: If old text not found or appears multiple times (without replace_all)
        PermissionError: If file can't be written

    Example:
        tool = EditTool()
        result = await tool.execute(
            path="app.py",
            old="def old_func():",
            new="def new_func():"
        )
        print(result.diff)  # Shows unified diff
    """

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

        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 "edit"

    @property
    def description(self) -> str:
        """Human-readable description"""
        return "Edit files with precise text replacements"

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

        Args:
            **kwargs: Must contain 'path', 'old', 'new' parameters

        Returns:
            Validated parameters dict

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

        if "old" not in kwargs:
            raise ValueError("old parameter is required")

        if "new" not in kwargs:
            raise ValueError("new parameter is required")

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

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

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

        if not old:  # Empty string is valid for 'new', but not for 'old'
            raise ValueError("old parameter cannot be empty")

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

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

        return kwargs

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

        Args:
            **kwargs: Must contain 'path', 'old', 'new' parameters

        Returns:
            EditResult with operation details and diff

        Raises:
            FileNotFoundError: If file doesn't exist
            ValueError: If old text not found or appears multiple times
            PermissionError: If file can't be written
        """
        # Validate parameters
        validated = self.validate_params(**kwargs)
        path_str = validated["path"]
        old_text = validated["old"]
        new_text = validated["new"]
        replace_all = validated.get("replace_all", False)

        # 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
        try:
            file_path = file_path.resolve()
        except (OSError, RuntimeError) as e:
            raise ValueError(f"Cannot resolve path {path_str}: {e}")

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

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

        # Read file content
        try:
            old_content = file_path.read_text(encoding="utf-8")
        except UnicodeDecodeError as e:
            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}")

        # Count occurrences of old_text
        count = old_content.count(old_text)

        if count == 0:
            raise ValueError(
                f"Text to replace not found in {file_path}:\n{repr(old_text)}"
            )

        if count > 1 and not replace_all:
            raise ValueError(
                f"Text appears {count} times in {file_path}. "
                f"Use replace_all=True to replace all occurrences, or make the text more specific."
            )

        # Perform replacement
        if replace_all:
            new_content = old_content.replace(old_text, new_text)
            replacements = count
        else:
            # Replace only the first occurrence
            new_content = old_content.replace(old_text, new_text, 1)
            replacements = 1

        # Generate unified diff
        diff = self._generate_diff(file_path, old_content, new_content)

        # Write file
        try:
            file_path.write_text(new_content, encoding="utf-8")
        except PermissionError:
            raise PermissionError(f"Permission denied writing file: {file_path}")
        except OSError as e:
            raise OSError(f"Failed to write file {file_path}: {e}")

        return EditResult(
            success=True,
            path=file_path,
            replacements=replacements,
            diff=diff,
            old_content=old_content,
            new_content=new_content,
        )

    def _generate_diff(self, file_path: Path, old_content: str, new_content: str) -> str:
        """
        Generate unified diff between old and new content

        Args:
            file_path: Path to the file (for diff header)
            old_content: Original file content
            new_content: Modified file content

        Returns:
            Unified diff string
        """
        old_lines = old_content.splitlines(keepends=True)
        new_lines = new_content.splitlines(keepends=True)

        diff = difflib.unified_diff(
            old_lines,
            new_lines,
            fromfile=f"a/{file_path.name}",
            tofile=f"b/{file_path.name}",
            lineterm="",
        )

        return "".join(diff)
