"""Built-in file operation tools for CleverAgents.

The built-in file tools provide safe file system operations for agents.
All tools are registered under the ``builtin/`` namespace and include
path traversal prevention.

## Tools

| Tool                    | Capability     | Description                        |
|-------------------------|----------------|------------------------------------|
| ``builtin/file-read``   | ``read_only``  | Read file content                  |
| ``builtin/file-write``  | ``writes``     | Write content to file              |
| ``builtin/file-edit``   | ``writes``     | Edit file with string replacement  |
| ``builtin/file-delete`` | ``writes``     | Delete a file                      |
| ``builtin/file-list``   | ``read_only``  | List directory contents            |
| ``builtin/file-search`` | ``read_only``  | Search files for content           |

## Resource Slot Requirements

All file tools accept an optional ``sandbox_root`` input parameter that
constrains file operations to a specific directory. When not provided, the
current working directory is used as the sandbox root.

Path traversal attacks (using ``..`` to escape the sandbox) are detected
and rejected with a ``ValueError``.

## ChangeSet Capture

The ``ChangeSetCapture`` class (in
[``cleveragents.tool.builtins.changeset``][cleveragents.tool.builtins.changeset])
wraps write/edit/delete tool executions to automatically record change
entries:

- **ChangeSetEntry**: records ``operation`` (create/modify/delete/move),
  ``path``, optional ``before_hash`` and ``after_hash`` (SHA-256), and
  arbitrary ``metadata``.
- **ChangeSet**: accumulates entries with a ``plan_id`` and provides a
  ``summary()`` method.

## Registration

```python
from cleveragents.tool.builtins import (
    register_file_tools,
    register_file_tools_with_changeset,
)
from cleveragents.tool.builtins.changeset import ChangeSetCapture
from cleveragents.tool.registry import ToolRegistry

# Simple registration
registry = ToolRegistry()
register_file_tools(registry)

# With changeset capture
capture = ChangeSetCapture(plan_id="my-plan")
registry = ToolRegistry()
register_file_tools_with_changeset(registry, capture)
```
"""

from __future__ import annotations

import fnmatch
import re
from pathlib import Path
from typing import Any

from cleveragents.domain.models.core.tool import ToolCapability
from cleveragents.tool.runtime import ToolSpec

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


def validate_path(path_str: str, sandbox_root: str | None = None) -> Path:
    """Validate a path to prevent path traversal attacks.

    Raises ``ValueError`` when the resolved path escapes *sandbox_root*
    (defaults to the current working directory when not supplied).
    """
    root = Path(sandbox_root) if sandbox_root else Path.cwd()
    root = root.resolve()

    target = (root / path_str).resolve()
    if not str(target).startswith(str(root)):
        raise ValueError(f"Path traversal detected: '{path_str}' escapes sandbox root")
    return target


# ---------------------------------------------------------------------------
# Handlers
# ---------------------------------------------------------------------------


def _handle_file_read(inputs: dict[str, Any]) -> dict[str, Any]:
    """Read file content."""
    path = validate_path(inputs["path"], inputs.get("sandbox_root"))
    encoding: str = inputs.get("encoding", "utf-8")
    offset: int = inputs.get("offset", 0)
    limit: int | None = inputs.get("limit")

    content = path.read_text(encoding=encoding)
    lines = content.splitlines(keepends=True)

    if offset > 0 or limit is not None:
        end = offset + limit if limit is not None else len(lines)
        lines = lines[offset:end]
        content = "".join(lines)

    return {
        "content": content,
        "size": path.stat().st_size,
        "encoding": encoding,
    }


def _handle_file_write(inputs: dict[str, Any]) -> dict[str, Any]:
    """Write content to file (create or overwrite)."""
    path = validate_path(inputs["path"], inputs.get("sandbox_root"))
    content: str = inputs["content"]
    encoding: str = inputs.get("encoding", "utf-8")
    create_dirs: bool = inputs.get("create_dirs", True)

    created = not path.exists()
    if create_dirs:
        path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(content, encoding=encoding)

    return {
        "path": str(path),
        "size": path.stat().st_size,
        "created": created,
    }


