"""File manipulation tools for Promethian.

Provides structured file operations (Read, Write, Edit, Glob, Grep) that give
the LLM better context than bash commands.

These are built-in tools that are always available to the agent.
"""

from __future__ import annotations

import fnmatch
import os
import re
import subprocess
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Literal


# ============================================================================
# Result Types
# ============================================================================

@dataclass
class ReadResult:
    """Result from reading a file."""
    success: bool
    content: str  # Content with line numbers
    file_path: str
    total_lines: int
    lines_read: int
    offset: int
    error: str | None = None


@dataclass
class WriteResult:
    """Result from writing a file."""
    success: bool
    file_path: str
    bytes_written: int
    created: bool  # True if file was created, False if overwritten
    error: str | None = None


@dataclass
class EditResult:
    """Result from editing a file."""
    success: bool
    file_path: str
    replacements_made: int
    error: str | None = None


@dataclass
class GlobResult:
    """Result from glob pattern matching."""
    success: bool
    files: list[str]  # List of matching file paths
    pattern: str
    search_path: str
    error: str | None = None


@dataclass
class GrepResult:
    """Result from searching file contents."""
    success: bool
    matches: list[GrepMatch]
    pattern: str
    files_searched: int
    error: str | None = None


@dataclass
class GrepMatch:
    """A single grep match."""
    file: str
    line_number: int
    content: str
    context_before: list[str] | None = None
    context_after: list[str] | None = None


# ============================================================================
# Read Tool
# ============================================================================

class ReadTool:
    """
    Read file contents with line numbers.

    Features:
    - Line number prefix (cat -n style)
    - Offset/limit for large files
    - Truncates long lines
    - Handles binary files gracefully
    """

    MAX_LINE_LENGTH = 2000
    DEFAULT_LIMIT = 2000

    def __init__(self, working_dir: str | Path | None = None):
        self.working_dir = Path(working_dir) if working_dir else Path.cwd()

    def _resolve_path(self, file_path: str) -> Path:
        """Resolve file path, handling relative paths."""
        path = Path(file_path)
        if not path.is_absolute():
            path = self.working_dir / path
        return path.resolve()

    def _format_line_number(self, line_num: int, max_num: int) -> str:
        """Format line number with padding."""
        width = len(str(max_num))
        return f"{line_num:>{width}}\t"

    async def read(
        self,
        file_path: str,
        offset: int = 0,
        limit: int | None = None,
    ) -> ReadResult:
        """
        Read a file with line numbers.

        Args:
            file_path: Path to the file (absolute or relative to working_dir)
            offset: Line number to start from (0-based)
            limit: Maximum number of lines to read (default 2000)

        Returns:
            ReadResult with numbered content
        """
        limit = limit or self.DEFAULT_LIMIT
        path = self._resolve_path(file_path)

        if not path.exists():
            return ReadResult(
                success=False,
                content="",
                file_path=str(path),
                total_lines=0,
                lines_read=0,
                offset=offset,
                error=f"File not found: {path}"
            )

        if path.is_dir():
            return ReadResult(
                success=False,
                content="",
                file_path=str(path),
                total_lines=0,
                lines_read=0,
                offset=offset,
                error=f"Path is a directory, not a file: {path}"
            )

        try:
            # Read entire file to count lines
            with open(path, 'r', encoding='utf-8', errors='replace') as f:
                all_lines = f.readlines()

            total_lines = len(all_lines)

            # Slice to requested range
            end_idx = min(offset + limit, total_lines)
            lines = all_lines[offset:end_idx]

            # Format with line numbers
            output_lines = []
            max_line_num = end_idx

            for i, line in enumerate(lines):
                line_num = offset + i + 1  # 1-based line numbers
                prefix = self._format_line_number(line_num, max_line_num)

                # Truncate long lines
                line = line.rstrip('\n\r')
                if len(line) > self.MAX_LINE_LENGTH:
                    line = line[:self.MAX_LINE_LENGTH] + "..."

                output_lines.append(f"{prefix}{line}")

            content = '\n'.join(output_lines)

            return ReadResult(
                success=True,
                content=content,
                file_path=str(path),
                total_lines=total_lines,
                lines_read=len(lines),
                offset=offset,
            )

        except Exception as e:
            return ReadResult(
                success=False,
                content="",
                file_path=str(path),
                total_lines=0,
                lines_read=0,
                offset=offset,
                error=str(e)
            )

    def get_tool_schemas(self) -> list[dict]:
        """Get Claude tool schema for read."""
        return [{
            "name": "read",
            "description": (
                "Read a file's contents with line numbers. Use this to examine code, "
                "config files, or any text content. Always read a file before editing it. "
                "Supports offset/limit for large files."
            ),
            "input_schema": {
                "type": "object",
                "properties": {
                    "file_path": {
                        "type": "string",
                        "description": "Path to the file (absolute or relative to working directory)"
                    },
                    "offset": {
                        "type": "integer",
                        "description": "Line number to start from (0-based, default 0)"
                    },
                    "limit": {
                        "type": "integer",
                        "description": "Maximum lines to read (default 2000)"
                    }
                },
                "required": ["file_path"]
            }
        }]

    async def execute_tool(self, name: str, params: dict) -> ReadResult:
        """Execute the read tool."""
        if name == "read":
            return await self.read(
                file_path=params["file_path"],
                offset=params.get("offset", 0),
                limit=params.get("limit"),
            )
        return ReadResult(
            success=False,
            content="",
            file_path="",
            total_lines=0,
            lines_read=0,
            offset=0,
            error=f"Unknown read tool: {name}"
        )


