"""Skill-level directory listing and search tools for CleverAgents.

Provides tools that integrate with the skill framework:

- **ListDir**: List directory contents with ignore patterns, size limits,
  and type filters (files/dirs/all).
- **Glob**: Match files by glob pattern with include/exclude filters and
  deterministic sorting.
- **Grep**: Search file contents by regex with line numbers, context
  lines, binary file skipping, per-file match caps, and regex compile
  error handling.

## Path Traversal Guards

All tools reject paths that escape the sandbox root directory.

## Registration

```python
from cleveragents.skills.builtins.search_ops import register_skill_search_tools
from cleveragents.tool.registry import ToolRegistry

registry = ToolRegistry()
register_skill_search_tools(registry)
```
"""

from __future__ import annotations

import fnmatch
import re
from typing import Any

from cleveragents.domain.models.core.tool import ToolCapability
from cleveragents.skills.builtins.file_ops import (
    _is_binary,
    validate_sandbox_path,
)
from cleveragents.tool.registry import ToolRegistry
from cleveragents.tool.runtime import ToolSpec

# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------

#: Default maximum number of entries for ListDir.
DEFAULT_LIST_LIMIT: int = 1000

#: Default maximum matches per file for Grep.
DEFAULT_PER_FILE_CAP: int = 50

#: Default maximum total matches for Grep.
DEFAULT_MAX_MATCHES: int = 500

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


def _handle_skill_list_dir(inputs: dict[str, Any]) -> dict[str, Any]:
    """List directory contents with filters and limits."""
    path = validate_sandbox_path(inputs["path"], inputs.get("sandbox_root"))
    ignore_patterns: list[str] = inputs.get("ignore_patterns", [])
    type_filter: str = inputs.get("type_filter", "all")
    limit: int = inputs.get("limit", DEFAULT_LIST_LIMIT)

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

    if type_filter not in ("all", "files", "dirs"):
        raise ValueError(
            f"Invalid type_filter '{type_filter}'. Must be 'all', 'files', or 'dirs'"
        )

    entries: list[dict[str, Any]] = []
    for item in sorted(path.iterdir()):
        # Apply ignore patterns
        if any(fnmatch.fnmatch(item.name, p) for p in ignore_patterns):
            continue

        is_dir = item.is_dir()
        # Apply type filter
        if type_filter == "files" and is_dir:
            continue
        if type_filter == "dirs" and not is_dir:
            continue

        try:
            stat_info = item.stat()
            size = stat_info.st_size
        except OSError:
            size = 0

        entries.append(
            {
                "name": item.name,
                "path": str(item),
                "size": size,
                "is_dir": is_dir,
            }
        )
        if len(entries) >= limit:
            break

    return {
        "entries": entries,
        "total": len(entries),
        "truncated": len(entries) >= limit,
    }


def _handle_skill_glob(inputs: dict[str, Any]) -> dict[str, Any]:
    """Match files by glob pattern with include/exclude filters."""
    path = validate_sandbox_path(inputs["path"], inputs.get("sandbox_root"))
    pattern: str = inputs["pattern"]
    exclude_patterns: list[str] = inputs.get("exclude_patterns", [])
    include_patterns: list[str] = inputs.get("include_patterns", [])
    limit: int = inputs.get("limit", DEFAULT_LIST_LIMIT)

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

    raw_matches = sorted(path.rglob(pattern))
    matches: list[dict[str, Any]] = []

    for item in raw_matches:
        name = item.name
        # Apply exclude patterns
        if any(fnmatch.fnmatch(name, p) for p in exclude_patterns):
            continue
        # Apply include patterns (if specified, only include matching)
        if include_patterns and not any(
            fnmatch.fnmatch(name, p) for p in include_patterns
        ):
            continue

        try:
            stat_info = item.stat()
            size = stat_info.st_size
        except OSError:
            size = 0

        matches.append(
            {
                "name": name,
                "path": str(item),
                "size": size,
                "is_dir": item.is_dir(),
            }
        )
        if len(matches) >= limit:
            break

    return {
        "matches": matches,
        "total": len(matches),
        "truncated": len(matches) >= limit,
    }


def _handle_skill_grep(inputs: dict[str, Any]) -> dict[str, Any]:
    """Search file contents by regex with context lines."""
    path = validate_sandbox_path(inputs["path"], inputs.get("sandbox_root"))
    pattern: str = inputs["pattern"]
    include: str = inputs.get("include", "*")
    context_lines: int = inputs.get("context_lines", 0)
    per_file_cap: int = inputs.get("per_file_cap", DEFAULT_PER_FILE_CAP)
    max_matches: int = inputs.get("max_matches", DEFAULT_MAX_MATCHES)

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

    # Compile regex with error handling
    try:
        compiled = re.compile(pattern)
    except re.error as exc:
        raise ValueError(f"Invalid regex pattern '{pattern}': {exc}") from exc

    matches: list[dict[str, Any]] = []

    for file_path in sorted(path.rglob(include)):
        if file_path.is_dir():
            continue

        # Skip binary files
        if _is_binary(file_path):
            continue

        try:
            content = file_path.read_text(errors="replace")
        except OSError:
            continue

        file_lines = content.splitlines()
        file_match_count = 0

        for line_no, line in enumerate(file_lines, start=1):
            if compiled.search(line):
                match_entry: dict[str, Any] = {
                    "file": str(file_path),
                    "line": line_no,
                    "content": line,
                }

                # Add context lines if requested
                if context_lines > 0:
                    start = max(0, line_no - 1 - context_lines)
                    end = min(len(file_lines), line_no + context_lines)
                    match_entry["context_before"] = file_lines[start : line_no - 1]
                    match_entry["context_after"] = file_lines[line_no:end]

                matches.append(match_entry)
                file_match_count += 1

                if file_match_count >= per_file_cap:
                    break
                if len(matches) >= max_matches:
                    return {
                        "matches": matches,
                        "total": len(matches),
                        "truncated": True,
                    }

    return {
        "matches": matches,
        "total": len(matches),
        "truncated": False,
    }


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

