"""
File Editor Module

File operation tools with diff support:
- Read files
- Write files with confirmation
- Patch files with targeted edits
- Delete files with confirmation
- List directory contents
- Search file contents
"""

import difflib
import fnmatch
import re
from pathlib import Path
from typing import Optional

from rich.console import Console
from rich.syntax import Syntax

from ai_coder.tools.base import Tool, ToolResult
from ai_coder.safety.guards import get_safety_guard

console = Console()


class ReadFileTool(Tool):
    """Read the contents of a file."""

    @property
    def name(self) -> str:
        return "read_file"

    @property
    def description(self) -> str:
        return "Read the contents of a file at the given path"

    def execute(self, path: str, **kwargs) -> ToolResult:
        """Read file contents."""
        try:
            file_path = self.resolve_path(path)

            # Validate path
            valid, error = self.validate_path(file_path)
            if not valid:
                return ToolResult.error(error)

            # Check file size
            guard = get_safety_guard()
            valid, error = guard.validate_file_size(file_path)
            if not valid:
                return ToolResult.error(error)

            if not file_path.exists():
                return ToolResult.error(f"File not found: {path}")

            if not file_path.is_file():
                return ToolResult.error(f"Not a file: {path}")

            content = file_path.read_text(encoding="utf-8", errors="replace")
            return ToolResult.success(content, data={"path": str(file_path), "size": len(content)})

        except Exception as e:
            return ToolResult.error(f"Failed to read file: {e}")


class WriteFileTool(Tool):
    """Write content to a file with diff preview and confirmation."""

    @property
    def name(self) -> str:
        return "write_file"

    @property
    def description(self) -> str:
        return "Write content to a file. Creates the file if it doesn't exist."

    def execute(self, path: str, content: str, **kwargs) -> ToolResult:
        """Write content to file."""
        try:
            file_path = self.resolve_path(path)

            # Validate path
            valid, error = self.validate_path(file_path)
            if not valid:
                return ToolResult.error(error)

            guard = get_safety_guard()

            # Show diff if file exists
            diff_text = None
            if file_path.exists():
                original = file_path.read_text(encoding="utf-8", errors="replace")
                diff_text = self._generate_diff(original, content, path)

                if diff_text:
                    console.print("\n[bold cyan]═══ File Changes ═══[/bold cyan]")
                    console.print(diff_text)
            else:
                # New file
                console.print(f"\n[bold green]Creating new file: {path}[/bold green]")
                # Show preview of new content
                lines = content.split("\n")
                preview = "\n".join(lines[:20])
                if len(lines) > 20:
                    preview += f"\n... ({len(lines) - 20} more lines)"
                console.print(Syntax(preview, self._get_language(path), line_numbers=True))

            # Request confirmation
            if not guard.require_write_confirmation(path, diff_text):
                return ToolResult.error("Write cancelled by user")

            # Create parent directories if needed
            file_path.parent.mkdir(parents=True, exist_ok=True)

            # Write the file
            file_path.write_text(content, encoding="utf-8")

            return ToolResult.success(f"Successfully wrote {len(content)} bytes to {path}")

        except Exception as e:
            return ToolResult.error(f"Failed to write file: {e}")

    def _generate_diff(self, original: str, modified: str, filename: str) -> str:
        """Generate a unified diff between original and modified content."""
        original_lines = original.splitlines(keepends=True)
        modified_lines = modified.splitlines(keepends=True)

        diff = difflib.unified_diff(
            original_lines,
            modified_lines,
            fromfile=f"a/{filename}",
            tofile=f"b/{filename}",
            lineterm="",
        )

        diff_text = "".join(diff)
        return diff_text if diff_text.strip() else None

    def _get_language(self, filename: str) -> str:
        """Get syntax highlighting language from filename."""
        ext_map = {
            ".py": "python",
            ".js": "javascript",
            ".ts": "typescript",
            ".jsx": "jsx",
            ".tsx": "tsx",
            ".json": "json",
            ".yaml": "yaml",
            ".yml": "yaml",
            ".md": "markdown",
            ".html": "html",
            ".css": "css",
            ".sh": "bash",
            ".sql": "sql",
        }
        ext = Path(filename).suffix.lower()
        return ext_map.get(ext, "text")