# ============================================================================
# Write Tool
# ============================================================================

class WriteTool:
    """
    Write content to a file.

    Features:
    - Creates parent directories if needed
    - Tracks if file was created vs overwritten
    """

    def __init__(self, working_dir: str | Path | None = None):
        self.working_dir = Path(working_dir) if working_dir else Path.cwd()

    def _resolve_path(self, file_path: str) -> Path:
        """Resolve file path, handling relative paths."""
        path = Path(file_path)
        if not path.is_absolute():
            path = self.working_dir / path
        return path.resolve()

    async def write(
        self,
        file_path: str,
        content: str,
    ) -> WriteResult:
        """
        Write content to a file.

        Args:
            file_path: Path to the file
            content: Content to write

        Returns:
            WriteResult with status
        """
        path = self._resolve_path(file_path)
        created = not path.exists()

        try:
            # Create parent directories
            path.parent.mkdir(parents=True, exist_ok=True)

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

            return WriteResult(
                success=True,
                file_path=str(path),
                bytes_written=len(content.encode('utf-8')),
                created=created,
            )

        except Exception as e:
            return WriteResult(
                success=False,
                file_path=str(path),
                bytes_written=0,
                created=False,
                error=str(e)
            )

    def get_tool_schemas(self) -> list[dict]:
        """Get Claude tool schema for write."""
        return [{
            "name": "write",
            "description": (
                "Write content to a file, creating it if it doesn't exist. "
                "This overwrites the entire file. For small changes to existing files, "
                "prefer the edit tool instead. Creates parent directories automatically."
            ),
            "input_schema": {
                "type": "object",
                "properties": {
                    "file_path": {
                        "type": "string",
                        "description": "Path to the file (absolute or relative)"
                    },
                    "content": {
                        "type": "string",
                        "description": "Content to write to the file"
                    }
                },
                "required": ["file_path", "content"]
            }
        }]

    async def execute_tool(self, name: str, params: dict) -> WriteResult:
        """Execute the write tool."""
        if name == "write":
            return await self.write(
                file_path=params["file_path"],
                content=params["content"],
            )
        return WriteResult(
            success=False,
            file_path="",
            bytes_written=0,
            created=False,
            error=f"Unknown write tool: {name}"
        )


# ============================================================================
# Edit Tool
# ============================================================================