def _handle_file_edit(inputs: dict[str, Any]) -> dict[str, Any]:
    """Edit file with string replacement."""
    path = validate_path(inputs["path"], inputs.get("sandbox_root"))
    old_text: str = inputs["old_text"]
    new_text: str = inputs["new_text"]
    replace_all: bool = inputs.get("replace_all", False)

    content = path.read_text()
    count = content.count(old_text)

    if count == 0:
        raise ValueError(f"old_text not found in {path}")

    if replace_all:
        content = content.replace(old_text, new_text)
    else:
        content = content.replace(old_text, new_text, 1)
        count = 1

    path.write_text(content)

    return {
        "path": str(path),
        "replacements": count,
    }


def _handle_file_delete(inputs: dict[str, Any]) -> dict[str, Any]:
    """Delete a file."""
    path = validate_path(inputs["path"], inputs.get("sandbox_root"))
    existed = path.exists()
    if existed:
        path.unlink()
    return {
        "path": str(path),
        "existed": existed,
    }


def _handle_file_list(inputs: dict[str, Any]) -> dict[str, Any]:
    """List directory contents."""
    path = validate_path(inputs["path"], inputs.get("sandbox_root"))
    pattern: str = inputs.get("pattern", "*")
    recursive: bool = inputs.get("recursive", False)

    if not path.is_dir():
        raise ValueError(f"Path is not a directory: {path}")

    if recursive:
        entries = list(path.rglob(pattern))
    else:
        entries = [e for e in path.iterdir() if fnmatch.fnmatch(e.name, pattern)]

    files: list[dict[str, Any]] = []
    for entry in sorted(entries):
        stat = entry.stat()
        files.append(
            {
                "name": entry.name,
                "path": str(entry),
                "size": stat.st_size,
                "is_dir": entry.is_dir(),
            }
        )

    return {"files": files}


def _handle_file_search(inputs: dict[str, Any]) -> dict[str, Any]:
    """Search files for content."""
    path = validate_path(inputs["path"], inputs.get("sandbox_root"))
    pattern: str = inputs["pattern"]
    include: str = inputs.get("include", "*")
    max_results: int = inputs.get("max_results", 100)

    if not path.is_dir():
        raise ValueError(f"Path is not a directory: {path}")

    compiled = re.compile(pattern)
    matches: list[dict[str, Any]] = []

    for file_path in sorted(path.rglob(include)):
        if file_path.is_dir():
            continue
        try:
            content = file_path.read_text(errors="replace")
        except (OSError, UnicodeDecodeError):
            continue

        for line_no, line in enumerate(content.splitlines(), start=1):
            if compiled.search(line):
                matches.append(
                    {
                        "file": str(file_path),
                        "line": line_no,
                        "content": line,
                    }
                )
                if len(matches) >= max_results:
                    return {"matches": matches}

    return {"matches": matches}


# ---------------------------------------------------------------------------
# Tool specs
# ---------------------------------------------------------------------------


FILE_READ_SPEC = ToolSpec(
    name="builtin/file-read",
    description="Read file content",
    input_schema={
        "type": "object",
        "properties": {
            "path": {"type": "string", "description": "File path to read"},
            "encoding": {
                "type": "string",
                "default": "utf-8",
                "description": "File encoding",
            },
            "offset": {
                "type": "integer",
                "default": 0,
                "description": "Line offset to start reading from",
            },
            "limit": {
                "type": "integer",
                "description": "Maximum number of lines to read",
            },
        },
        "required": ["path"],
    },
    output_schema={
        "type": "object",
        "properties": {
            "content": {"type": "string"},
            "size": {"type": "integer"},
            "encoding": {"type": "string"},
        },
    },
    capabilities=ToolCapability(read_only=True),
    handler=_handle_file_read,
)

