"""Framework-agnostic tool definitions for Cadence SDK.

This module provides the UvTool class and @uvtool decorator for defining
tools that can be used across different orchestration frameworks.
"""

import asyncio
import inspect
from dataclasses import dataclass
from typing import Any, Callable, Dict, List, Optional, Union

from pydantic import BaseModel

DEFAULT_CACHE_TTL_SECONDS = 3600
DEFAULT_SIMILARITY_THRESHOLD = 0.85
DESCRIPTION_PREVIEW_LENGTH = 50


@dataclass
class StreamConfig:
    enabled: bool = True


@dataclass
class ToolResult:
    input: Optional[dict[str, Any]]
    output: Optional[Any]
    required_validate: bool = False


@dataclass
class CacheConfig:
    """Cache configuration for a UvTool.

    Attributes:
        enabled: Whether caching is enabled for this tool
        ttl: Time-to-live in seconds for cached results (default 3600 = 1 hour)
        similarity_threshold: Similarity threshold (0.0-1.0) for semantic matching
            (default 0.85). Higher values require closer matches.
        cache_key_fields: Optional list of parameter names to use for cache key.
            If None, all parameters are used.
    """

    enabled: bool = True
    ttl: int = DEFAULT_CACHE_TTL_SECONDS
    similarity_threshold: float = DEFAULT_SIMILARITY_THRESHOLD
    cache_key_fields: Optional[List[str]] = None


class UvTool:
    """Framework-agnostic tool wrapper.

    A UvTool wraps a callable function and provides metadata for
    tool discovery and invocation across different orchestration frameworks.

    Attributes:
        name: Tool name (used for invocation)
        description: Human-readable tool description
        func: The underlying callable function
        args_schema: Optional Pydantic model defining the tool's arguments
        cache: Optional cache configuration for semantic caching
        metadata: Additional metadata
        is_async: Whether the tool is async
    """

    def __init__(
        self,
        name: str,
        description: str,
        func: Callable,
        args_schema: Optional[type[BaseModel]] = None,
        cache: Optional[CacheConfig] = None,
        metadata: Optional[Dict[str, Any]] = None,
    ):
        self.name = name
        self.description = description
        self.func = func
        self.args_schema = args_schema
        self.cache = cache
        self.metadata = metadata or {}
        self.is_async = asyncio.iscoroutinefunction(func)

    def __call__(self, *args, **kwargs) -> Any:
        """Synchronous invocation.

        Raises:
            RuntimeError: If tool is async (use ainvoke instead)
        """
        if self.is_async:
            raise RuntimeError(
                f"Tool '{self.name}' is async. Use ainvoke() instead of direct call."
            )
        return self.func(*args, **kwargs)

    async def ainvoke(self, *args, **kwargs) -> Any:
        """Async invocation.

        For sync tools, runs in executor. For async tools, awaits directly.
        """
        if self.is_async:
            return await self.func(*args, **kwargs)
        loop = asyncio.get_event_loop()
        return await loop.run_in_executor(None, lambda: self.func(*args, **kwargs))

    def invoke(self, *args, **kwargs) -> Any:
        """Sync invocation alias."""
        return self(*args, **kwargs)

    def __repr__(self) -> str:
        return (
            f"UvTool(name='{self.name}', "
            f"description='{self.description[:DESCRIPTION_PREVIEW_LENGTH]}...', "
            f"is_async={self.is_async})"
        )


def uvtool(
    func: Optional[Callable] = None,
    *,
    name: Optional[str] = None,
    description: Optional[str] = None,
    args_schema: Optional[type[BaseModel]] = None,
    cache: Optional[Union[CacheConfig, bool, dict]] = None,
    stream: bool = False,
    **metadata,
) -> Union[Callable, UvTool]:
    """Decorator to convert a function into a UvTool.

    Can be used with or without parentheses:
        @uvtool
        def my_tool(query: str) -> str:
            '''Search for information.'''
            return search(query)

        @uvtool(name="custom_name", cache=True)
        async def async_tool(param: int) -> dict:
            return await some_async_operation(param)

        @uvtool(cache=CacheConfig(ttl=3600, cache_key_fields=["query"]))
        def cached_tool(query: str) -> str:
            '''Expensive operation.'''
            return expensive_search(query)

    Args:
        func: Function to decorate (when used without parentheses)
        name: Tool name (defaults to function name)
        description: Tool description (defaults to function docstring)
        args_schema: Optional Pydantic model for argument validation
        cache: Cache configuration (True/False, dict, or CacheConfig instance)
        **metadata: Additional metadata to store in tool.metadata

    Returns:
        UvTool instance (when used without parentheses) or
        decorator function (when used with parentheses)
    """

    def _create_tool_from_function(func: Callable) -> UvTool:
        tool_name = name if name is not None else func.__name__
        tool_description = _extract_description(func, tool_name, description)
        cache_config = _parse_cache_parameter(cache) if cache is not None else None
        tool = UvTool(
            name=tool_name,
            description=tool_description,
            func=func,
            args_schema=args_schema,
            cache=cache_config,
            metadata=dict(metadata),
        )
        _preserve_function_signature(tool, func)
        return tool

    if func is not None:
        return _create_tool_from_function(func)
    return _create_tool_from_function


def _extract_description(
    func: Callable, tool_name: str, explicit_description: Optional[str]
) -> str:
    if explicit_description is not None:
        return explicit_description
    docstring = inspect.getdoc(func)
    if docstring:
        return docstring.split("\n")[0].strip()
    return f"Tool: {tool_name}"


def _parse_cache_parameter(
    cache_param: Union[CacheConfig, bool, dict],
) -> Optional[CacheConfig]:
    if isinstance(cache_param, CacheConfig):
        return cache_param
    if isinstance(cache_param, bool):
        return CacheConfig(enabled=cache_param) if cache_param else None
    if isinstance(cache_param, dict):
        return CacheConfig(**cache_param)
    return None


def _preserve_function_signature(tool: UvTool, func: Callable) -> None:
    tool.__signature__ = inspect.signature(func)
    tool.__doc__ = func.__doc__
    tool.__name__ = func.__name__
    tool.__module__ = func.__module__
