"""Coding tools for file operations and command execution."""

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

from emdash.agent.tools.base import BaseTool, ToolResult, ToolCategory


class CodingTool(BaseTool):
    """Base class for coding tools that require repo_root."""

    category = ToolCategory.PLANNING  # Use planning category for coding tools

    def __init__(self, repo_root: Path, connection=None):
        """Initialize with repository root for path validation.

        Args:
            repo_root: Root directory of the repository
            connection: Kuzu connection (optional, not used by coding tools)
        """
        super().__init__(connection)
        self.repo_root = repo_root.resolve()

    def _validate_path(self, path: str) -> tuple[bool, Path, Optional[str]]:
        """Validate that path is within repo_root.

        Args:
            path: Relative or absolute path

        Returns:
            Tuple of (is_valid, resolved_path, error_message)
        """
        try:
            # Handle relative paths
            if not os.path.isabs(path):
                full_path = (self.repo_root / path).resolve()
            else:
                full_path = Path(path).resolve()

            # Check if path is within repo_root
            try:
                full_path.relative_to(self.repo_root)
                return True, full_path, None
            except ValueError:
                return False, full_path, f"Path '{path}' is outside repository root"

        except Exception as e:
            return False, Path(path), f"Invalid path: {e}"


class ReadFileTool(CodingTool):
    """Read file contents with optional line range."""

    name = "read_file"
    description = "Read the contents of a file. Supports optional line range for large files."

    def execute(
        self,
        path: str,
        start_line: Optional[int] = None,
        end_line: Optional[int] = None,
    ) -> ToolResult:
        """Read file contents.

        Args:
            path: Path to file (relative to repo root)
            start_line: Optional start line (1-indexed)
            end_line: Optional end line (1-indexed, inclusive)

        Returns:
            ToolResult with file contents
        """
        valid, full_path, error = self._validate_path(path)
        if not valid:
            return ToolResult.error_result(error)

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

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

        try:
            with open(full_path, "r", encoding="utf-8", errors="replace") as f:
                lines = f.readlines()

            total_lines = len(lines)

            # Apply line range if specified
            if start_line is not None or end_line is not None:
                start = (start_line or 1) - 1  # Convert to 0-indexed
                end = end_line or total_lines
                start = max(0, start)
                end = min(total_lines, end)
                lines = lines[start:end]
                line_offset = start
            else:
                line_offset = 0

            # Format with line numbers
            numbered_lines = []
            for i, line in enumerate(lines):
                line_num = i + line_offset + 1
                numbered_lines.append(f"{line_num:4d} | {line.rstrip()}")

            content = "\n".join(numbered_lines)

            return ToolResult.success_result({
                "path": path,
                "content": content,
                "total_lines": total_lines,
                "lines_shown": len(lines),
                "start_line": line_offset + 1,
                "end_line": line_offset + len(lines),
            })

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

    def get_schema(self) -> dict:
        return self._make_schema(
            properties={
                "path": {
                    "type": "string",
                    "description": "Path to the file (relative to repository root)",
                },
                "start_line": {
                    "type": "integer",
                    "description": "Start line number (1-indexed, optional)",
                },
                "end_line": {
                    "type": "integer",
                    "description": "End line number (1-indexed, inclusive, optional)",
                },
            },
            required=["path"],
        )


class WriteToFileTool(CodingTool):
    """Write/create file with complete content."""

    name = "write_to_file"
    description = "Write content to a file. Creates the file and parent directories if they don't exist. Overwrites existing content."

    def execute(self, path: str, content: str) -> ToolResult:
        """Write content to file.

        Args:
            path: Path to file (relative to repo root)
            content: Content to write

        Returns:
            ToolResult with success status
        """
        valid, full_path, error = self._validate_path(path)
        if not valid:
            return ToolResult.error_result(error)

        try:
            # Create parent directories if needed
            full_path.parent.mkdir(parents=True, exist_ok=True)

            # Check if file exists for reporting
            existed = full_path.exists()

            # Write content
            with open(full_path, "w", encoding="utf-8") as f:
                f.write(content)

            lines_written = content.count("\n") + (1 if content and not content.endswith("\n") else 0)

            return ToolResult.success_result({
                "path": path,
                "created": not existed,
                "lines_written": lines_written,
                "bytes_written": len(content.encode("utf-8")),
            })

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

    def get_schema(self) -> dict:
        return self._make_schema(
            properties={
                "path": {
                    "type": "string",
                    "description": "Path to the file (relative to repository root)",
                },
                "content": {
                    "type": "string",
                    "description": "Content to write to the file",
                },
            },
            required=["path", "content"],
        )


