"""Tool interface and registry (build plan Section 6.3).

A tool declares a name, description, JSON-schema parameters, a sensitivity flag,
and required scopes. The Phase 4 connector framework will register tools through
this same interface, so the agent loop never needs to know where a tool came
from. Tool outputs are always treated as untrusted data by the runner.
"""

from __future__ import annotations

import uuid
from collections.abc import Awaitable, Callable
from dataclasses import dataclass, field
from typing import TYPE_CHECKING

if TYPE_CHECKING:  # avoid importing heavy/optional deps at module load
    import httpx
    from sqlalchemy.orm import Session

    from legate.config import Settings
    from legate.rag.embeddings.base import Embedder
    from legate.rag.vectorstore.base import VectorStore


@dataclass
class ToolSpec:
    name: str
    description: str
    parameters: dict  # JSON schema (object)
    is_sensitive: bool = False
    scopes: list[str] = field(default_factory=list)

    def as_dict(self) -> dict:
        return {
            "name": self.name,
            "description": self.description,
            "parameters": self.parameters,
            "is_sensitive": self.is_sensitive,
            "scopes": list(self.scopes),
        }


@dataclass
class ToolResult:
    ok: bool
    data: dict = field(default_factory=dict)
    error: str | None = None


@dataclass
class ToolContext:
    """Execution context handed to a tool handler."""

    workspace_id: uuid.UUID
    settings: "Settings"
    db: "Session | None" = None
    http_client: "httpx.AsyncClient | None" = None
    embedder: "Embedder | None" = None
    vector_store: "VectorStore | None" = None


ToolHandler = Callable[[dict, ToolContext], Awaitable[ToolResult]]


@dataclass
class RegisteredTool:
    spec: ToolSpec
    handler: ToolHandler


class ToolRegistry:
    def __init__(self) -> None:
        self._tools: dict[str, RegisteredTool] = {}

    def register(self, spec: ToolSpec, handler: ToolHandler) -> None:
        if spec.name in self._tools:
            raise ValueError(f"tool already registered: {spec.name}")
        self._tools[spec.name] = RegisteredTool(spec=spec, handler=handler)

    def get(self, name: str) -> RegisteredTool | None:
        return self._tools.get(name)

    def specs(self) -> list[ToolSpec]:
        return [t.spec for t in self._tools.values()]

    def names(self) -> list[str]:
        return list(self._tools.keys())


_JSON_TYPES: dict[str, type | tuple[type, ...]] = {
    "string": str,
    "integer": int,
    "number": (int, float),
    "boolean": bool,
    "object": dict,
    "array": list,
}


def validate_args(schema: dict, args: dict) -> list[str]:
    """Minimal JSON-schema validation: required keys and top-level types.

    Returns a list of human-readable errors (empty means valid). Full schema
    validation lands with the connector framework in Phase 4.
    """
    errors: list[str] = []
    if not isinstance(args, dict):
        return ["arguments must be an object"]

    for key in schema.get("required", []):
        if key not in args:
            errors.append(f"missing required argument: {key}")

    properties: dict = schema.get("properties", {})
    for key, value in args.items():
        prop = properties.get(key)
        if not prop:
            continue
        expected = prop.get("type")
        py_type = _JSON_TYPES.get(expected) if expected else None
        if py_type is None:
            continue
        # bool is a subclass of int; reject it for integer/number.
        rejects_bool = expected in {"integer", "number"} and isinstance(value, bool)
        if rejects_bool or not isinstance(value, py_type):
            errors.append(f"argument '{key}' must be of type {expected}")
    return errors