class EditTool:
    """
    Edit files with precise string replacement.

    Features:
    - Exact string matching (no regex)
    - replace_all option for multiple occurrences
    - Validates old_string exists before editing
    """

    def __init__(self, working_dir: str | Path | None = None):
        self.working_dir = Path(working_dir) if working_dir else Path.cwd()

    def _resolve_path(self, file_path: str) -> Path:
        """Resolve file path, handling relative paths."""
        path = Path(file_path)
        if not path.is_absolute():
            path = self.working_dir / path
        return path.resolve()

    async def edit(
        self,
        file_path: str,
        old_string: str,
        new_string: str,
        replace_all: bool = False,
    ) -> EditResult:
        """
        Edit a file by replacing strings.

        Args:
            file_path: Path to the file
            old_string: Text to find and replace
            new_string: Text to replace with
            replace_all: If True, replace all occurrences; if False, replace first only

        Returns:
            EditResult with status
        """
        path = self._resolve_path(file_path)

        if not path.exists():
            return EditResult(
                success=False,
                file_path=str(path),
                replacements_made=0,
                error=f"File not found: {path}"
            )

        try:
            # Read current content
            with open(path, 'r', encoding='utf-8') as f:
                content = f.read()

            # Check if old_string exists
            if old_string not in content:
                return EditResult(
                    success=False,
                    file_path=str(path),
                    replacements_made=0,
                    error=f"String not found in file. Make sure to read the file first and use exact text."
                )

            # Count occurrences
            count = content.count(old_string)

            # Validate uniqueness for single replacement
            if not replace_all and count > 1:
                return EditResult(
                    success=False,
                    file_path=str(path),
                    replacements_made=0,
                    error=f"Found {count} occurrences of the string. Either use replace_all=true or provide more context to make it unique."
                )

            # Make replacement
            if replace_all:
                new_content = content.replace(old_string, new_string)
                replacements = count
            else:
                new_content = content.replace(old_string, new_string, 1)
                replacements = 1

            # Write back
            with open(path, 'w', encoding='utf-8') as f:
                f.write(new_content)

            return EditResult(
                success=True,
                file_path=str(path),
                replacements_made=replacements,
            )

        except Exception as e:
            return EditResult(
                success=False,
                file_path=str(path),
                replacements_made=0,
                error=str(e)
            )

    def get_tool_schemas(self) -> list[dict]:
        """Get Claude tool schema for edit."""
        return [{
            "name": "edit",
            "description": (
                "Edit a file by replacing exact string matches. You MUST read the file first "
                "before editing. The old_string must match exactly (including whitespace/indentation). "
                "For replacing all occurrences, set replace_all=true. "
                "For unique replacements, include enough surrounding context."
            ),
            "input_schema": {
                "type": "object",
                "properties": {
                    "file_path": {
                        "type": "string",
                        "description": "Path to the file to edit"
                    },
                    "old_string": {
                        "type": "string",
                        "description": "Exact text to find (must match exactly)"
                    },
                    "new_string": {
                        "type": "string",
                        "description": "Text to replace with"
                    },
                    "replace_all": {
                        "type": "boolean",
                        "description": "Replace all occurrences (default false)"
                    }
                },
                "required": ["file_path", "old_string", "new_string"]
            }
        }]

    async def execute_tool(self, name: str, params: dict) -> EditResult:
        """Execute the edit tool."""
        if name == "edit":
            return await self.edit(
                file_path=params["file_path"],
                old_string=params["old_string"],
                new_string=params["new_string"],
                replace_all=params.get("replace_all", False),
            )
        return EditResult(
            success=False,
            file_path="",
            replacements_made=0,
            error=f"Unknown edit tool: {name}"
        )


# ============================================================================
# Glob Tool
# ============================================================================

