import json
from pathlib import Path
from typing import Any, Dict, List, Optional

from . import tools
from .logger import Logger
from .tools import ToolError


class ToolRegistry:
    """Maps tool names to handlers and provides specs for function calling."""

    def __init__(self, logger: Optional[Logger] = None):
        self.log = logger or Logger()
        self._handlers = {
            "apply_patch": self._handle_apply_patch,
            "shell_command": self._handle_shell_command,
            "read_file": self._handle_read_file,
            "list_dir": self._handle_list_dir,
            "grep_files": self._handle_grep_files,
            "web_search": self._handle_web_search,
            "exec_command": self._handle_exec_command,
            "write_stdin": self._handle_write_stdin,
            "update_plan": self._handle_update_plan,
        }

    def specs(self) -> List[Dict[str, Any]]:
        return [
            {
                "type": "function",
                "function": {
                    "name": "apply_patch",
                    "description": "Apply a patch to files in the workspace.",
                    "parameters": {
                        "type": "object",
                        "properties": {"patch_text": {"type": "string"}},
                        "required": ["patch_text"],
                    },
                },
            },
            {
                "type": "function",
                "function": {
                    "name": "shell_command",
                    "description": "Run a shell command in a given workdir.",
                    "parameters": {
                        "type": "object",
                        "properties": {
                            "command": {"type": "string"},
                            "workdir": {"type": "string"},
                            "timeout": {"type": "integer"},
                        },
                        "required": ["command", "workdir"],
                    },
                },
            },
            {
                "type": "function",
                "function": {
                    "name": "read_file",
                    "description": "Read a slice of a file with line numbers (offset is 1-based, >=1). indent=trueで行番号なしの生テキストを返す。",
                    "parameters": {
                        "type": "object",
                        "properties": {
                            "file": {"type": "string"},
                            "offset": {"type": "integer", "minimum": 1},
                            "limit": {"type": "integer", "minimum": 1},
                            "indent": {"type": "boolean"},
                        },
                        "required": ["file"],
                    },
                },
            },
            {
                "type": "function",
                "function": {
                    "name": "list_dir",
                    "description": "List a directory with optional offset/limit/depth (offset/depth are 1-based, >=1). depthは環境変数LIST_DIR_MAX_DEPTH上限。",
                    "parameters": {
                        "type": "object",
                        "properties": {
                            "dir": {"type": "string"},
                            "offset": {"type": "integer", "minimum": 1},
                            "limit": {"type": "integer", "minimum": 1},
                            "depth": {"type": "integer", "minimum": 1},
                        },
                        "required": ["dir"],
                    },
                },
            },
            {
                "type": "function",
                "function": {
                    "name": "grep_files",
                    "description": "Search files with ripgrep. pattern required; limit 1-500; timeout 60s.",
                    "parameters": {
                        "type": "object",
                        "properties": {
                            "pattern": {"type": "string"},
                            "workdir": {"type": "string"},
                            "limit": {"type": "integer", "minimum": 1, "maximum": 500},
                            "timeout": {"type": "integer", "minimum": 1},
                        },
                        "required": ["pattern"],
                    },
                },
            },
            {
                "type": "function",
                "function": {
                    "name": "web_search",
                    "description": "Perform web search (provider from env); limit<=10; timeout<=60s.",
                    "parameters": {
                        "type": "object",
                        "properties": {
                            "query": {"type": "string"},
                            "limit": {"type": "integer", "minimum": 1, "maximum": 10},
                            "timeout": {"type": "integer", "minimum": 1, "maximum": 60},
                        },
                        "required": ["query"],
                    },
                },
            },
            {
                "type": "function",
                "function": {
                    "name": "exec_command",
                    "description": "Start a PTY exec session. session<=64 chars. timeout<=1200s.",
                    "parameters": {
                        "type": "object",
                        "properties": {
                            "command": {"type": "string"},
                            "session": {"type": "string"},
                            "workdir": {"type": "string"},
                            "timeout": {"type": "integer", "minimum": 1, "maximum": 1200},
                        },
                        "required": ["command", "session", "workdir"],
                    },
                },
            },
            {
                "type": "function",
                "function": {
                    "name": "write_stdin",
                    "description": "Write stdin to an existing exec session and read output.",
                    "parameters": {
                        "type": "object",
                        "properties": {
                            "session": {"type": "string"},
                            "input": {"type": "string"},
                        },
                        "required": ["session", "input"],
                    },
                },
            },
            {
                "type": "function",
                "function": {
                    "name": "update_plan",
                    "description": "Update plan JSON (step/status).",
                    "parameters": {
                        "type": "object",
                        "properties": {"plan": {"type": "object"}},
                        "required": ["plan"],
                    },
                },
            },
        ]

    def execute(self, name: str, arguments: Dict[str, Any]) -> str:
        handler = self._handlers.get(name)
        if not handler:
            raise ToolError(f"Unknown tool: {name}")
        return handler(arguments)

    def _handle_apply_patch(self, args: Dict[str, Any]) -> str:
        patch_text = args.get("patch_text", "")
        changed = tools.apply_patch(patch_text)
        return f"Applied patch to {len(changed)} file(s)."

    def _handle_shell_command(self, args: Dict[str, Any]) -> str:
        command = args.get("command")
        workdir = args.get("workdir")
        timeout = args.get("timeout", 60)
        if not command or not workdir:
            raise ToolError("command and workdir are required")
        result = tools.run_shell_command(command, Path(workdir), timeout=timeout)
        out = []
        out.append(f"Exit code: {result.exit_code}")
        if result.stdout:
            out.append(f"stdout:\n{result.stdout}")
        if result.stderr:
            out.append(f"stderr:\n{result.stderr}")
        return "\n".join(out)

    def _handle_read_file(self, args: Dict[str, Any]) -> str:
        file_path = args.get("file")
        if not file_path:
            raise ToolError("file is required")
        offset = int(args.get("offset", 1))
        limit = int(args.get("limit", 2000))
        indent = bool(args.get("indent", False))
        path = Path(file_path)
        if not path.is_absolute():
            path = Path.cwd() / path
        return tools.read_file_slice(path, offset=offset, limit=limit, indent=indent)

    def _handle_list_dir(self, args: Dict[str, Any]) -> str:
        dir_path = args.get("dir") or str(Path.cwd())
        offset = int(args.get("offset", 1))
        limit = int(args.get("limit", 25))
        depth = int(args.get("depth", 2))
        return tools.list_dir(Path(dir_path), offset=offset, limit=limit, depth=depth)

    def _handle_grep_files(self, args: Dict[str, Any]) -> str:
        pattern = args.get("pattern")
        if not pattern:
            raise ToolError("pattern is required")
        workdir = args.get("workdir")
        limit = int(args.get("limit", 100))
        timeout = int(args.get("timeout", 60))
        return tools.grep_files(pattern, workdir=Path(workdir) if workdir else None, limit=limit, timeout=timeout)

    def _handle_web_search(self, args: Dict[str, Any]) -> str:
        query = args.get("query")
        if not query:
            raise ToolError("query is required")
        limit = int(args.get("limit", 5))
        timeout = int(args.get("timeout", 30))
        return tools.web_search(query, limit=limit, timeout=timeout)

    def _handle_exec_command(self, args: Dict[str, Any]) -> str:
        command = args.get("command")
        session = args.get("session")
        workdir = args.get("workdir")
        timeout = int(args.get("timeout", 600))
        if not command or not session or not workdir:
            raise ToolError("command, session, and workdir are required")
        return tools.exec_command(command, session=session, workdir=Path(workdir), timeout=timeout)

    def _handle_write_stdin(self, args: Dict[str, Any]) -> str:
        session = args.get("session")
        if not session:
            raise ToolError("session is required")
        data = args.get("input", "")
        return tools.write_stdin(session=session, input=data)

    def _handle_update_plan(self, args: Dict[str, Any]) -> str:
        plan = args.get("plan")
        if plan is None:
            raise ToolError("plan is required")
        plan_json = json.dumps({"plan": plan})
        return tools.update_plan(plan_json)
