"""Tool abstractions and registry."""

from __future__ import annotations

import json
import math
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import Any


class ToolValidationError(ValueError):
    """Raised when tool arguments fail local validation."""


@dataclass(slots=True)
class ToolResult:
    """Structured tool execution result."""

    ok: bool
    data: Any = None
    error: dict[str, Any] | None = None
    meta: dict[str, Any] = field(default_factory=dict)

    def as_dict(self, *, name: str, arguments: dict[str, Any]) -> dict[str, Any]:
        payload: dict[str, Any] = {
            "tool": name,
            "arguments": arguments,
            "ok": self.ok,
        }
        if self.data is not None:
            payload["data"] = self.data
        if self.error is not None:
            payload["error"] = self.error
        if self.meta:
            payload["meta"] = self.meta
        return payload


class Tool(ABC):
    """Base tool definition."""

    name: str
    description: str
    parameters: dict[str, Any]

    @abstractmethod
    def execute(self, arguments: dict[str, Any]) -> ToolResult:
        """Execute a tool call."""

    def validate_arguments(self, arguments: dict[str, Any]) -> None:
        validate_object_schema(arguments, self.parameters)

    def schema(self) -> dict[str, Any]:
        return {
            "type": "function",
            "function": {
                "name": self.name,
                "description": self.description,
                "parameters": self.parameters,
            },
        }

    @staticmethod
    def success(
        data: Any = None,
        *,
        meta: dict[str, Any] | None = None,
    ) -> ToolResult:
        return ToolResult(ok=True, data=data, meta=meta or {})

    @staticmethod
    def failure(
        code: str,
        message: str,
        *,
        data: Any = None,
        meta: dict[str, Any] | None = None,
    ) -> ToolResult:
        return ToolResult(
            ok=False,
            data=data,
            error={"code": code, "message": message},
            meta=meta or {},
        )


@dataclass(slots=True)
class ToolRegistry:
    """Dynamic tool registry."""

    _tools: dict[str, Tool] = field(default_factory=dict)

    def register(self, tool: Tool) -> None:
        self._tools[tool.name] = tool

    def copy(self) -> "ToolRegistry":
        clone = ToolRegistry()
        clone._tools = dict(self._tools)
        return clone

    def schemas(self) -> list[dict[str, Any]]:
        return [tool.schema() for tool in self._tools.values()]

    def summaries(self) -> list[str]:
        return [f"{tool.name}: {tool.description}" for tool in self._tools.values()]

    def execute(self, name: str, arguments: dict[str, Any]) -> ToolResult:
        tool = self._tools.get(name)
        if tool is None:
            return Tool.failure(
                "tool_not_registered",
                f"Tool '{name}' is not registered.",
            )
        try:
            tool.validate_arguments(arguments)
            return tool.execute(arguments)
        except ToolValidationError as exc:
            return Tool.failure("validation_error", str(exc))
        except Exception as exc:
            return Tool.failure(
                "execution_error",
                f"Tool '{name}' failed: {exc}",
                meta={"exception_type": type(exc).__name__},
            )

    @staticmethod
    def stringify_result(
        name: str,
        arguments: dict[str, Any],
        result: ToolResult,
    ) -> str:
        return json.dumps(
            result.as_dict(name=name, arguments=arguments),
            ensure_ascii=False,
            indent=2,
        )


def paginate_items(
    items: list[Any],
    *,
    page: int,
    page_size: int,
) -> tuple[list[Any], dict[str, Any]]:
    """Return one page from a list-like payload."""
    if page < 1:
        raise ValueError("page must be greater than or equal to 1")
    if page_size < 1:
        raise ValueError("page_size must be greater than or equal to 1")
    total_items = len(items)
    total_pages = max(1, math.ceil(total_items / page_size)) if total_items else 1
    if page > total_pages:
        raise ValueError(
            f"page {page} is out of range; valid pages are 1..{total_pages}"
        )
    start = (page - 1) * page_size
    end = start + page_size
    return items[start:end], {
        "page": page,
        "page_size": page_size,
        "total_pages": total_pages,
        "total_items": total_items,
        "has_next_page": page < total_pages,
        "has_previous_page": page > 1,
    }


def paginate_text(
    text: str,
    *,
    page: int,
    page_size: int,
) -> tuple[str, dict[str, Any]]:
    """Return one page from a text payload."""
    if page < 1:
        raise ValueError("page must be greater than or equal to 1")
    if page_size < 1:
        raise ValueError("page_size must be greater than or equal to 1")
    total_chars = len(text)
    total_pages = max(1, math.ceil(total_chars / page_size)) if total_chars else 1
    if page > total_pages:
        raise ValueError(
            f"page {page} is out of range; valid pages are 1..{total_pages}"
        )
    start = (page - 1) * page_size
    end = start + page_size
    return text[start:end], {
        "page": page,
        "page_size": page_size,
        "total_pages": total_pages,
        "total_chars": total_chars,
        "has_next_page": page < total_pages,
        "has_previous_page": page > 1,
    }


def validate_object_schema(arguments: dict[str, Any], schema: dict[str, Any]) -> None:
    """Validate a small JSON-schema-like object payload."""
    if not isinstance(arguments, dict):
        raise ToolValidationError("arguments must be an object")

    issues: list[str] = []
    properties = schema.get("properties") or {}
    required = schema.get("required") or []

    for key in required:
        if key not in arguments:
            issues.append(f"missing required field '{key}'")

    if schema.get("additionalProperties") is False:
        unknown = sorted(key for key in arguments if key not in properties)
        if unknown:
            issues.append(
                "unexpected fields: " + ", ".join(f"'{key}'" for key in unknown)
            )

    for key, value in arguments.items():
        property_schema = properties.get(key)
        if property_schema is None:
            continue
        _validate_value(path=key, value=value, schema=property_schema, issues=issues)

    if issues:
        raise ToolValidationError("; ".join(issues))


def _validate_value(
    *,
    path: str,
    value: Any,
    schema: dict[str, Any],
    issues: list[str],
) -> None:
    expected_type = schema.get("type")
    if expected_type == "string":
        if not isinstance(value, str):
            issues.append(f"field '{path}' must be a string")
            return
        min_length = schema.get("minLength")
        if isinstance(min_length, int) and len(value) < min_length:
            issues.append(
                f"field '{path}' must be at least {min_length} characters long"
            )
    elif expected_type == "integer":
        if isinstance(value, bool) or not isinstance(value, int):
            issues.append(f"field '{path}' must be an integer")
            return
        minimum = schema.get("minimum")
        if isinstance(minimum, int) and value < minimum:
            issues.append(f"field '{path}' must be greater than or equal to {minimum}")
        maximum = schema.get("maximum")
        if isinstance(maximum, int) and value > maximum:
            issues.append(f"field '{path}' must be less than or equal to {maximum}")
    elif expected_type == "object":
        if not isinstance(value, dict):
            issues.append(f"field '{path}' must be an object")
            return

    enum_values = schema.get("enum")
    if isinstance(enum_values, list) and value not in enum_values:
        issues.append(
            f"field '{path}' must be one of: "
            + ", ".join(repr(item) for item in enum_values)
        )