class PatchFileTool(Tool):
    """Apply targeted edits to a file using search/replace."""

    @property
    def name(self) -> str:
        return "patch_file"

    @property
    def description(self) -> str:
        return "Apply targeted edits to a file using search/replace blocks"

    def execute(self, path: str, patches: list[dict], **kwargs) -> ToolResult:
        """Apply patches to file."""
        try:
            file_path = self.resolve_path(path)

            # Validate path
            valid, error = self.validate_path(file_path)
            if not valid:
                return ToolResult.error(error)

            if not file_path.exists():
                return ToolResult.error(f"File not found: {path}")

            content = file_path.read_text(encoding="utf-8", errors="replace")
            original = content

            # Apply each patch
            applied = 0
            failed = []

            for i, patch in enumerate(patches):
                search = patch.get("search", "")
                replace = patch.get("replace", "")

                if not search:
                    failed.append(f"Patch {i + 1}: empty search string")
                    continue

                if search in content:
                    content = content.replace(search, replace, 1)
                    applied += 1
                else:
                    # Try fuzzy matching
                    fuzzy_match = self._fuzzy_find(content, search)
                    if fuzzy_match:
                        content = content.replace(fuzzy_match, replace, 1)
                        applied += 1
                    else:
                        failed.append(f"Patch {i + 1}: search text not found")

            if applied == 0:
                return ToolResult.error(f"No patches applied. Issues: {'; '.join(failed)}")

            # Show diff
            diff_text = self._generate_diff(original, content, path)
            if diff_text:
                console.print("\n[bold cyan]═══ Patch Changes ═══[/bold cyan]")
                console.print(diff_text)

            # Request confirmation
            guard = get_safety_guard()
            if not guard.require_write_confirmation(path, diff_text):
                return ToolResult.error("Patch cancelled by user")

            # Write the patched content
            file_path.write_text(content, encoding="utf-8")

            result_msg = f"Applied {applied}/{len(patches)} patches to {path}"
            if failed:
                result_msg += f". Failed: {'; '.join(failed)}"

            return ToolResult.success(result_msg)

        except Exception as e:
            return ToolResult.error(f"Failed to patch file: {e}")

    def _fuzzy_find(self, content: str, search: str, threshold: float = 0.8) -> Optional[str]:
        """Try to find a fuzzy match for the search string."""
        search_lines = search.strip().split("\n")
        content_lines = content.split("\n")

        if len(search_lines) == 1:
            # Single line fuzzy match
            for line in content_lines:
                ratio = difflib.SequenceMatcher(None, search.strip(), line.strip()).ratio()
                if ratio >= threshold:
                    return line
        else:
            # Multi-line fuzzy match
            for i in range(len(content_lines) - len(search_lines) + 1):
                block = "\n".join(content_lines[i : i + len(search_lines)])
                ratio = difflib.SequenceMatcher(None, search.strip(), block.strip()).ratio()
                if ratio >= threshold:
                    return block

        return None

    def _generate_diff(self, original: str, modified: str, filename: str) -> str:
        """Generate unified diff."""
        original_lines = original.splitlines(keepends=True)
        modified_lines = modified.splitlines(keepends=True)

        diff = difflib.unified_diff(
            original_lines,
            modified_lines,
            fromfile=f"a/{filename}",
            tofile=f"b/{filename}",
        )

        return "".join(diff)


