"""Built-in filesystem/shell tools for the direct-API fallback path.

These mirror Claude Code's Read, Glob, Grep, Bash, Write, and Edit tools
so the agentic loop can operate on local files without Claude Code CLI.
"""

import fnmatch
import os
import re
import subprocess
from pathlib import Path


def _resolve_safe(file_path: str, cwd: str) -> Path:
    """Resolve *file_path* ensuring it stays within *cwd*."""
    resolved = Path(cwd).resolve() / file_path if not os.path.isabs(file_path) else Path(file_path).resolve()
    cwd_resolved = Path(cwd).resolve()
    if not str(resolved).startswith(str(cwd_resolved)):
        raise ValueError(f"Path {file_path} resolves outside the project directory")
    return resolved


def builtin_read(file_path: str, cwd: str, offset: int = 0, limit: int = 2000) -> str:
    """Read a file with line numbers (cat -n style)."""
    p = _resolve_safe(file_path, cwd)
    if not p.exists():
        return f"Error: file not found: {p}"
    if p.is_dir():
        return f"Error: {p} is a directory, not a file"
    lines = p.read_text(encoding="utf-8", errors="replace").splitlines()
    start = max(0, offset)
    end = start + limit
    selected = lines[start:end]
    numbered = [f"{i + start + 1:>6}\t{line}" for i, line in enumerate(selected)]
    return "\n".join(numbered)


def builtin_glob(pattern: str, cwd: str, path: str | None = None) -> str:
    """Glob for files matching *pattern* under *path* (or cwd)."""
    base = Path(path) if path and os.path.isabs(path) else Path(cwd) / (path or "")
    base = base.resolve()
    matches = sorted(base.glob(pattern), key=lambda p: p.stat().st_mtime if p.exists() else 0, reverse=True)
    if not matches:
        return "No files matched."
    return "\n".join(str(m) for m in matches[:500])


def builtin_grep(pattern: str, cwd: str, path: str | None = None, glob_filter: str | None = None) -> str:
    """Search file contents for *pattern* (regex)."""
    base = Path(path) if path and os.path.isabs(path) else Path(cwd) / (path or "")
    base = base.resolve()
    try:
        regex = re.compile(pattern)
    except re.error as e:
        return f"Invalid regex: {e}"

    results: list[str] = []
    files = base.rglob(glob_filter or "*") if base.is_dir() else [base]
    for f in files:
        if not f.is_file():
            continue
        try:
            for i, line in enumerate(f.read_text(encoding="utf-8", errors="replace").splitlines(), 1):
                if regex.search(line):
                    results.append(f"{f}:{i}: {line.rstrip()}")
                    if len(results) >= 500:
                        results.append("... (truncated at 500 matches)")
                        return "\n".join(results)
        except (OSError, UnicodeDecodeError):
            continue
    return "\n".join(results) if results else "No matches found."


def builtin_bash(command: str, cwd: str, timeout: int = 120) -> str:
    """Run a shell command and return combined output."""
    try:
        result = subprocess.run(
            command,
            shell=True,
            capture_output=True,
            text=True,
            cwd=cwd,
            timeout=timeout,
        )
        output = result.stdout
        if result.stderr:
            output += "\n" + result.stderr if output else result.stderr
        if result.returncode != 0:
            output += f"\n(exit code {result.returncode})"
        return output.strip() or "(no output)"
    except subprocess.TimeoutExpired:
        return f"Command timed out after {timeout}s"
    except Exception as e:
        return f"Error running command: {e}"


def builtin_write(file_path: str, content: str, cwd: str) -> str:
    """Write content to a file, creating parent directories as needed."""
    p = _resolve_safe(file_path, cwd)
    p.parent.mkdir(parents=True, exist_ok=True)
    p.write_text(content, encoding="utf-8")
    return f"Wrote {len(content)} bytes to {p}"


def builtin_edit(file_path: str, old_string: str, new_string: str, cwd: str) -> str:
    """Replace *old_string* with *new_string* in a file."""
    p = _resolve_safe(file_path, cwd)
    if not p.exists():
        return f"Error: file not found: {p}"
    text = p.read_text(encoding="utf-8", errors="replace")
    count = text.count(old_string)
    if count == 0:
        return "Error: old_string not found in file"
    if count > 1:
        return f"Error: old_string found {count} times — provide more context to make it unique"
    text = text.replace(old_string, new_string, 1)
    p.write_text(text, encoding="utf-8")
    return f"Edited {p}"


