"""Tool Calling Utilities — helpers for LLM function calling.

Provides utilities for:
- Getting tool schemas as LLMToolSpec Pydantic models
- Deterministic safe_name <-> original_name mapping
- Executing tool calls via the registry

ALL PYDANTIC — LLMToolSpec models, ToolCall models, ToolExecutionResultItem models.
"""

from __future__ import annotations

import json
import logging
from typing import TYPE_CHECKING

from core.pathways.models.tool import (
    ToolArgs,
    ToolCall,
    ToolCallPlan,
    ToolExecutionResult,
    ToolResult,
)
from core.pathways.nodes.models import ToolExecutionResultItem
from core.tools import get_tool_registry
from core.types.llm import LLMFunctionParameters, LLMFunctionSpec, LLMToolSpec

if TYPE_CHECKING:
    from core.pathways.context import Context

logger = logging.getLogger(__name__)

# Type alias for the safe_name -> original_name reverse map.
ToolNameMap = dict[str, str]


def get_tool_schemas_for_names(
    tool_names: list[str],
    ctx: Context,
) -> tuple[list[LLMToolSpec], ToolNameMap]:
    """Get LLMToolSpec models for the given tool names.

    Supports wildcards like "pathway.*" to include all pathway tools.

    Returns a ``(schemas, name_map)`` tuple where ``name_map`` maps each
    sanitised name (dots replaced with underscores) back to the original
    registry name.  This is the single source of truth for the reverse
    mapping — no heuristic needed.

    Parameters
    ----------
    tool_names
        List of tool names or patterns (e.g., ``["web.*", "pathway.create"]``).
    ctx
        Execution context with tools and definitions.
    """
    # Get tool definitions from global registry
    registry = get_tool_registry()
    tool_definitions = {
        td.name: {"description": td.description, "parameters": td.parameters}
        for td in registry.list()
    }

    # Get available tools from both context and registry
    available = sorted({*ctx.tools.keys(), *tool_definitions.keys()})

    # Expand wildcards
    expanded_names: list[str] = []
    for name in tool_names:
        if name.endswith(".*"):
            prefix = name[:-2]
            expanded_names.extend(t for t in available if t.startswith(f"{prefix}."))
        elif name == "*":
            expanded_names.extend(available)
        elif name in available:
            expanded_names.append(name)

    # Build schemas as Pydantic models + deterministic reverse map
    schemas: list[LLMToolSpec] = []
    name_map: ToolNameMap = {}

    for name in expanded_names:
        safe_name = name.replace(".", "_")
        name_map[safe_name] = name

        if (
            tool_definitions
            and name in tool_definitions
            and isinstance(tool_definitions[name], dict)
        ):
            defn = tool_definitions[name]
            raw_params = defn.get("parameters") or {
                "type": "object",
                "properties": {},
                "required": [],
            }

            # Build typed parameter model
            if isinstance(raw_params, dict):
                params = LLMFunctionParameters.model_validate(raw_params)
            else:
                params = LLMFunctionParameters()

            schema = LLMToolSpec(
                function=LLMFunctionSpec(
                    name=safe_name,
                    description=str(defn.get("description", "") or ""),
                    parameters=params,
                ),
            )
            schemas.append(schema)
        else:
            # Fallback to minimal schema
            schemas.append(
                LLMToolSpec(
                    function=LLMFunctionSpec(
                        name=safe_name,
                        description=f"Call the {name} tool",
                        parameters=LLMFunctionParameters(),
                    ),
                ),
            )

    return schemas, name_map


async def execute_tool_calls(
    tool_calls: list[ToolCall],
    ctx: Context,
    name_map: ToolNameMap | None = None,
) -> list[ToolExecutionResultItem]:
    """Execute tool calls via the global registry.

    Parameters
    ----------
    tool_calls
        Tool calls to execute (names may be sanitised).
    ctx
        Execution context.
    name_map
        Deterministic ``safe_name -> original_name`` map from
        ``get_tool_schemas_for_names``.  Falls back to using the
        tool name as-is when the map is ``None`` or the name is
        not found.
    """
    results: list[ToolExecutionResultItem] = []
    registry = get_tool_registry()
    _map = name_map or {}

    for tc in tool_calls:
        tool_name = _map.get(tc.tool, tc.tool)
        raw_args = tc.args
        if isinstance(raw_args, str):
            raw_args = json.loads(raw_args)
        tool_args = ToolArgs.model_validate(raw_args or {})

        out = await registry.call(tool_name, tool_args, ctx)
        results.append(
            ToolExecutionResultItem(
                tool=tool_name,
                success=out.success,
                output=out.data,
                error=out.error,
            ),
        )

    return results


__all__ = [
    # DTOs
    "ToolCall",
    "ToolCallPlan",
    "ToolExecutionResult",
    "ToolNameMap",
    "ToolResult",
    # Utilities
    "execute_tool_calls",
    "get_tool_schemas_for_names",
]
