"""MemoryTool20250818 - Anthropic's native memory tool.

Enables Claude to create, read, update, and delete files in a persistent
/memories directory across conversations.
"""

from __future__ import annotations

from pathlib import Path
from typing import Any, Literal

from nanobot.agent.tools.anthropic.base import BaseAnthropicTool, CLIResult


class MemoryTool20250818(BaseAnthropicTool):
    """Anthropic's native memory_20250818 tool.

    Client-side tool for persistent memory storage across conversations.
    All operations are restricted to the /memories directory.

    Commands:
        - view: Show directory contents or file contents with line numbers
        - create: Create a new file with content
        - str_replace: Replace unique text occurrence in a file
        - insert: Insert text at a specific line number
        - delete: Delete a file or directory
        - rename: Rename or move a file/directory
    """

    api_type: Literal["memory_20250818"] = "memory_20250818"
    name: Literal["memory"] = "memory"
    beta_flag: str = "context-management-2025-06-27"

    def __init__(self, workspace: Path):
        """Initialize Memory tool.

        Args:
            workspace: Root workspace directory
        """
        self.workspace = workspace
        self.memories_dir = workspace / "memories"
        self.memories_dir.mkdir(parents=True, exist_ok=True)

    def _validate_memory_path(self, path: str) -> Path:
        """Validate and resolve path to prevent directory traversal.

        Args:
            path: Path string starting with /memories

        Returns:
            Validated absolute Path within memories directory

        Raises:
            ValueError: If path is invalid or escapes /memories directory
        """
        # Reject paths not starting with /memories
        if not path.startswith("/memories"):
            raise ValueError(f"Path must start with /memories, got: {path}")

        # Resolve to absolute path within workspace
        # lstrip("/") removes leading slash: "/memories/file.txt" -> "memories/file.txt"
        relative_path = path.lstrip("/")
        full_path = (self.workspace / relative_path).resolve()

        # Verify resolved path is within memories directory
        memories_dir_resolved = self.memories_dir.resolve()
        try:
            full_path.relative_to(memories_dir_resolved)
        except ValueError:
            raise ValueError(f"Path escapes /memories directory: {path}")

        return full_path

    async def __call__(
        self,
        command: Literal["view", "create", "str_replace", "insert", "delete", "rename"],
        path: str | None = None,
        old_path: str | None = None,
        new_path: str | None = None,
        file_text: str | None = None,
        old_str: str | None = None,
        new_str: str | None = None,
        insert_line: int | None = None,
        insert_text: str | None = None,
        view_range: list[int] | None = None,
        **kwargs: Any,
    ) -> CLIResult:
        """Execute memory command.

        Args:
            command: Command to execute
            path: File/directory path (for view/create/str_replace/insert/delete)
            old_path: Source path (for rename)
            new_path: Destination path (for rename)
            file_text: File content (for create)
            old_str: Text to find (for str_replace)
            new_str: Replacement text (for str_replace)
            insert_line: Line number to insert at (for insert)
            insert_text: Text to insert (for insert)
            view_range: [start_line, end_line] for view
            **kwargs: Additional arguments (ignored)

        Returns:
            CLIResult with command output or error
        """
        try:
            if command == "view":
                return await self._view(path, view_range)
            elif command == "create":
                return await self._create(path, file_text)
            elif command == "str_replace":
                return await self._str_replace(path, old_str, new_str)
            elif command == "insert":
                return await self._insert(path, insert_line, insert_text)
            elif command == "delete":
                return await self._delete(path)
            elif command == "rename":
                return await self._rename(old_path, new_path)
            else:
                return CLIResult(
                    exit_code=1,
                    output="",
                    error=f"Unknown command: {command}"
                )
        except ValueError as e:
            # Path security error
            return CLIResult(
                exit_code=1,
                output="",
                error=f"Error: {e}"
            )
        except Exception as e:
            return CLIResult(
                exit_code=1,
                output="",
                error=f"Error: {e}"
            )

    async def _view(
        self,
        path: str | None,
        view_range: list[int] | None = None,
    ) -> CLIResult:
        """View directory listing or file contents.

        Args:
            path: Path to view
            view_range: Optional [start_line, end_line] for file viewing (1-indexed)

        Returns:
            CLIResult with directory listing or file contents
        """
        if path is None:
            return CLIResult(
                exit_code=1,
                output="",
                error="Error: path is required for view command"
            )

        path_str = path  # Keep original for error messages
        validated_path = self._validate_memory_path(path)

        # Directory listing
        if validated_path.is_dir():
            return await self._view_directory(validated_path, path_str)

        # File viewing
        if not validated_path.exists():
            return CLIResult(
                exit_code=1,
                output="",
                error=f"The path {path_str} does not exist. Please provide a valid path."
            )

        # Read file
        try:
            content = validated_path.read_text()
        except Exception as e:
            return CLIResult(
                exit_code=1,
                output="",
                error=f"Error reading file: {e}"
            )

        lines = content.splitlines(keepends=True)

        # Check line limit
        if len(lines) > 999_999:
            return CLIResult(
                exit_code=1,
                output="",
                error=f"File {path_str} exceeds maximum line limit of 999,999 lines."
            )

        # Apply view_range if specified
        if view_range:
            start, end = view_range
            # Convert to 0-indexed, clamp to valid range
            start_idx = max(0, start - 1)
            end_idx = min(len(lines), end)
            lines_to_show = lines[start_idx:end_idx]
            start_num = start
        else:
            lines_to_show = lines
            start_num = 1

        # Format with line numbers (6 chars, right-aligned, tab-separated)
        formatted_lines = []
        for i, line in enumerate(lines_to_show):
            line_num = start_num + i
            # Remove trailing newline for display
            line_content = line.rstrip("\n")
            formatted_lines.append(f"{line_num:6d}\t{line_content}")

        output = f"Here's the content of {path_str} with line numbers:\n"
        output += "\n".join(formatted_lines)

        return CLIResult(
            exit_code=0,
            output=output,
            error=""
        )

    async def _view_directory(self, path: Path, path_str: str) -> CLIResult:
        """View directory listing up to 2 levels deep.

        Args:
            path: Validated Path object
            path_str: Original path string for display

        Returns:
            CLIResult with directory listing
        """
        import os

        def format_size(size_bytes: int) -> str:
            """Convert bytes to human-readable format."""
            for unit in ['B', 'K', 'M', 'G', 'T']:
                if size_bytes < 1024:
                    return f"{size_bytes:.1f}{unit}"
                size_bytes /= 1024
            return f"{size_bytes:.1f}P"

        lines = []
        header = f"Here're the files and directories up to 2 levels deep in {path_str}, excluding hidden items and node_modules:"
        lines.append(header)

        # Walk directory tree (max depth 2)
        base_depth = str(path).count(os.sep)

        for root, dirs, files in os.walk(path):
            # Calculate current depth
            current_depth = str(root).count(os.sep) - base_depth

            # Filter out hidden items and node_modules at this level
            dirs[:] = [d for d in dirs if not d.startswith('.') and d != 'node_modules']

            # Stop if we've gone too deep
            if current_depth >= 2:
                dirs.clear()  # Don't recurse further
                continue

            # Get size and add directory entry
            root_path = Path(root)
            try:
                # Directory size (sum of all files within, or 4K default)
                dir_size = sum(f.stat().st_size for f in root_path.rglob('*') if f.is_file())
                if dir_size == 0:
                    dir_size = 4096  # Default directory size
                size_str = format_size(dir_size)

                # Convert absolute path to /memories/... format
                relative = root_path.relative_to(self.workspace)
                display_path = "/" + str(relative).replace(os.sep, "/")

                lines.append(f"{size_str}\t{display_path}")
            except Exception:
                pass

            # Add file entries at this level
            for filename in sorted(files):
                if filename.startswith('.'):
                    continue  # Skip hidden files

                file_path = root_path / filename
                try:
                    file_size = file_path.stat().st_size
                    size_str = format_size(file_size)

                    # Convert to /memories/... format
                    relative = file_path.relative_to(self.workspace)
                    display_path = "/" + str(relative).replace(os.sep, "/")

                    lines.append(f"{size_str}\t{display_path}")
                except Exception:
                    pass

        return CLIResult(
            exit_code=0,
            output="\n".join(lines),
            error=""
        )

    async def _create(
        self,
        path: str | None,
        file_text: str | None,
    ) -> CLIResult:
        """Create a new file with content.

        Args:
            path: File path to create
            file_text: Content to write

        Returns:
            CLIResult with success message or error
        """
        if path is None:
            return CLIResult(
                exit_code=1,
                output="",
                error="Error: path is required for create command"
            )

        if file_text is None:
            return CLIResult(
                exit_code=1,
                output="",
                error="Error: file_text is required for create command"
            )

        path_str = path  # Keep original for error messages
        validated_path = self._validate_memory_path(path)

        # Check if file already exists
        if validated_path.exists():
            return CLIResult(
                exit_code=1,
                output="",
                error=f"Error: File {path_str} already exists"
            )

        # Create parent directories if needed
        try:
            validated_path.parent.mkdir(parents=True, exist_ok=True)
        except Exception as e:
            return CLIResult(
                exit_code=1,
                output="",
                error=f"Error creating parent directories: {e}"
            )

        # Write file
        try:
            validated_path.write_text(file_text)
        except Exception as e:
            return CLIResult(
                exit_code=1,
                output="",
                error=f"Error writing file: {e}"
            )

        return CLIResult(
            exit_code=0,
            output=f"File created successfully at: {path_str}",
            error=""
        )

    async def _str_replace(
        self,
        path: str | None,
        old_str: str | None,
        new_str: str | None,
    ) -> CLIResult:
        """Replace unique occurrence of old_str with new_str."""
        if path is None or old_str is None or new_str is None:
            return CLIResult(
                exit_code=1,
                output="",
                error="Error: old_str and new_str are required for str_replace command"
            )

        path_str = path
        validated_path = self._validate_memory_path(path)

        if not validated_path.exists() or validated_path.is_dir():
            return CLIResult(
                exit_code=1,
                output="",
                error=f"Error: The path {path_str} does not exist. Please provide a valid path."
            )

        content = validated_path.read_text()
        count = content.count(old_str)

        if count == 0:
            return CLIResult(
                exit_code=1,
                output="",
                error=f"No replacement was performed, old_str `{old_str}` did not appear verbatim in {path_str}."
            )
        elif count > 1:
            lines = content.splitlines()
            line_nums = [i + 1 for i, line in enumerate(lines) if old_str in line]
            return CLIResult(
                exit_code=1,
                output="",
                error=f"No replacement was performed. Multiple occurrences of old_str `{old_str}` in lines: {line_nums}. Please ensure it is unique"
            )

        new_content = content.replace(old_str, new_str, 1)
        validated_path.write_text(new_content)

        return CLIResult(
            exit_code=0,
            output="The memory file has been edited.",
            error=""
        )

    async def _insert(
        self, path: str | None, insert_line: int | None, insert_text: str | None
    ) -> CLIResult:
        """Insert text at a specific line number.

        Args:
            path: File path to modify
            insert_line: Line number to insert at (0 = beginning)
            insert_text: Text to insert

        Returns:
            CLIResult with success message or error
        """
        if path is None or insert_line is None or insert_text is None:
            return CLIResult(
                exit_code=1,
                output="",
                error="Error: path, insert_line, and insert_text are required for insert command"
            )

        path_str = path
        validated_path = self._validate_memory_path(path)

        if not validated_path.exists() or validated_path.is_dir():
            return CLIResult(
                exit_code=1,
                output="",
                error=f"Error: The path {path_str} does not exist. Please provide a valid path."
            )

        # Read current content
        content = validated_path.read_text()
        lines = content.splitlines(keepends=True)

        # Validate insert_line
        if insert_line < 0 or insert_line > len(lines):
            return CLIResult(
                exit_code=1,
                output="",
                error=f"Invalid `insert_line` parameter: {insert_line}. It should be within 0 to {len(lines)}"
            )

        # Insert text at specified line
        lines.insert(insert_line, insert_text)
        new_content = "".join(lines)
        validated_path.write_text(new_content)

        return CLIResult(
            exit_code=0,
            output=f"The file {path_str} has been edited.",
            error=""
        )

    async def _delete(self, path: str | None) -> CLIResult:
        """Delete a file or directory.

        Args:
            path: Path to delete

        Returns:
            CLIResult with success message or error
        """
        if path is None:
            return CLIResult(
                exit_code=1,
                output="",
                error="Error: path is required for delete command"
            )

        path_str = path
        validated_path = self._validate_memory_path(path)

        if not validated_path.exists():
            return CLIResult(
                exit_code=1,
                output="",
                error=f"Error: The path {path_str} does not exist. Please provide a valid path."
            )

        # Delete file or directory
        try:
            if validated_path.is_dir():
                import shutil
                shutil.rmtree(validated_path)
            else:
                validated_path.unlink()
        except Exception as e:
            return CLIResult(
                exit_code=1,
                output="",
                error=f"Error deleting {path_str}: {e}"
            )

        return CLIResult(
            exit_code=0,
            output=f"Successfully deleted {path_str}",
            error=""
        )

    async def _rename(self, old_path: str | None, new_path: str | None) -> CLIResult:
        """Rename or move a file or directory.

        Args:
            old_path: Source path
            new_path: Destination path

        Returns:
            CLIResult with success message or error
        """
        if old_path is None or new_path is None:
            return CLIResult(
                exit_code=1,
                output="",
                error="Error: old_path and new_path are required for rename command"
            )

        old_path_str = old_path
        new_path_str = new_path
        validated_old = self._validate_memory_path(old_path)
        validated_new = self._validate_memory_path(new_path)

        # Check if source exists
        if not validated_old.exists():
            return CLIResult(
                exit_code=1,
                output="",
                error=f"Error: The path {old_path_str} does not exist. Please provide a valid path."
            )

        # Check if destination already exists
        if validated_new.exists():
            return CLIResult(
                exit_code=1,
                output="",
                error=f"Error: The destination {new_path_str} already exists. Please provide a different destination."
            )

        # Create parent directories if needed
        try:
            validated_new.parent.mkdir(parents=True, exist_ok=True)
        except Exception as e:
            return CLIResult(
                exit_code=1,
                output="",
                error=f"Error creating parent directories: {e}"
            )

        # Rename/move
        try:
            validated_old.rename(validated_new)
        except Exception as e:
            return CLIResult(
                exit_code=1,
                output="",
                error=f"Error renaming {old_path_str}: {e}"
            )

        return CLIResult(
            exit_code=0,
            output=f"Successfully renamed {old_path_str} to {new_path_str}",
            error=""
        )

    def to_params(self) -> dict[str, Any]:
        """Convert to Anthropic API tool parameter format.

        Returns:
            Tool definition for Anthropic API
        """
        return {
            "type": self.api_type,
            "name": self.name,
        }