class ApplyDiffTool(CodingTool):
    """Apply search/replace blocks using fuzzy matching."""

    name = "apply_diff"
    description = """Apply search/replace blocks to modify a file. Uses fuzzy matching to handle minor whitespace differences.

Format:
<<<<<<< SEARCH
original code to find
=======
replacement code
>>>>>>> REPLACE

Multiple blocks can be applied in sequence."""

    # Minimum similarity ratio for fuzzy matching
    MIN_SIMILARITY = 0.6

    def execute(self, path: str, diff: str) -> ToolResult:
        """Apply diff to file.

        Args:
            path: Path to file (relative to repo root)
            diff: Diff in search/replace format

        Returns:
            ToolResult with applied changes
        """
        valid, full_path, error = self._validate_path(path)
        if not valid:
            return ToolResult.error_result(error)

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

        try:
            with open(full_path, "r", encoding="utf-8") as f:
                content = f.read()

            # Parse diff blocks
            blocks = self._parse_diff_blocks(diff)
            if not blocks:
                return ToolResult.error_result(
                    "No valid SEARCH/REPLACE blocks found. Use format:\n"
                    "<<<<<<< SEARCH\noriginal\n=======\nreplacement\n>>>>>>> REPLACE"
                )

            # Apply each block
            applied = []
            failed = []
            for i, (search, replace) in enumerate(blocks):
                success, content, match_info = self._apply_block(content, search, replace)
                if success:
                    applied.append({
                        "block": i + 1,
                        "match_ratio": match_info.get("ratio", 1.0),
                        "match_type": match_info.get("type", "exact"),
                    })
                else:
                    failed.append({
                        "block": i + 1,
                        "search_preview": search[:100] + "..." if len(search) > 100 else search,
                        "error": match_info.get("error", "No match found"),
                    })

            if failed and not applied:
                return ToolResult.error_result(
                    f"Failed to apply any blocks. First failure: {failed[0]['error']}",
                    suggestions=["Ensure SEARCH text matches file content", "Use read_file to check current content"],
                )

            # Write modified content
            with open(full_path, "w", encoding="utf-8") as f:
                f.write(content)

            return ToolResult.success_result({
                "path": path,
                "blocks_applied": len(applied),
                "blocks_failed": len(failed),
                "applied": applied,
                "failed": failed if failed else None,
            })

        except Exception as e:
            return ToolResult.error_result(f"Failed to apply diff: {e}")

    def _parse_diff_blocks(self, diff: str) -> list[tuple[str, str]]:
        """Parse diff into search/replace block pairs.

        Args:
            diff: Raw diff text

        Returns:
            List of (search, replace) tuples
        """
        blocks = []
        # Match blocks with various marker styles
        pattern = r"<<<<<<<?[ ]*SEARCH[ ]*\n(.*?)\n=======[ ]*\n(.*?)\n>>>>>>>?[ ]*REPLACE[ ]*"
        matches = re.findall(pattern, diff, re.DOTALL)

        for search, replace in matches:
            blocks.append((search, replace))

        return blocks

    def _apply_block(
        self, content: str, search: str, replace: str
    ) -> tuple[bool, str, dict]:
        """Apply a single search/replace block.

        Args:
            content: File content
            search: Text to search for
            replace: Replacement text

        Returns:
            Tuple of (success, new_content, match_info)
        """
        # Try exact match first
        if search in content:
            return True, content.replace(search, replace, 1), {"type": "exact", "ratio": 1.0}

        # Try fuzzy matching
        content_lines = content.split("\n")
        search_lines = search.split("\n")

        best_ratio = 0.0
        best_start = -1
        best_end = -1

        # Sliding window search
        for i in range(len(content_lines) - len(search_lines) + 1):
            window = "\n".join(content_lines[i : i + len(search_lines)])
            ratio = difflib.SequenceMatcher(None, search, window).ratio()

            if ratio > best_ratio:
                best_ratio = ratio
                best_start = i
                best_end = i + len(search_lines)

        if best_ratio >= self.MIN_SIMILARITY:
            # Apply replacement
            new_lines = (
                content_lines[:best_start]
                + replace.split("\n")
                + content_lines[best_end:]
            )
            return True, "\n".join(new_lines), {"type": "fuzzy", "ratio": best_ratio}

        return False, content, {"error": f"No match found (best ratio: {best_ratio:.2f})"}

    def get_schema(self) -> dict:
        return self._make_schema(
            properties={
                "path": {
                    "type": "string",
                    "description": "Path to the file (relative to repository root)",
                },
                "diff": {
                    "type": "string",
                    "description": "Diff in SEARCH/REPLACE format. Multiple blocks supported.",
                },
            },
            required=["path", "diff"],
        )


class DeleteFileTool(CodingTool):
    """Delete a file from the repository."""

    name = "delete_file"
    description = "Delete a file from the repository."

    def execute(self, path: str) -> ToolResult:
        """Delete file.

        Args:
            path: Path to file (relative to repo root)

        Returns:
            ToolResult with success status
        """
        valid, full_path, error = self._validate_path(path)
        if not valid:
            return ToolResult.error_result(error)

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

        if not full_path.is_file():
            return ToolResult.error_result(f"Not a file (use rmdir for directories): {path}")

        try:
            full_path.unlink()
            return ToolResult.success_result({
                "path": path,
                "deleted": True,
            })

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

    def get_schema(self) -> dict:
        return self._make_schema(
            properties={
                "path": {
                    "type": "string",
                    "description": "Path to the file to delete (relative to repository root)",
                },
            },
            required=["path"],
        )


