"""Agent-managed memory tool with sandboxed file operations.

Implements the six file-system-style commands from Chapter 4.8.3:

    view        – read a file or list a directory
    create      – create or overwrite a file (parent dirs are created)
    str_replace – replace the first occurrence of a string inside a file
    insert      – insert a line of text after a specified 1-based line number
    delete      – delete a file
    rename      – move / rename a file within the memory sandbox

Security: all paths are resolved and validated against *base_path* to
prevent directory-traversal attacks (e.g. ``../../etc/passwd``).
"""

from pathlib import Path
from typing import Any, Dict, List, Union

from agentbyte.types import ToolResult

from .base import ApprovalMode, BaseTool


class MemoryBackend:
    """File-backed knowledge store constrained to a base directory."""

    def __init__(self, base_path: Union[str, Path] = "./memories"):
        self.base_path = Path(base_path).resolve()
        self.base_path.mkdir(parents=True, exist_ok=True)

    # ------------------------------------------------------------------
    # Path validation
    # ------------------------------------------------------------------

    def _validate_path(self, path: str) -> Path:
        """Resolve *path* and assert it stays within *base_path*.

        Raises ``ValueError`` for empty paths or directory-traversal attempts.
        """
        normalized = path.strip().lstrip("/")
        if not normalized:
            raise ValueError("path cannot be empty")

        full_path = (self.base_path / normalized).resolve()
        try:
            full_path.relative_to(self.base_path)
        except ValueError as exc:
            raise ValueError("Access denied: path is outside memory directory") from exc

        return full_path

    # ------------------------------------------------------------------
    # The six memory commands
    # ------------------------------------------------------------------

    def view(self, path: str) -> Union[str, List[str]]:
        """Return file contents, or a sorted listing if *path* is a directory.

        Pass ``"/"`` to list all files in the root memory directory.
        """
        # "/" represents the memory root — list everything.
        if path.strip() == "/":
            return sorted(
                str(p.relative_to(self.base_path))
                for p in self.base_path.rglob("*")
                if p.is_file()
            )
        target = self._validate_path(path)
        if target.is_dir():
            return sorted(
                str(p.relative_to(self.base_path))
                for p in target.rglob("*")
                if p.is_file()
            )
        if target.is_file():
            return target.read_text(encoding="utf-8")
        raise FileNotFoundError(f"Path not found: {path}")

    def create(self, path: str, file_text: str) -> str:
        """Create or overwrite *path* with *file_text* (parent dirs auto-created)."""
        target = self._validate_path(path)
        target.parent.mkdir(parents=True, exist_ok=True)
        target.write_text(file_text, encoding="utf-8")
        return f"Created: {path}"

    def str_replace(self, path: str, old_str: str, new_str: str) -> str:
        """Replace the **first occurrence** of *old_str* with *new_str* in *path*."""
        target = self._validate_path(path)
        if not target.is_file():
            raise FileNotFoundError(f"File not found: {path}")
        content = target.read_text(encoding="utf-8")
        if old_str not in content:
            raise ValueError(f"String not found in file: {old_str!r}")
        updated = content.replace(old_str, new_str, 1)
        target.write_text(updated, encoding="utf-8")
        return f"Replaced in: {path}"

    def insert(self, path: str, insert_line: int, insert_text: str) -> str:
        """Insert *insert_text* after 1-based line *insert_line* in *path*."""
        target = self._validate_path(path)
        if not target.is_file():
            raise FileNotFoundError(f"File not found: {path}")
        lines = target.read_text(encoding="utf-8").splitlines(keepends=True)
        if insert_line < 0 or insert_line > len(lines):
            raise ValueError(
                f"insert_line {insert_line} out of range (file has {len(lines)} lines)"
            )
        text = insert_text if insert_text.endswith("\n") else insert_text + "\n"
        lines.insert(insert_line, text)
        target.write_text("".join(lines), encoding="utf-8")
        return f"Inserted after line {insert_line} in: {path}"

    def delete(self, path: str) -> str:
        """Delete the file at *path*."""
        target = self._validate_path(path)
        if not target.exists():
            raise FileNotFoundError(f"Path not found: {path}")
        if target.is_dir():
            raise ValueError("delete only supports files; use a file path")
        target.unlink()
        return f"Deleted: {path}"

    def rename(self, path: str, new_path: str) -> str:
        """Move / rename *path* to *new_path* (both must be within base_path)."""
        src = self._validate_path(path)
        dst = self._validate_path(new_path)
        if not src.exists():
            raise FileNotFoundError(f"Source not found: {path}")
        if dst.exists():
            raise FileExistsError(f"Destination already exists: {new_path}")
        dst.parent.mkdir(parents=True, exist_ok=True)
        src.rename(dst)
        return f"Renamed: {path} -> {new_path}"