class GlobTool:
    """
    Find files matching glob patterns.

    Features:
    - Standard glob patterns (*, **, ?)
    - Sorted by modification time (newest first)
    - Respects .gitignore (if in git repo)
    """

    MAX_RESULTS = 1000

    def __init__(self, working_dir: str | Path | None = None):
        self.working_dir = Path(working_dir) if working_dir else Path.cwd()

    def _resolve_path(self, search_path: str | None) -> Path:
        """Resolve search path, defaulting to working directory."""
        if search_path is None:
            return self.working_dir
        path = Path(search_path)
        if not path.is_absolute():
            path = self.working_dir / path
        return path.resolve()

    async def glob(
        self,
        pattern: str,
        path: str | None = None,
    ) -> GlobResult:
        """
        Find files matching a glob pattern.

        Args:
            pattern: Glob pattern (e.g., "**/*.py", "src/*.ts")
            path: Directory to search in (default: working directory)

        Returns:
            GlobResult with matching files
        """
        search_path = self._resolve_path(path)

        if not search_path.exists():
            return GlobResult(
                success=False,
                files=[],
                pattern=pattern,
                search_path=str(search_path),
                error=f"Search path not found: {search_path}"
            )

        try:
            # Use pathlib glob
            if "**" in pattern:
                matches = list(search_path.glob(pattern))
            else:
                matches = list(search_path.glob(pattern))

            # Filter to files only
            files = [str(m) for m in matches if m.is_file()]

            # Sort by modification time (newest first)
            files.sort(key=lambda f: os.path.getmtime(f), reverse=True)

            # Limit results
            if len(files) > self.MAX_RESULTS:
                files = files[:self.MAX_RESULTS]

            return GlobResult(
                success=True,
                files=files,
                pattern=pattern,
                search_path=str(search_path),
            )

        except Exception as e:
            return GlobResult(
                success=False,
                files=[],
                pattern=pattern,
                search_path=str(search_path),
                error=str(e)
            )

    def get_tool_schemas(self) -> list[dict]:
        """Get Claude tool schema for glob."""
        return [{
            "name": "glob",
            "description": (
                "Find files matching a glob pattern. Returns files sorted by modification time "
                "(newest first). Supports ** for recursive matching. "
                "Examples: '**/*.py' (all Python files), 'src/**/*.ts' (TypeScript in src)."
            ),
            "input_schema": {
                "type": "object",
                "properties": {
                    "pattern": {
                        "type": "string",
                        "description": "Glob pattern to match (e.g., '**/*.py')"
                    },
                    "path": {
                        "type": "string",
                        "description": "Directory to search in (default: working directory)"
                    }
                },
                "required": ["pattern"]
            }
        }]

    async def execute_tool(self, name: str, params: dict) -> GlobResult:
        """Execute the glob tool."""
        if name == "glob":
            return await self.glob(
                pattern=params["pattern"],
                path=params.get("path"),
            )
        return GlobResult(
            success=False,
            files=[],
            pattern="",
            search_path="",
            error=f"Unknown glob tool: {name}"
        )


# ============================================================================
# Grep Tool
# ============================================================================

