"""Tool dispatch module with security checks.

Provides a ToolExecutor class that dispatches to sibling tool modules
with path validation ensuring all operations stay within repo_root.
"""

from dataclasses import dataclass
from pathlib import Path
from typing import Any, Callable, Awaitable

from .file_tools import read_file, read_file_chunked, list_files
from .git_tools import git_status, git_log, get_diff
from .search_tools import grep


@dataclass
class ToolResult:
    """Structured result from tool execution.

    Attributes:
        success: Whether the tool executed successfully.
        data: Result data on success (type varies by tool).
        error: Error message on failure.
    """
    success: bool
    data: Any = None
    error: str | None = None


# Supported tools mapped to their required arguments (for backward compatibility)
TOOL_SPECS: dict[str, dict[str, Any]] = {
    "read_file": {
        "func": "read_file",
        "required": ["path"],
        "optional": {"max_lines": 500},
        "path_args": ["path"],
    },
    "read_file_chunked": {
        "func": "read_file_chunked",
        "required": ["path"],
        "optional": {"start_line": 1, "max_lines": 100},
        "path_args": ["path"],
    },
    "list_files": {
        "func": "list_files",
        "required": ["path"],
        "optional": {"pattern": "*"},
        "path_args": ["path"],
    },
    "grep": {
        "func": "grep",
        "required": ["pattern"],
        "optional": {"path": ".", "max_results": 100},
        "path_args": ["path"],
    },
    "git_status": {
        "func": "git_status",
        "required": [],
        "optional": {},
        "path_args": [],
    },
    "git_log": {
        "func": "git_log",
        "required": [],
        "optional": {"n": 5},
        "path_args": [],
    },
    "git_diff": {
        "func": "get_diff",
        "required": ["base", "head"],
        "optional": {},
        "path_args": [],
    },
}


def _validate_path_in_repo(path: str | Path, repo_root: Path) -> str | None:
    """Validate that path resolves within repo_root.

    Args:
        path: Path to validate (absolute or relative)
        repo_root: Repository root that path must resolve within

    Returns:
        None if valid, error message string if invalid
    """
    try:
        path_obj = Path(path)
        if path_obj.is_absolute():
            resolved = path_obj.resolve()
        else:
            resolved = (repo_root / path_obj).resolve()

        # Check path is within repo_root
        try:
            resolved.relative_to(repo_root)
            return None  # Valid
        except ValueError:
            return f"Error: Path outside repository: {path}"
    except Exception as e:
        return f"Error: Invalid path '{path}': {e}"