class DeleteFileTool(Tool):
    """Delete a file with confirmation."""

    @property
    def name(self) -> str:
        return "delete_file"

    @property
    def description(self) -> str:
        return "Delete a file at the given path"

    def execute(self, path: str, **kwargs) -> ToolResult:
        """Delete a file."""
        try:
            file_path = self.resolve_path(path)

            # Validate path
            valid, error = self.validate_path(file_path)
            if not valid:
                return ToolResult.error(error)

            if not file_path.exists():
                return ToolResult.error(f"File not found: {path}")

            if not file_path.is_file():
                return ToolResult.error(f"Not a file (cannot delete directories): {path}")

            # Request confirmation
            guard = get_safety_guard()
            if not guard.require_delete_confirmation(path):
                return ToolResult.error("Delete cancelled by user")

            file_path.unlink()
            return ToolResult.success(f"Deleted file: {path}")

        except Exception as e:
            return ToolResult.error(f"Failed to delete file: {e}")


class ListFilesTool(Tool):
    """List files in a directory."""

    @property
    def name(self) -> str:
        return "list_files"

    @property
    def description(self) -> str:
        return "List files in a directory"

    def execute(self, path: str = "", **kwargs) -> ToolResult:
        """List directory contents."""
        try:
            if path:
                dir_path = self.resolve_path(path)
            else:
                dir_path = self.project_root

            if dir_path is None:
                return ToolResult.error("Project root not set")

            # Validate path
            valid, error = self.validate_path(dir_path)
            if not valid:
                return ToolResult.error(error)

            if not dir_path.exists():
                return ToolResult.error(f"Directory not found: {path or '.'}")

            if not dir_path.is_dir():
                return ToolResult.error(f"Not a directory: {path}")

            # List contents
            entries = []
            for item in sorted(dir_path.iterdir()):
                if item.is_dir():
                    entries.append(f"[D] {item.name}/")
                else:
                    size = item.stat().st_size
                    entries.append(f"[F] {item.name} ({self._format_size(size)})")

            if not entries:
                return ToolResult.success("(empty directory)")

            return ToolResult.success("\n".join(entries))

        except Exception as e:
            return ToolResult.error(f"Failed to list directory: {e}")

    def _format_size(self, size: int) -> str:
        """Format file size for display."""
        if size < 1024:
            return f"{size} B"
        elif size < 1024 * 1024:
            return f"{size / 1024:.1f} KB"
        else:
            return f"{size / 1024 / 1024:.1f} MB"


class SearchFilesTool(Tool):
    """Search for text patterns across project files."""

    @property
    def name(self) -> str:
        return "search_files"

    @property
    def description(self) -> str:
        return "Search for text patterns across project files"

    def execute(self, pattern: str, file_pattern: str = "*", **kwargs) -> ToolResult:
        """Search for pattern in files."""
        try:
            if self.project_root is None:
                return ToolResult.error("Project root not set")

            # Compile pattern
            try:
                regex = re.compile(pattern, re.IGNORECASE)
            except re.error:
                # Treat as literal string
                regex = re.compile(re.escape(pattern), re.IGNORECASE)

            results = []
            files_searched = 0
            max_results = 50

            for file_path in self.project_root.rglob("*"):
                if not file_path.is_file():
                    continue

                # Check file pattern
                if not fnmatch.fnmatch(file_path.name, file_pattern):
                    continue

                # Skip binary and large files
                try:
                    if file_path.stat().st_size > 100_000:  # 100KB
                        continue
                except Exception:
                    continue

                try:
                    content = file_path.read_text(encoding="utf-8", errors="ignore")
                    files_searched += 1
                except Exception:
                    continue

                # Search for matches
                for i, line in enumerate(content.split("\n"), 1):
                    if regex.search(line):
                        rel_path = file_path.relative_to(self.project_root)
                        results.append(f"{rel_path}:{i}: {line.strip()[:100]}")

                        if len(results) >= max_results:
                            break

                if len(results) >= max_results:
                    break

            if not results:
                return ToolResult.success(f"No matches found for '{pattern}' in {files_searched} files")

            header = f"Found {len(results)} matches in {files_searched} files:\n"
            return ToolResult.success(header + "\n".join(results))

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