# ── Anthropic tool definitions (for the API's `tools` parameter) ─────

BUILTIN_TOOL_DEFINITIONS = [
    {
        "name": "Read",
        "description": "Read a file from the local filesystem with line numbers.",
        "input_schema": {
            "type": "object",
            "properties": {
                "file_path": {"type": "string", "description": "Absolute or relative path to the file"},
                "offset": {"type": "integer", "description": "Line offset to start reading from (0-based)", "default": 0},
                "limit": {"type": "integer", "description": "Max lines to read", "default": 2000},
            },
            "required": ["file_path"],
        },
    },
    {
        "name": "Glob",
        "description": "Find files matching a glob pattern.",
        "input_schema": {
            "type": "object",
            "properties": {
                "pattern": {"type": "string", "description": "Glob pattern (e.g. '**/*.py')"},
                "path": {"type": "string", "description": "Directory to search in (default: project root)"},
            },
            "required": ["pattern"],
        },
    },
    {
        "name": "Grep",
        "description": "Search file contents for a regex pattern.",
        "input_schema": {
            "type": "object",
            "properties": {
                "pattern": {"type": "string", "description": "Regex pattern to search for"},
                "path": {"type": "string", "description": "File or directory to search"},
                "glob": {"type": "string", "description": "Glob filter for files (e.g. '*.py')"},
            },
            "required": ["pattern"],
        },
    },
    {
        "name": "Bash",
        "description": "Execute a shell command and return its output.",
        "input_schema": {
            "type": "object",
            "properties": {
                "command": {"type": "string", "description": "The shell command to execute"},
                "timeout": {"type": "integer", "description": "Timeout in seconds (default 120)", "default": 120},
            },
            "required": ["command"],
        },
    },
    {
        "name": "Write",
        "description": "Write content to a file, creating directories as needed.",
        "input_schema": {
            "type": "object",
            "properties": {
                "file_path": {"type": "string", "description": "Path to the file to write"},
                "content": {"type": "string", "description": "Content to write"},
            },
            "required": ["file_path", "content"],
        },
    },
    {
        "name": "Edit",
        "description": "Replace a unique string in a file with new content.",
        "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 be unique in the file)"},
                "new_string": {"type": "string", "description": "Replacement text"},
            },
            "required": ["file_path", "old_string", "new_string"],
        },
    },
]


# ── Scope-aware filtering helpers ────────────────────────────────────


def get_allowed_builtin_definitions(codebase_scope: str) -> list[dict]:
    """Return only builtin tool definitions allowed by the codebase scope."""
    from maeris_mcp.auth.scopes import is_builtin_tool_allowed

    return [d for d in BUILTIN_TOOL_DEFINITIONS if is_builtin_tool_allowed(d["name"], codebase_scope)]


def get_allowed_builtin_dispatch(codebase_scope: str) -> dict:
    """Return only builtin dispatch entries allowed by the codebase scope."""
    from maeris_mcp.auth.scopes import is_builtin_tool_allowed

    return {k: v for k, v in BUILTIN_DISPATCH.items() if is_builtin_tool_allowed(k, codebase_scope)}


# ── Dispatch dict ────────────────────────────────────────────────────

BUILTIN_DISPATCH = {
    "Read": lambda args, cwd: builtin_read(
        args["file_path"], cwd,
        offset=args.get("offset", 0),
        limit=args.get("limit", 2000),
    ),
    "Glob": lambda args, cwd: builtin_glob(
        args["pattern"], cwd,
        path=args.get("path"),
    ),
    "Grep": lambda args, cwd: builtin_grep(
        args["pattern"], cwd,
        path=args.get("path"),
        glob_filter=args.get("glob"),
    ),
    "Bash": lambda args, cwd: builtin_bash(
        args["command"], cwd,
        timeout=args.get("timeout", 120),
    ),
    "Write": lambda args, cwd: builtin_write(
        args["file_path"], args["content"], cwd,
    ),
    "Edit": lambda args, cwd: builtin_edit(
        args["file_path"], args["old_string"], args["new_string"], cwd,
    ),
}