SKILL_LIST_DIR_SPEC = ToolSpec(
    name="skill/list-dir",
    description=(
        "List directory contents with ignore patterns, size limits, and type filters"
    ),
    input_schema={
        "type": "object",
        "properties": {
            "path": {
                "type": "string",
                "description": "Directory path to list",
            },
            "ignore_patterns": {
                "type": "array",
                "items": {"type": "string"},
                "default": [],
                "description": "Glob patterns to ignore",
            },
            "type_filter": {
                "type": "string",
                "default": "all",
                "description": "Filter: all, files, or dirs",
            },
            "limit": {
                "type": "integer",
                "default": 1000,
                "description": "Maximum entries to return",
            },
            "sandbox_root": {
                "type": "string",
                "description": "Sandbox root directory",
            },
        },
        "required": ["path"],
    },
    output_schema={
        "type": "object",
        "properties": {
            "entries": {
                "type": "array",
                "items": {
                    "type": "object",
                    "properties": {
                        "name": {"type": "string"},
                        "path": {"type": "string"},
                        "size": {"type": "integer"},
                        "is_dir": {"type": "boolean"},
                    },
                },
            },
            "total": {"type": "integer"},
            "truncated": {"type": "boolean"},
        },
    },
    capabilities=ToolCapability(read_only=True),
    handler=_handle_skill_list_dir,
)

SKILL_GLOB_SPEC = ToolSpec(
    name="skill/glob",
    description=(
        "Match files by glob pattern with include/exclude filters "
        "and deterministic sorting"
    ),
    input_schema={
        "type": "object",
        "properties": {
            "path": {
                "type": "string",
                "description": "Directory path to search",
            },
            "pattern": {
                "type": "string",
                "description": "Glob pattern to match",
            },
            "exclude_patterns": {
                "type": "array",
                "items": {"type": "string"},
                "default": [],
                "description": "Glob patterns to exclude",
            },
            "include_patterns": {
                "type": "array",
                "items": {"type": "string"},
                "default": [],
                "description": "Glob patterns to include (filter)",
            },
            "limit": {
                "type": "integer",
                "default": 1000,
                "description": "Maximum matches to return",
            },
            "sandbox_root": {
                "type": "string",
                "description": "Sandbox root directory",
            },
        },
        "required": ["path", "pattern"],
    },
    output_schema={
        "type": "object",
        "properties": {
            "matches": {
                "type": "array",
                "items": {
                    "type": "object",
                    "properties": {
                        "name": {"type": "string"},
                        "path": {"type": "string"},
                        "size": {"type": "integer"},
                        "is_dir": {"type": "boolean"},
                    },
                },
            },
            "total": {"type": "integer"},
            "truncated": {"type": "boolean"},
        },
    },
    capabilities=ToolCapability(read_only=True),
    handler=_handle_skill_glob,
)

SKILL_GREP_SPEC = ToolSpec(
    name="skill/grep",
    description=(
        "Search file contents by regex with line numbers, context lines, "
        "and binary file skipping"
    ),
    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",
            },
            "context_lines": {
                "type": "integer",
                "default": 0,
                "description": "Number of context lines around matches",
            },
            "per_file_cap": {
                "type": "integer",
                "default": 50,
                "description": "Maximum matches per file",
            },
            "max_matches": {
                "type": "integer",
                "default": 500,
                "description": "Maximum total matches",
            },
            "sandbox_root": {
                "type": "string",
                "description": "Sandbox root directory",
            },
        },
        "required": ["path", "pattern"],
    },
    output_schema={
        "type": "object",
        "properties": {
            "matches": {
                "type": "array",
                "items": {
                    "type": "object",
                    "properties": {
                        "file": {"type": "string"},
                        "line": {"type": "integer"},
                        "content": {"type": "string"},
                        "context_before": {
                            "type": "array",
                            "items": {"type": "string"},
                        },
                        "context_after": {
                            "type": "array",
                            "items": {"type": "string"},
                        },
                    },
                },
            },
            "total": {"type": "integer"},
            "truncated": {"type": "boolean"},
        },
    },
    capabilities=ToolCapability(read_only=True),
    handler=_handle_skill_grep,
)

ALL_SKILL_SEARCH_TOOLS: list[ToolSpec] = [
    SKILL_LIST_DIR_SPEC,
    SKILL_GLOB_SPEC,
    SKILL_GREP_SPEC,
]


def register_skill_search_tools(registry: ToolRegistry) -> None:
    """Register all skill-level search tools into *registry*."""
    for spec in ALL_SKILL_SEARCH_TOOLS:
        registry.register(spec)
