"""
Agent executor utility for calling agents with different interfaces.

This module provides utilities to execute agents regardless of their implementation:
- Agent class instances
- Synchronous agent functions (str -> str, list[dict] -> str, AgentInput -> AgentResponse)
- Asynchronous agent functions (same signatures, async)

The executor normalizes all responses to AgentResponse objects.
"""

import asyncio
import inspect
from typing import Any, List, Optional, Union

from galtea.utils.agent import (
    Agent,
    AgentInput,
    AgentResponse,
    AgentType,
    ChatHistory,
    ConversationMessage,
)


def is_async_callable(obj: Any) -> bool:
    """Check if an object is an async callable (coroutine function).

    Handles both regular async functions and callable objects with async __call__.

    Args:
        obj: The object to check.

    Returns:
        bool: True if the object is an async callable, False otherwise.

    Example:
        ```python
        async def my_async_func():
            pass

        def my_sync_func():
            pass

        is_async_callable(my_async_func)  # True
        is_async_callable(my_sync_func)   # False
        ```
    """
    if asyncio.iscoroutinefunction(obj):
        return True

    # Check for callable objects with async __call__ method
    # callable(obj) guarantees __call__ exists
    if callable(obj):
        return asyncio.iscoroutinefunction(obj.__call__)

    return False


def _expects_agent_input(func: Any) -> bool:
    """Check if an agent function expects an AgentInput parameter.

    Inspects the function signature to determine if the first parameter
    has a type hint of `AgentInput`, which indicates the structured
    callable signature that mirrors Agent.call().

    Args:
        func: The agent function to inspect.

    Returns:
        bool: True if the agent function expects AgentInput, False otherwise.
    """
    try:
        sig = inspect.signature(func)
        params = list(sig.parameters.values())
        if not params:
            return False

        first_param = params[0]
        hint = first_param.annotation

        if hint is inspect.Parameter.empty:
            return False

        if hint is AgentInput:
            return True

        return False
    except (ValueError, TypeError):
        return False


def _expects_string_input(func: Any) -> bool:
    """Check if an agent function expects a single string parameter (vs chat history).

    Inspects the function signature to determine the expected input type.
    If the first parameter has a type hint of `str`, returns True.
    If the first parameter has a type hint of `list` or similar, returns False.
    If no type hints, defaults to False (assumes chat history format).

    Args:
        func: The agent function to inspect.

    Returns:
        bool: True if the agent function expects a single string, False otherwise.
    """
    try:
        sig = inspect.signature(func)
        params = list(sig.parameters.values())
        if not params:
            return False

        first_param = params[0]
        hint = first_param.annotation

        if hint is inspect.Parameter.empty:
            return False

        # Check if it's a string type
        if hint is str:
            return True

        # Handle string types from typing module
        origin = getattr(hint, "__origin__", None)
        if origin is None and hint is str:
            return True

        return False
    except (ValueError, TypeError):
        return False


def _run_async_callable(callable_agent: Any, arg: Union[str, ChatHistory, AgentInput]) -> Any:
    """Execute an async agent function and return the result.

    Handles the complexity of running async code from sync context,
    including when already inside an event loop.

    Args:
        callable_agent: The async agent function to execute.
        arg: The argument to pass to the agent function (string, chat history, or AgentInput).

    Returns:
        Any: The result from the agent function (str, dict, or AgentResponse).
    """
    try:
        loop = asyncio.get_running_loop()
    except RuntimeError:
        loop = None

    if loop is not None:
        # Already in an event loop - run in a separate thread
        import concurrent.futures

        with concurrent.futures.ThreadPoolExecutor() as executor:
            future = executor.submit(asyncio.run, callable_agent(arg))
            return future.result()
    else:
        # No event loop - can run directly
        return asyncio.run(callable_agent(arg))


def _normalize_callable_result(result: Any) -> AgentResponse:
    """Normalize an agent function result to AgentResponse.

    Args:
        result: The result from an agent function (str, dict, or AgentResponse).

    Returns:
        AgentResponse: The normalized response.

    Raises:
        ValueError: If the result type is not supported.
    """
    if isinstance(result, str):
        return AgentResponse(content=result)
    elif isinstance(result, dict):
        return AgentResponse(**result)
    elif isinstance(result, AgentResponse):
        return result
    else:
        raise ValueError(f"Agent function must return str, dict, or AgentResponse, got {type(result).__name__}")


def _convert_messages_to_dict(messages: List[ConversationMessage]) -> ChatHistory:
    """Convert ConversationMessage list to chat history dict format.

    Args:
        messages: List of ConversationMessage objects.

    Returns:
        ChatHistory: List of dicts with 'role' and 'content' keys.
    """
    return [{"role": m.role, "content": m.content} for m in messages]


def execute_agent(
    agent: AgentType,
    messages: List[ConversationMessage],
    session_id: str,
    inference_result_id: Optional[str] = None,
) -> AgentResponse:
    """Execute any agent type and return a normalized AgentResponse.

    This function handles the complexity of different agent interfaces:
    - Agent class: Calls agent.call() with AgentInput
    - Agent function (str -> str): Passes last user message as string
    - Agent function (list[dict] -> str): Passes full chat history
    - Agent function (AgentInput -> AgentResponse): Passes structured input
    - Async versions of all agent function signatures

    Args:
        agent: The agent to execute (Agent class instance or agent function).
        messages: The conversation messages.
        session_id: The session ID for context.
        inference_result_id: Optional inference result ID for trace correlation.
            Passed through to AgentInput so remote agents can forward it.

    Returns:
        AgentResponse: The normalized response from the agent.

    Raises:
        ValueError: If the agent type is not supported or returns invalid data.

    Example:
        ```python
        # Works with Agent class
        response = execute_agent(MyAgent(), messages, session_id)

        # Works with agent function (str -> str)
        response = execute_agent(lambda msg: f"Echo: {msg}", messages, session_id)

        # Works with agent function (list[dict] -> str)
        response = execute_agent(lambda msgs: msgs[-1]["content"], messages, session_id)

        # Works with structured agent function (AgentInput -> AgentResponse)
        response = execute_agent(structured_agent_func, messages, session_id)

        # Works with async agent function
        response = execute_agent(async_agent_func, messages, session_id)
        ```
    """
    # Handle Agent class instances
    if isinstance(agent, Agent):
        agent_input = AgentInput(messages=messages, session_id=session_id, inference_result_id=inference_result_id)
        return agent.call(agent_input)

    # Handle agent functions (sync or async)
    if callable(agent):
        # Determine input format based on function signature
        if _expects_agent_input(agent):
            # Structured agent function (AgentInput -> AgentResponse): pass AgentInput
            agent_input = AgentInput(messages=messages, session_id=session_id, inference_result_id=inference_result_id)
            arg: Union[str, ChatHistory, AgentInput] = agent_input
        elif _expects_string_input(agent):
            # Agent function (str -> str): pass last user message as string
            last_user_message = next((m.content for m in reversed(messages) if m.role == "user"), "")
            arg = last_user_message
        else:
            # Agent function (list[dict] -> str): pass full chat history as list of dicts
            arg = _convert_messages_to_dict(messages)

        if is_async_callable(agent):
            result = _run_async_callable(agent, arg)
        else:
            result = agent(arg)

        return _normalize_callable_result(result)

    raise ValueError(f"agent must be an Agent class instance or an agent function, got {type(agent).__name__}")