class ListFilesTool(CodingTool):
    """List files in directory with optional pattern matching."""

    name = "list_files"
    description = "List files and directories. Supports glob patterns for filtering."

    def execute(
        self,
        path: str = ".",
        pattern: Optional[str] = None,
        max_depth: int = 3,
    ) -> ToolResult:
        """List files in directory.

        Args:
            path: Directory path (relative to repo root)
            pattern: Optional glob pattern (e.g., "*.py")
            max_depth: Maximum depth to traverse (default 3)

        Returns:
            ToolResult with file listing
        """
        valid, full_path, error = self._validate_path(path)
        if not valid:
            return ToolResult.error_result(error)

        if not full_path.exists():
            return ToolResult.error_result(f"Directory not found: {path}")

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

        try:
            entries = []
            self._list_recursive(full_path, entries, pattern, max_depth, 0)

            # Build tree structure
            tree_lines = []
            for entry in entries:
                rel_path = entry["path"]
                indent = "  " * entry["depth"]
                icon = "/" if entry["is_dir"] else ""
                tree_lines.append(f"{indent}{entry['name']}{icon}")

            return ToolResult.success_result({
                "path": path,
                "pattern": pattern,
                "entries": entries,
                "tree": "\n".join(tree_lines),
                "total_files": sum(1 for e in entries if not e["is_dir"]),
                "total_dirs": sum(1 for e in entries if e["is_dir"]),
            })

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

    def _list_recursive(
        self,
        dir_path: Path,
        entries: list,
        pattern: Optional[str],
        max_depth: int,
        current_depth: int,
    ):
        """Recursively list directory contents."""
        if current_depth > max_depth:
            return

        try:
            items = sorted(dir_path.iterdir(), key=lambda p: (not p.is_dir(), p.name.lower()))
        except PermissionError:
            return

        for item in items:
            # Skip hidden files and common ignore patterns
            if item.name.startswith(".") or item.name in ("__pycache__", "node_modules", ".git"):
                continue

            rel_path = str(item.relative_to(self.repo_root))
            is_dir = item.is_dir()

            # Apply pattern filter
            if pattern and not is_dir:
                if not fnmatch.fnmatch(item.name, pattern):
                    continue

            entries.append({
                "name": item.name,
                "path": rel_path,
                "is_dir": is_dir,
                "depth": current_depth,
            })

            if is_dir:
                self._list_recursive(item, entries, pattern, max_depth, current_depth + 1)

    def get_schema(self) -> dict:
        return self._make_schema(
            properties={
                "path": {
                    "type": "string",
                    "description": "Directory path (relative to repository root, default '.')",
                },
                "pattern": {
                    "type": "string",
                    "description": "Glob pattern to filter files (e.g., '*.py', '*.ts')",
                },
                "max_depth": {
                    "type": "integer",
                    "description": "Maximum depth to traverse (default 3)",
                },
            },
            required=[],
        )


class ExecuteCommandTool(CodingTool):
    """Execute shell command in repository root."""

    name = "execute_command"
    description = "Execute a shell command in the repository root directory."

    # Default timeout in seconds
    DEFAULT_TIMEOUT = 30

    # Maximum output size in characters
    MAX_OUTPUT = 50000

    def execute(
        self,
        command: str,
        timeout: Optional[int] = None,
    ) -> ToolResult:
        """Execute shell command.

        Args:
            command: Shell command to execute
            timeout: Timeout in seconds (default 30)

        Returns:
            ToolResult with command output
        """
        if not command or not command.strip():
            return ToolResult.error_result("Empty command")

        timeout = timeout or self.DEFAULT_TIMEOUT

        try:
            result = subprocess.run(
                command,
                shell=True,
                cwd=str(self.repo_root),
                capture_output=True,
                text=True,
                timeout=timeout,
            )

            stdout = result.stdout
            stderr = result.stderr

            # Truncate if needed
            if len(stdout) > self.MAX_OUTPUT:
                stdout = stdout[: self.MAX_OUTPUT] + "\n... [truncated]"
            if len(stderr) > self.MAX_OUTPUT:
                stderr = stderr[: self.MAX_OUTPUT] + "\n... [truncated]"

            return ToolResult.success_result({
                "command": command,
                "exit_code": result.returncode,
                "stdout": stdout,
                "stderr": stderr,
                "success": result.returncode == 0,
            })

        except subprocess.TimeoutExpired:
            return ToolResult.error_result(
                f"Command timed out after {timeout} seconds",
                suggestions=["Increase timeout or simplify command"],
            )
        except Exception as e:
            return ToolResult.error_result(f"Command execution failed: {e}")

    def get_schema(self) -> dict:
        return self._make_schema(
            properties={
                "command": {
                    "type": "string",
                    "description": "Shell command to execute",
                },
                "timeout": {
                    "type": "integer",
                    "description": "Timeout in seconds (default 30)",
                },
            },
            required=["command"],
        )