class MemoryTool(BaseTool):
    """Tool for explicit agent-managed persistent knowledge operations.

    Agents call this tool to actively curate their own knowledge base across
    sessions.  Unlike ``BaseMemory`` (application-managed), the agent decides
    when to read, write, and organise information through explicit tool calls.

    Supported commands: ``view``, ``create``, ``str_replace``, ``insert``,
    ``delete``, ``rename``.
    """

    def __init__(
        self,
        base_path: Union[str, Path] = "./memories",
        approval_mode: ApprovalMode = ApprovalMode.NEVER,
    ):
        super().__init__(
            name="memory",
            description=(
                "Manage persistent knowledge files. "
                "Commands: view (read file or list directory), "
                "create (write/overwrite file), "
                "str_replace (replace first occurrence of text), "
                "insert (insert text after a line number), "
                "delete (remove a file), "
                "rename (move/rename a file)."
            ),
            approval_mode=approval_mode,
        )
        self.backend = MemoryBackend(base_path)

    @property
    def parameters(self) -> Dict[str, Any]:
        return {
            "type": "object",
            "properties": {
                "command": {
                    "type": "string",
                    "enum": ["view", "create", "str_replace", "insert", "delete", "rename"],
                    "description": "Memory operation to perform",
                },
                "path": {
                    "type": "string",
                    "description": "Target file or directory path relative to memory root",
                },
                "file_text": {
                    "type": "string",
                    "description": "File content for the 'create' command",
                },
                "old_str": {
                    "type": "string",
                    "description": "String to replace (first occurrence) for 'str_replace'",
                },
                "new_str": {
                    "type": "string",
                    "description": "Replacement string for 'str_replace'",
                },
                "insert_line": {
                    "type": "integer",
                    "description": "1-based line number after which to insert text ('insert')",
                },
                "insert_text": {
                    "type": "string",
                    "description": "Text to insert for the 'insert' command",
                },
                "new_path": {
                    "type": "string",
                    "description": "Destination path for the 'rename' command",
                },
            },
            "required": ["command"],
        }

    async def execute(self, parameters: Dict[str, Any]) -> ToolResult:
        command = parameters.get("command")

        try:
            if command == "view":
                path = self._require_str(parameters, "path")
                result = self.backend.view(path)
                metadata = {"command": command, "path": path}

            elif command == "create":
                path = self._require_str(parameters, "path")
                file_text = self._require_str(parameters, "file_text")
                result = self.backend.create(path, file_text)
                metadata = {
                    "command": command,
                    "path": path,
                    "content_length": len(file_text),
                }

            elif command == "str_replace":
                path = self._require_str(parameters, "path")
                old_str = self._require_str(parameters, "old_str")
                new_str = self._require_str(parameters, "new_str")
                result = self.backend.str_replace(path, old_str, new_str)
                metadata = {"command": command, "path": path}

            elif command == "insert":
                path = self._require_str(parameters, "path")
                insert_line = parameters.get("insert_line")
                if not isinstance(insert_line, int):
                    raise ValueError("'insert_line' must be an integer")
                insert_text = self._require_str(parameters, "insert_text")
                result = self.backend.insert(path, insert_line, insert_text)
                metadata = {"command": command, "path": path, "insert_line": insert_line}

            elif command == "delete":
                path = self._require_str(parameters, "path")
                result = self.backend.delete(path)
                metadata = {"command": command, "path": path}

            elif command == "rename":
                path = self._require_str(parameters, "path")
                new_path = self._require_str(parameters, "new_path")
                result = self.backend.rename(path, new_path)
                metadata = {"command": command, "path": path, "new_path": new_path}

            else:
                raise ValueError(
                    f"Unknown command: {command!r}. "
                    "Valid commands: view, create, str_replace, insert, delete, rename."
                )

            return ToolResult(
                success=True,
                result=result,
                metadata=metadata,
            )
        except Exception as exc:
            return ToolResult(
                success=False,
                result=None,
                error=f"Memory operation failed: {exc}",
                metadata={"command": command},
            )

    @staticmethod
    def _require_str(parameters: Dict[str, Any], key: str) -> str:
        value = parameters.get(key)
        if not isinstance(value, str) or not value.strip():
            raise ValueError(f"'{key}' must be a non-empty string")
        return value


__all__ = ["MemoryBackend", "MemoryTool"]