"""
@snowglobe_tool decorator for automatic tool mocking.
"""

import asyncio
import inspect
import logging
import os
from functools import wraps
from typing import Any, Callable, Dict

from .api import (
    ToolExecutionValidationError,
    fetch_mocked_tool_response_async,
    fetch_mocked_tool_response_sync,
)
from .context import ToolCallContext

LOGGER = logging.getLogger(__name__)
TOOL_TRACE_ENABLED = os.getenv("SNOWGLOBE_TOOL_TRACE", "false").lower() == "true"


def _trace(message: str) -> None:
    if TOOL_TRACE_ENABLED:
        print(message)


def _extract_arguments(args: tuple, kwargs: dict, func: Callable) -> Dict[str, Any]:
    """
    Extract function arguments as a dictionary.

    Args:
        args: Positional arguments
        kwargs: Keyword arguments
        func: The function being called

    Returns:
        Dictionary of argument name -> value
    """
    # Get function signature
    sig = inspect.signature(func)
    bound = sig.bind(*args, **kwargs)
    bound.apply_defaults()

    return dict(bound.arguments)


def _should_mock_tool() -> bool:
    """
    Determine if tool should be mocked based on environment and context.

    Priority order:
    1. Explicit user override (SNOWGLOBE_MOCK_TOOLS env var)
    2. Thread-local context (set by runner when in Snowglobe mode)
    3. Default to False

    Returns:
        True if tool should be mocked, False otherwise
    """
    # Check user override first
    user_override = os.getenv("SNOWGLOBE_MOCK_TOOLS")
    if user_override is not None:
        return user_override.lower() == "true"

    # Check thread-local context
    context = ToolCallContext.get_context()
    return context.get("mock_enabled", False)


def snowglobe_tool(func: Callable) -> Callable:
    """
    Decorator that enables automatic tool mocking in Snowglobe mode.

    When running via `snowglobe-connect start`, tool calls are automatically
    intercepted and mocked responses are fetched from Snowglobe servers.

    When running directly (e.g., `python my_agent.py`), tools execute normally.

    Supports both sync and async functions.

    Args:
        func: The tool function to decorate

    Returns:
        Wrapped function with mocking capability

    Example:
        @snowglobe_tool
        def get_weather(location: str, unit: str = "fahrenheit") -> str:
            # This will be mocked when running via snowglobe-connect
            return weather_api.get(location, unit)

        @snowglobe_tool
        async def search_database(query: str) -> str:
            # Async tools are also supported
            return await db.search(query)
    """

    # Check if function is async
    is_async = asyncio.iscoroutinefunction(func)

    if is_async:
        # Async wrapper
        @wraps(func)
        async def async_wrapper(*args, **kwargs):
            # Check if mocking is enabled
            if not _should_mock_tool():
                # Execute real function
                _trace(f"\n🔧 [{func.__name__}] Executing REAL tool")
                _trace(f"   Args: {args}")
                _trace(f"   Kwargs: {kwargs}")
                result = await func(*args, **kwargs)
                _trace(
                    f"   ✓ Result: {result[:100] if isinstance(result, str) and len(result) > 100 else result}..."
                )
                return result

            # Get context
            context = ToolCallContext.get_context()
            conversation_id = context.get("conversation_id")
            test_id = context.get("test_id")

            # Extract arguments
            try:
                arguments = _extract_arguments(args, kwargs, func)
            except Exception as e:
                LOGGER.error(
                    f"Failed to extract arguments for tool '{func.__name__}': {e}"
                )
                # Fall back to real execution
                return await func(*args, **kwargs)

            _trace(f"\n🎭 [{func.__name__}] Calling MOCKED tool")
            _trace(f"   Arguments: {arguments}")
            _trace(f"   Context: conversation_id={conversation_id}, test_id={test_id}")

            # If context is missing, fail fast
            if not conversation_id or not test_id:
                message = f"Tool '{func.__name__}' called with mocking enabled but missing context."
                _trace(f"   ❌ {message}")
                LOGGER.error(message)
                raise RuntimeError(message)

            # Fetch mocked response
            try:
                mocked_response = await fetch_mocked_tool_response_async(
                    tool_name=func.__name__,
                    arguments=arguments,
                    test_id=test_id,
                    application_id=context.get("application_id"),
                )

                # If mocked response is None (404), fail fast
                if mocked_response is None:
                    message = f"No mock configured for tool '{func.__name__}'."
                    _trace(f"   ❌ {message}")
                    LOGGER.error(message)
                    raise RuntimeError(message)

                _trace("   ✓ Mocked response received:")
                _trace(
                    f"      {mocked_response[:200] if len(mocked_response) > 200 else mocked_response}..."
                )
                LOGGER.debug(f"Using mocked response for tool '{func.__name__}'")
                return mocked_response

            except ToolExecutionValidationError:
                raise
            except Exception as e:
                _trace(f"   ❌ Error fetching mock: {e}")
                LOGGER.error(
                    f"Error fetching mocked response for tool '{func.__name__}': {e}"
                )
                raise

        return async_wrapper

    else:
        # Sync wrapper
        @wraps(func)
        def sync_wrapper(*args, **kwargs):
            # Check if mocking is enabled
            if not _should_mock_tool():
                # Execute real function
                _trace(f"\n🔧 [{func.__name__}] Executing REAL tool")
                _trace(f"   Args: {args}")
                _trace(f"   Kwargs: {kwargs}")
                result = func(*args, **kwargs)
                _trace(
                    f"   ✓ Result: {result[:100] if isinstance(result, str) and len(result) > 100 else result}..."
                )
                return result

            # Get context
            context = ToolCallContext.get_context()
            conversation_id = context.get("conversation_id")
            test_id = context.get("test_id")

            # Extract arguments
            try:
                arguments = _extract_arguments(args, kwargs, func)
            except Exception as e:
                LOGGER.error(
                    f"Failed to extract arguments for tool '{func.__name__}': {e}"
                )
                # Fall back to real execution
                return func(*args, **kwargs)

            _trace(f"\n🎭 [{func.__name__}] Calling MOCKED tool")
            _trace(f"   Arguments: {arguments}")
            _trace(f"   Context: conversation_id={conversation_id}, test_id={test_id}")

            # If context is missing, fail fast
            if not conversation_id or not test_id:
                message = f"Tool '{func.__name__}' called with mocking enabled but missing context."
                _trace(f"   ❌ {message}")
                LOGGER.error(message)
                raise RuntimeError(message)

            # Fetch mocked response
            try:
                mocked_response = fetch_mocked_tool_response_sync(
                    tool_name=func.__name__,
                    arguments=arguments,
                    test_id=test_id,
                    application_id=context.get("application_id"),
                )

                # If mocked response is None (404), fail fast
                if mocked_response is None:
                    message = f"No mock configured for tool '{func.__name__}'."
                    _trace(f"   ❌ {message}")
                    LOGGER.error(message)
                    raise RuntimeError(message)

                _trace("   ✓ Mocked response received:")
                _trace(
                    f"      {mocked_response[:200] if len(mocked_response) > 200 else mocked_response}..."
                )
                LOGGER.debug(f"Using mocked response for tool '{func.__name__}'")
                return mocked_response

            except ToolExecutionValidationError:
                raise
            except Exception as e:
                _trace(f"   ❌ Error fetching mock: {e}")
                LOGGER.error(
                    f"Error fetching mocked response for tool '{func.__name__}': {e}"
                )
                raise

        return sync_wrapper
