from __future__ import annotations

import ast
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Callable, Dict, Optional


class ToolError(RuntimeError):
    pass


def _safe_eval_arithmetic(expr: str) -> str:
    s = (expr or "").strip()
    if not s:
        raise ToolError("empty expression")

    tree = ast.parse(s, mode="eval")

    allowed = (
        ast.Expression,
        ast.BinOp,
        ast.UnaryOp,
        ast.Add,
        ast.Sub,
        ast.Mult,
        ast.Div,
        ast.FloorDiv,
        ast.Mod,
        ast.Pow,
        ast.USub,
        ast.UAdd,
        ast.Constant,
        ast.Load,
        ast.Tuple,
        ast.Call,  # rejected below; kept to give better error message
        ast.Name,  # rejected below
    )

    for node in ast.walk(tree):
        if not isinstance(node, allowed):
            raise ToolError(f"unsupported syntax: {type(node).__name__}")
        if isinstance(node, ast.Call) or isinstance(node, ast.Name):
            raise ToolError("names/calls are not allowed")
        if isinstance(node, ast.Constant) and not isinstance(node.value, (int, float)):
            raise ToolError("only numeric constants allowed")

    val = eval(compile(tree, "<calc>", "eval"), {"__builtins__": {}}, {})  # noqa: S307
    if isinstance(val, float) and val.is_integer():
        return str(int(val))
    return str(val)


def _read_sandbox_file(sandbox_dir: Path, rel_path: str) -> str:
    rel = (rel_path or "").strip().replace("\\", "/")
    if not rel or ".." in rel or rel.startswith("/") or rel.startswith("."):
        raise ToolError("invalid path")

    path = (sandbox_dir / rel).resolve()
    if sandbox_dir.resolve() not in path.parents and path != sandbox_dir.resolve():
        raise ToolError("path escape blocked")
    if not path.exists() or not path.is_file():
        raise ToolError("file not found")

    return path.read_text(encoding="utf-8")


@dataclass(frozen=True)
class ToolCall:
    name: str
    tool_input: Any


class ToolRegistry:
    def __init__(self, sandbox_dir: Optional[Path] = None):
        self._tools: Dict[str, Callable[[Any], str]] = {}
        self._sandbox_dir = sandbox_dir

    def register(self, name: str, fn: Callable[[Any], str]) -> None:
        self._tools[name] = fn

    def has(self, name: str) -> bool:
        return name in self._tools

    def execute(self, name: str, tool_input: Any) -> str:
        fn = self._tools.get(name)
        if fn is None:
            raise ToolError(f"unknown tool: {name}")
        return fn(tool_input)


def default_registry(*, sandbox_dir: Path) -> ToolRegistry:
    reg = ToolRegistry(sandbox_dir=sandbox_dir)

    def _calc(inp: Any) -> str:
        if isinstance(inp, dict) and "expr" in inp:
            return _safe_eval_arithmetic(str(inp["expr"]))
        return _safe_eval_arithmetic(str(inp))

    def _read_file(inp: Any) -> str:
        if isinstance(inp, dict) and "path" in inp:
            p = str(inp["path"])
        else:
            p = str(inp)
        return _read_sandbox_file(sandbox_dir, p)

    reg.register("calc", _calc)
    reg.register("read_file", _read_file)
    return reg