class GrepTool:
    """
    Search file contents with regex patterns.

    Features:
    - Regex support
    - Context lines (before/after)
    - File type filtering
    - Uses ripgrep if available, falls back to Python
    """

    MAX_MATCHES = 500

    def __init__(self, working_dir: str | Path | None = None):
        self.working_dir = Path(working_dir) if working_dir else Path.cwd()
        self._has_ripgrep = self._check_ripgrep()

    def _check_ripgrep(self) -> bool:
        """Check if ripgrep is available."""
        try:
            subprocess.run(["rg", "--version"], capture_output=True, check=True)
            return True
        except (subprocess.CalledProcessError, FileNotFoundError):
            return False

    def _resolve_path(self, search_path: str | None) -> Path:
        """Resolve search path, defaulting to working directory."""
        if search_path is None:
            return self.working_dir
        path = Path(search_path)
        if not path.is_absolute():
            path = self.working_dir / path
        return path.resolve()

    async def grep(
        self,
        pattern: str,
        path: str | None = None,
        glob_pattern: str | None = None,
        context_before: int = 0,
        context_after: int = 0,
        case_insensitive: bool = False,
    ) -> GrepResult:
        """
        Search for pattern in files.

        Args:
            pattern: Regex pattern to search for
            path: Directory or file to search (default: working directory)
            glob_pattern: File pattern to filter (e.g., "*.py")
            context_before: Lines of context before match
            context_after: Lines of context after match
            case_insensitive: Ignore case in matching

        Returns:
            GrepResult with matches
        """
        search_path = self._resolve_path(path)

        if not search_path.exists():
            return GrepResult(
                success=False,
                matches=[],
                pattern=pattern,
                files_searched=0,
                error=f"Search path not found: {search_path}"
            )

        if self._has_ripgrep:
            return await self._grep_with_ripgrep(
                pattern, search_path, glob_pattern,
                context_before, context_after, case_insensitive
            )
        else:
            return await self._grep_with_python(
                pattern, search_path, glob_pattern,
                context_before, context_after, case_insensitive
            )

    async def _grep_with_ripgrep(
        self,
        pattern: str,
        search_path: Path,
        glob_pattern: str | None,
        context_before: int,
        context_after: int,
        case_insensitive: bool,
    ) -> GrepResult:
        """Use ripgrep for fast searching."""
        cmd = ["rg", "--line-number", "--no-heading"]

        if case_insensitive:
            cmd.append("-i")
        if context_before > 0:
            cmd.extend(["-B", str(context_before)])
        if context_after > 0:
            cmd.extend(["-A", str(context_after)])
        if glob_pattern:
            cmd.extend(["--glob", glob_pattern])

        cmd.extend([pattern, str(search_path)])

        try:
            result = subprocess.run(
                cmd,
                capture_output=True,
                text=True,
                timeout=60,
            )

            matches = []
            files_seen = set()

            for line in result.stdout.split('\n'):
                if not line.strip():
                    continue

                # Parse ripgrep output: file:line:content
                parts = line.split(':', 2)
                if len(parts) >= 3:
                    file_path = parts[0]
                    try:
                        line_num = int(parts[1])
                        content = parts[2]
                        files_seen.add(file_path)

                        matches.append(GrepMatch(
                            file=file_path,
                            line_number=line_num,
                            content=content,
                        ))

                        if len(matches) >= self.MAX_MATCHES:
                            break
                    except ValueError:
                        continue

            return GrepResult(
                success=True,
                matches=matches,
                pattern=pattern,
                files_searched=len(files_seen),
            )

        except subprocess.TimeoutExpired:
            return GrepResult(
                success=False,
                matches=[],
                pattern=pattern,
                files_searched=0,
                error="Search timed out after 60 seconds"
            )
        except Exception as e:
            return GrepResult(
                success=False,
                matches=[],
                pattern=pattern,
                files_searched=0,
                error=str(e)
            )

    async def _grep_with_python(
        self,
        pattern: str,
        search_path: Path,
        glob_pattern: str | None,
        context_before: int,
        context_after: int,
        case_insensitive: bool,
    ) -> GrepResult:
        """Fallback to Python-based search."""
        flags = re.IGNORECASE if case_insensitive else 0

        try:
            regex = re.compile(pattern, flags)
        except re.error as e:
            return GrepResult(
                success=False,
                matches=[],
                pattern=pattern,
                files_searched=0,
                error=f"Invalid regex: {e}"
            )

        matches = []
        files_searched = 0

        # Get files to search
        if search_path.is_file():
            files = [search_path]
        else:
            glob_pat = glob_pattern or "**/*"
            files = [f for f in search_path.glob(glob_pat) if f.is_file()]

        for file_path in files:
            if len(matches) >= self.MAX_MATCHES:
                break

            try:
                with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
                    lines = f.readlines()

                files_searched += 1

                for i, line in enumerate(lines):
                    if regex.search(line):
                        # Get context
                        ctx_before = None
                        ctx_after = None

                        if context_before > 0:
                            start = max(0, i - context_before)
                            ctx_before = [l.rstrip() for l in lines[start:i]]

                        if context_after > 0:
                            end = min(len(lines), i + context_after + 1)
                            ctx_after = [l.rstrip() for l in lines[i+1:end]]

                        matches.append(GrepMatch(
                            file=str(file_path),
                            line_number=i + 1,
                            content=line.rstrip(),
                            context_before=ctx_before,
                            context_after=ctx_after,
                        ))

                        if len(matches) >= self.MAX_MATCHES:
                            break

            except Exception:
                continue  # Skip files that can't be read

        return GrepResult(
            success=True,
            matches=matches,
            pattern=pattern,
            files_searched=files_searched,
        )

    def get_tool_schemas(self) -> list[dict]:
        """Get Claude tool schema for grep."""
        return [{
            "name": "grep",
            "description": (
                "Search for a regex pattern in files. Returns matching lines with file paths "
                "and line numbers. Supports context lines and file filtering. "
                "Uses ripgrep for speed when available."
            ),
            "input_schema": {
                "type": "object",
                "properties": {
                    "pattern": {
                        "type": "string",
                        "description": "Regex pattern to search for"
                    },
                    "path": {
                        "type": "string",
                        "description": "Directory or file to search (default: working directory)"
                    },
                    "glob": {
                        "type": "string",
                        "description": "File pattern filter (e.g., '*.py')"
                    },
                    "context_before": {
                        "type": "integer",
                        "description": "Lines of context before each match"
                    },
                    "context_after": {
                        "type": "integer",
                        "description": "Lines of context after each match"
                    },
                    "case_insensitive": {
                        "type": "boolean",
                        "description": "Ignore case when matching (default false)"
                    }
                },
                "required": ["pattern"]
            }
        }]

    async def execute_tool(self, name: str, params: dict) -> GrepResult:
        """Execute the grep tool."""
        if name == "grep":
            return await self.grep(
                pattern=params["pattern"],
                path=params.get("path"),
                glob_pattern=params.get("glob"),
                context_before=params.get("context_before", 0),
                context_after=params.get("context_after", 0),
                case_insensitive=params.get("case_insensitive", False),
            )
        return GrepResult(
            success=False,
            matches=[],
            pattern="",
            files_searched=0,
            error=f"Unknown grep tool: {name}"
        )


