import inspect
import contextlib
import functools
from typing import TYPE_CHECKING, Callable, Any, Union, Iterable
from aworld.trace.base import (
    AttributeValueType
)

from aworld.trace.stack_info import get_filepath_attribute
from aworld.trace.constants import (
    ATTRIBUTES_MESSAGE_TEMPLATE_KEY
)

if TYPE_CHECKING:
    from aworld.trace.context_manager import TraceManager, ContextSpan


def trace_func(trace_manager: "TraceManager",
               msg_template: str = None,
               attributes: dict[str, AttributeValueType] = None,
               span_name: str = None,
               extract_args: Union[bool, Iterable[str]] = False):
    """A decorator that traces the execution of a function.

    Args:
        trace_manager: The trace manager to use.
        msg_template: The message template to use.
        attributes: The attributes to use.
        span_name: The span name to use.
        extract_args: Whether to extract arguments from the function call.

    Returns:
        The decorated function.
    """

    def decorator(func: Callable) -> Callable:
        func_meta = get_function_meta(func, msg_template)
        func_meta.update(attributes or {})
        final_span_name = span_name or func_meta.get(ATTRIBUTES_MESSAGE_TEMPLATE_KEY) or func.__name__

        if inspect.isgeneratorfunction(func):
            def wrapper(*args, **kwargs):
                with open_func_span(trace_manager, func_meta, final_span_name,
                                    get_func_args(func, extract_args, *args, **kwargs)):
                    for item in func(*args, **kwargs):
                        yield item
        elif inspect.isasyncgenfunction(func):
            async def wrapper(*args, **kwargs):
                with open_func_span(trace_manager, func_meta, final_span_name,
                                    get_func_args(func, extract_args, *args, **kwargs)):
                    async for item in func(*args, **kwargs):
                        yield item
        elif inspect.iscoroutinefunction(func):
            async def wrapper(*args, **kwargs):
                with open_func_span(trace_manager, func_meta, final_span_name,
                                    get_func_args(func, extract_args, *args, **kwargs)):
                    return await func(*args, **kwargs)
        else:
            def wrapper(*args, **kwargs):
                with open_func_span(trace_manager, func_meta, final_span_name,
                                    get_func_args(func, extract_args, *args, **kwargs)):
                    return func(*args, **kwargs)

        wrapper = functools.wraps(func)(wrapper)  # type: ignore
        return wrapper

    return decorator


def open_func_span(trace_manager: "TraceManager",
                   func_meta: dict[str, AttributeValueType],
                   span_name: str,
                   func_args: dict[str, AttributeValueType]):
    """Open a function span.

    Args:
        func_meta: The function meta information.
        span_name: The span name.

    Returns:
        The function span.
    """
    func_meta.update(func_args)
    return trace_manager._create_auto_span(name=span_name, attributes=func_meta)


def get_func_args(func: Callable,
                  extract_args: Union[bool, Iterable[str]] = False,
                  *args,
                  **kwargs):
    """Get the arguments of a function.

    Args:
        func: The function to get the arguments of.
        extract_args: Whether to extract arguments from the function call.
        *args: The positional arguments.
        **kwargs: The keyword arguments.

    Returns:
        The arguments of the function.
    """
    func_sig = inspect.signature(func)
    if func_sig.parameters:
        func_args = func_sig.bind(*args, **kwargs).arguments
        if extract_args is not False:
            if isinstance(extract_args, bool):
                extract_args = func_sig.parameters.keys()
            func_args = {k: v for k, v in func_args.items() if k in extract_args}
        return func_args
    return {}


def get_function_meta(func: Any,
                      msg_template: str = None) -> dict[str, AttributeValueType]:
    """Get the meta information of a function.\

    Args:
        func: The function to get the meta information of.
        msg_template: The message template to use.

    Returns:
        The meta information of the function.
    """
    func = inspect.unwrap(func)
    if not inspect.isfunction(func) and hasattr(func, '__call__'):
        func = func.__call__
        func = inspect.unwrap(func)

    func_name = getattr(func, '__qualname__', getattr(func, '__name__', build_func_name(func)))
    if not msg_template:
        try:
            msg_template = f'Calling {inspect.getmodule(func).__name__}.{func_name}'  # type: ignore
        except Exception:  # pragma: no cover
            msg_template = f'Calling {func_name}'
    meta: dict[str, AttributeValueType] = {
        'code.function': func_name,
        ATTRIBUTES_MESSAGE_TEMPLATE_KEY: msg_template,
    }
    with contextlib.suppress(Exception):
        meta['code.lineno'] = func.__code__.co_firstlineno
    with contextlib.suppress(Exception):
        # get code.filepath
        meta.update(get_filepath_attribute(inspect.getsourcefile(func)))

    func_sig = inspect.signature(func)
    if func_sig.parameters:
        meta['func.args'] = [str(param) for param in func_sig.parameters.values()
                             if param.name != 'self']
    return meta


def build_func_name(func: Any) -> str:
    """Build the function name.

    Args:
        func: The function to build the name of.

    Returns:
        The function name.
    """
    try:
        result = repr(func)
    except Exception:
        result = f'<{type(func).__name__} object>'

    return result