class ToolExecutor:
    """Executor for dispatching tool calls with security validation.

    All path arguments are validated to ensure they resolve within repo_root.
    Unknown tools and missing required arguments return error results.

    Supports two modes:
    1. Dynamic registration: Use register() to add tools, execute() with kwargs
    2. Legacy mode: Use execute_legacy() with dict args for backward compatibility
    """

    def __init__(self, repo_root: Path) -> None:
        """Initialize executor with repository root.

        Args:
            repo_root: Repository root path. All file operations will be
                      validated to stay within this directory.
        """
        self.repo_root = repo_root.resolve()
        self._tools: dict[str, Callable[..., Awaitable[ToolResult]]] = {}

    def register(self, name: str, func: Callable[..., Awaitable[ToolResult]]) -> None:
        """Register a tool function.

        Args:
            name: Name to register the tool under.
            func: Async callable that takes repo_root and kwargs, returns ToolResult.
        """
        self._tools[name] = func

    async def execute(self, name: str, **kwargs: Any) -> ToolResult:
        """Execute a registered tool with keyword arguments.

        Args:
            name: Name of tool to execute.
            **kwargs: Arguments to pass to the tool function.

        Returns:
            ToolResult with success/data on success, or success=False/error on failure.
        """
        if name not in self._tools:
            return ToolResult(success=False, error=f"Unknown tool: {name}")

        try:
            result = await self._tools[name](repo_root=self.repo_root, **kwargs)
            return result
        except Exception as e:
            return ToolResult(success=False, error=str(e))

    async def execute_legacy_async(self, tool: str, args: dict[str, Any]) -> str:
        """Execute a tool with dict arguments (async version).

        Validates tool name is in allowed list, validates all path arguments
        are within repo_root, then dispatches to the appropriate tool function.

        Args:
            tool: Name of tool to execute
            args: Dictionary of arguments to pass to tool

        Returns:
            Tool result as string on success, or "Error: ..." message on failure
        """
        # Validate tool name
        if tool not in TOOL_SPECS:
            return f"Error: Unknown tool: {tool}"

        spec = TOOL_SPECS[tool]

        # Validate required arguments
        for required_arg in spec["required"]:
            if required_arg not in args:
                return f"Error: Missing required argument: {required_arg}"

        # Validate path arguments are within repo_root
        for path_arg in spec["path_args"]:
            if path_arg in args:
                error = _validate_path_in_repo(args[path_arg], self.repo_root)
                if error:
                    return error

        # Build final arguments with defaults
        final_args: dict[str, Any] = dict(spec["optional"])  # Start with defaults
        final_args.update(args)  # Override with provided args

        # Dispatch to appropriate tool (all tools are async)
        try:
            if tool == "read_file":
                result = await read_file(
                    path=final_args["path"],
                    repo_root=self.repo_root,
                    max_lines=final_args.get("max_lines", 500),
                )
                return result.data if result.success else f"Error: {result.error}"

            elif tool == "read_file_chunked":
                result = await read_file_chunked(
                    path=final_args["path"],
                    repo_root=self.repo_root,
                    start_line=final_args.get("start_line", 1),
                    max_lines=final_args.get("max_lines", 100),
                )
                return result.data if result.success else f"Error: {result.error}"

            elif tool == "list_files":
                result = await list_files(
                    path=final_args["path"],
                    repo_root=self.repo_root,
                    pattern=final_args.get("pattern", "*"),
                )
                if result.success:
                    return "\n".join(result.data) if result.data else "(no files found)"
                return f"Error: {result.error}"

            elif tool == "grep":
                result = await grep(
                    pattern=final_args["pattern"],
                    repo_root=self.repo_root,
                    path=final_args.get("path", "."),
                    max_results=final_args.get("max_results", 100),
                )
                if result.success:
                    return "\n".join(result.data) if result.data else "No matches found"
                return f"Error: {result.error}"

            elif tool == "git_status":
                result = await git_status(self.repo_root)
                return result.data if result.success else f"Error: {result.error}"

            elif tool == "git_log":
                result = await git_log(
                    repo_root=self.repo_root,
                    n=final_args.get("n", 5),
                )
                return result.data if result.success else f"Error: {result.error}"

            elif tool == "git_diff":
                result = await get_diff(
                    repo_root=self.repo_root,
                    base=final_args["base"],
                    head=final_args["head"],
                )
                return result.data if result.success else f"Error: {result.error}"

            else:
                # Should not reach here if TOOL_SPECS is complete
                return f"Error: Tool dispatch not implemented: {tool}"

        except Exception as e:
            # Pass through tool errors
            return f"Error: {e}"

    def execute_legacy(self, tool: str, args: dict[str, Any]) -> str:
        """Execute a tool with dict arguments (sync wrapper for legacy interface).

        Note: This runs the async tools via asyncio.run(). Prefer using
        execute_legacy_async() in async contexts.

        Args:
            tool: Name of tool to execute
            args: Dictionary of arguments to pass to tool

        Returns:
            Tool result as string on success, or "Error: ..." message on failure
        """
        import asyncio
        return asyncio.run(self.execute_legacy_async(tool, args))

    def list_tools(self) -> list[str]:
        """List all supported tools.

        Returns:
            List of tool names that can be executed (both registered and legacy).
        """
        # Combine registered tools and legacy TOOL_SPECS
        all_tools = set(self._tools.keys()) | set(TOOL_SPECS.keys())
        return sorted(all_tools)