# ============================================================================
# Combined File Tools
# ============================================================================

class FileTools:
    """
    Combined file tools for convenient access.

    Provides a single entry point for all file operations.
    """

    def __init__(self, working_dir: str | Path | None = None):
        self.working_dir = Path(working_dir) if working_dir else Path.cwd()
        self.read = ReadTool(working_dir)
        self.write = WriteTool(working_dir)
        self.edit = EditTool(working_dir)
        self.glob = GlobTool(working_dir)
        self.grep = GrepTool(working_dir)

    def get_tool_schemas(self) -> list[dict]:
        """Get all file tool schemas."""
        schemas = []
        schemas.extend(self.read.get_tool_schemas())
        schemas.extend(self.write.get_tool_schemas())
        schemas.extend(self.edit.get_tool_schemas())
        schemas.extend(self.glob.get_tool_schemas())
        schemas.extend(self.grep.get_tool_schemas())
        return schemas

    async def execute_tool(self, name: str, params: dict) -> Any:
        """Execute a file tool by name."""
        if name == "read":
            return await self.read.execute_tool(name, params)
        elif name == "write":
            return await self.write.execute_tool(name, params)
        elif name == "edit":
            return await self.edit.execute_tool(name, params)
        elif name == "glob":
            return await self.glob.execute_tool(name, params)
        elif name == "grep":
            return await self.grep.execute_tool(name, params)
        else:
            raise ValueError(f"Unknown file tool: {name}")


# ============================================================================
# Singleton Accessors
# ============================================================================

_file_tools: FileTools | None = None


def get_file_tools(working_dir: str | Path | None = None) -> FileTools:
    """Get the global file tools instance."""
    global _file_tools
    if _file_tools is None:
        _file_tools = FileTools(working_dir)
    return _file_tools


def reset_file_tools() -> None:
    """Reset the global file tools instance."""
    global _file_tools
    _file_tools = None


# Individual tool accessors for convenience
def get_read(working_dir: str | Path | None = None) -> ReadTool:
    """Get a read tool instance."""
    return get_file_tools(working_dir).read


def get_write(working_dir: str | Path | None = None) -> WriteTool:
    """Get a write tool instance."""
    return get_file_tools(working_dir).write


def get_edit(working_dir: str | Path | None = None) -> EditTool:
    """Get an edit tool instance."""
    return get_file_tools(working_dir).edit


def get_glob(working_dir: str | Path | None = None) -> GlobTool:
    """Get a glob tool instance."""
    return get_file_tools(working_dir).glob


def get_grep(working_dir: str | Path | None = None) -> GrepTool:
    """Get a grep tool instance."""
    return get_file_tools(working_dir).grep