FILE_WRITE_SPEC = ToolSpec(
    name="builtin/file-write",
    description="Write content to file (create or overwrite)",
    input_schema={
        "type": "object",
        "properties": {
            "path": {"type": "string", "description": "File path to write"},
            "content": {"type": "string", "description": "Content to write"},
            "encoding": {
                "type": "string",
                "default": "utf-8",
                "description": "File encoding",
            },
            "create_dirs": {
                "type": "boolean",
                "default": True,
                "description": "Create parent directories if needed",
            },
        },
        "required": ["path", "content"],
    },
    output_schema={
        "type": "object",
        "properties": {
            "path": {"type": "string"},
            "size": {"type": "integer"},
            "created": {"type": "boolean"},
        },
    },
    capabilities=ToolCapability(writes=True),
    handler=_handle_file_write,
)

FILE_EDIT_SPEC = ToolSpec(
    name="builtin/file-edit",
    description="Edit file with string replacement",
    input_schema={
        "type": "object",
        "properties": {
            "path": {"type": "string", "description": "File path to edit"},
            "old_text": {"type": "string", "description": "Text to find"},
            "new_text": {"type": "string", "description": "Replacement text"},
            "replace_all": {
                "type": "boolean",
                "default": False,
                "description": "Replace all occurrences",
            },
        },
        "required": ["path", "old_text", "new_text"],
    },
    output_schema={
        "type": "object",
        "properties": {
            "path": {"type": "string"},
            "replacements": {"type": "integer"},
        },
    },
    capabilities=ToolCapability(writes=True),
    handler=_handle_file_edit,
)

FILE_DELETE_SPEC = ToolSpec(
    name="builtin/file-delete",
    description="Delete a file",
    input_schema={
        "type": "object",
        "properties": {
            "path": {"type": "string", "description": "File path to delete"},
        },
        "required": ["path"],
    },
    output_schema={
        "type": "object",
        "properties": {
            "path": {"type": "string"},
            "existed": {"type": "boolean"},
        },
    },
    capabilities=ToolCapability(writes=True),
    handler=_handle_file_delete,
)

FILE_LIST_SPEC = ToolSpec(
    name="builtin/file-list",
    description="List directory contents",
    input_schema={
        "type": "object",
        "properties": {
            "path": {
                "type": "string",
                "description": "Directory path to list",
            },
            "pattern": {
                "type": "string",
                "default": "*",
                "description": "Glob pattern to filter entries",
            },
            "recursive": {
                "type": "boolean",
                "default": False,
                "description": "Recurse into subdirectories",
            },
        },
        "required": ["path"],
    },
    output_schema={
        "type": "object",
        "properties": {
            "files": {
                "type": "array",
                "items": {
                    "type": "object",
                    "properties": {
                        "name": {"type": "string"},
                        "path": {"type": "string"},
                        "size": {"type": "integer"},
                        "is_dir": {"type": "boolean"},
                    },
                },
            },
        },
    },
    capabilities=ToolCapability(read_only=True),
    handler=_handle_file_list,
)

FILE_SEARCH_SPEC = ToolSpec(
    name="builtin/file-search",
    description="Search files for content",
    input_schema={
        "type": "object",
        "properties": {
            "path": {
                "type": "string",
                "description": "Directory path to search",
            },
            "pattern": {
                "type": "string",
                "description": "Regex pattern to search for",
            },
            "include": {
                "type": "string",
                "default": "*",
                "description": "Glob pattern to filter files",
            },
            "max_results": {
                "type": "integer",
                "default": 100,
                "description": "Maximum number of results",
            },
        },
        "required": ["path", "pattern"],
    },
    output_schema={
        "type": "object",
        "properties": {
            "matches": {
                "type": "array",
                "items": {
                    "type": "object",
                    "properties": {
                        "file": {"type": "string"},
                        "line": {"type": "integer"},
                        "content": {"type": "string"},
                    },
                },
            },
        },
    },
    capabilities=ToolCapability(read_only=True),
    handler=_handle_file_search,
)


ALL_FILE_TOOLS: list[ToolSpec] = [
    FILE_READ_SPEC,
    FILE_WRITE_SPEC,
    FILE_EDIT_SPEC,
    FILE_DELETE_SPEC,
    FILE_LIST_SPEC,
    FILE_SEARCH_SPEC,
]
