import difflib
import fnmatch
import os
from pathlib import Path

import git

from mcp_server_code_assist.base_tools import BaseTools


class FileTools(BaseTools):
    def is_valid_operation(self, path: Path) -> bool:
        """Validate if operation can be performed on path"""
        return path.exists() and path.is_file()

    async def validate_path(self, path: str) -> Path:
        abs_path = os.path.abspath(path)
        if not any(abs_path.startswith(p) for p in self.allowed_paths):
            raise ValueError(f"Path {path} is outside allowed directories")
        return Path(abs_path)

    async def read_file(self, path: str) -> str:
        path = await self.validate_path(path)
        try:
            return path.read_text()
        except Exception as e:
            self.handle_error(e, {"operation": "read", "path": str(path)})

    async def write_file(self, path: str, content: str) -> None:
        path = await self.validate_path(path)
        try:
            path.parent.mkdir(parents=True, exist_ok=True)
            path.write_text(content)
        except Exception as e:
            self.handle_error(e, {"operation": "write", "path": str(path)})

    async def create_file(self, path: str, content: str = "") -> str:
        await self.write_file(path, content)
        return f"Created file: {path}"

    async def delete_file(self, path: str) -> str:
        path = await self.validate_path(path)
        if not path.is_file():
            return f"Path not found: {path}"

        # Create trash directory
        trash_dir = path.parent / ".mcp_server_code_assist_trash"
        trash_dir.mkdir(exist_ok=True)

        # Move file to trash with timestamp to avoid conflicts
        from datetime import datetime

        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        trash_path = trash_dir / f"{path.name}_{timestamp}"
        path.rename(trash_path)

        return f"Moved file to trash: {trash_path}"

    async def modify_file(self, path: str, replacements: dict[str, str]) -> str:
        path = await self.validate_path(path)
        content = await self.read_file(path)
        original = content

        for old, new in replacements.items():
            content = content.replace(old, new)

        await self.write_file(path, content)
        return self.generate_diff(original, content)

    async def rewrite_file(self, path: str, content: str) -> str:
        path = await self.validate_path(path)
        original = await self.read_file(path) if path.exists() else ""
        await self.write_file(path, content)
        return self.generate_diff(original, content)

    @staticmethod
    def generate_diff(original: str, modified: str) -> str:
        diff = difflib.unified_diff(original.splitlines(keepends=True), modified.splitlines(keepends=True), fromfile="original", tofile="modified")
        return "".join(diff)

    async def file_tree(self, path: str) -> tuple[str, int, int]:
        """Generate tree view of directory structure.

        Args:
            path: Root directory path

        Returns:
            Tree view as string
        """
        path = await self.validate_path(path)
        base_path = path

        # Try git tracking first
        tracked_files = self._get_tracked_files(path)
        gitignore = self._load_gitignore(path) if tracked_files is None else []

        def gen_tree(path: Path, prefix: str = "") -> tuple[list[str], int, int]:
            entries = []
            dir_count = 0
            file_count = 0

            items = sorted(path.iterdir(), key=lambda x: (x.is_file(), x.name))
            for i, item in enumerate(items):
                rel_path = str(item.relative_to(base_path))

                # Skip if file should be ignored
                if tracked_files is not None:
                    if rel_path not in tracked_files and not any(str(p.relative_to(base_path)) in tracked_files for p in item.rglob("*") if p.is_file()):
                        continue
                else:
                    # Use gitignore
                    if self._should_ignore(rel_path, gitignore):
                        continue

                is_last = i == len(items) - 1
                curr_prefix = "└── " if is_last else "├── "
                curr_line = prefix + curr_prefix + item.name

                if item.is_dir():
                    next_prefix = prefix + ("    " if is_last else "│   ")
                    subtree, sub_dirs, sub_files = gen_tree(item, next_prefix)
                    if tracked_files is not None and not subtree:
                        continue
                    entries.extend([curr_line] + subtree)
                    dir_count += 1 + sub_dirs
                    file_count += sub_files
                else:
                    if tracked_files is not None and rel_path not in tracked_files:
                        continue
                    entries.append(curr_line)
                    file_count += 1

            return entries, dir_count, file_count

        tree_lines, _, _ = gen_tree(path)
        return "\n".join(tree_lines)

    def _should_ignore(self, path: str, patterns: list[str]) -> bool:
        """Check if path matches gitignore patterns.

        Args:
            path: Path to check
            patterns: List of gitignore patterns

        Returns:
            True if path should be ignored
        """
        if not patterns:
            return False

        parts = Path(path).parts
        for pattern in patterns:
            pattern = pattern.strip()
            if not pattern or pattern.startswith("#"):
                continue

            if pattern.endswith("/"):
                pattern = pattern.rstrip("/")
                if pattern in parts:
                    return True
            else:
                if fnmatch.fnmatch(parts[-1], pattern):  # Match basename
                    return True
                # Match full path
                if fnmatch.fnmatch(path, pattern):
                    return True

        return False

    def _load_gitignore(self, path: str) -> list[str]:
        """Load gitignore patterns from a directory.

        Args:
            path: Directory containing .gitignore

        Returns:
            List of gitignore patterns
        """
        gitignore_path = os.path.join(path, ".gitignore")
        patterns = []
        if os.path.exists(gitignore_path):
            with open(gitignore_path) as f:
                for line in f:
                    line = line.strip()
                    if line and not line.startswith("#"):
                        patterns.append(line)
        return patterns

    def _get_tracked_files(self, repo_path: str) -> set[str] | None:
        """Get set of tracked files in a git repository.

        Args:
            repo_path: Path to git repository

        Returns:
            Set of tracked file paths or None if not a git repo
        """
        try:
            repo = git.Repo(repo_path)
            return set(repo.git.ls_files().splitlines())
        except git.exc.InvalidGitRepositoryError:
            return None
