from .logger import Logger
from .types import Types
from .context import Context
from .defaults import Defaults

from functools import wraps
import inspect
from typing import Callable, Any



# Helpers #####

def __get_logger_and_cleanup_params(
    func: Callable,
    args: tuple[Any,...],
    kwargs: dict[str,Any]
) -> tuple[Logger|None,tuple[Any,...],dict[str,Any]]:
    args_ = list(args)
    kwargs_ = dict(kwargs)

    logger_ = None
    func_signature = inspect.signature(func)

    # search in kwargs_
    if logger_ is None:
        for k, v in list(kwargs_.items()):
            if isinstance(v, Logger):
                logger_ = v
                #
                if k not in func_signature.parameters.keys():
                    del kwargs_[k]
                #
                break

    # search in args
    if logger_ is None:
        params_signature = list(func_signature.parameters.values())

        for i, v in enumerate(args_):
            if isinstance(v, Logger):
                logger_ = v
                #
                found = False
                if (i < len(params_signature)
                    and (params_signature[i].annotation is Logger
                        or 'logger' in params_signature[i].name.lower()
                    )
                ):
                    found = True
                #
                if not found:
                    args_.pop(i)
                #
                break

    # search in context
    if logger_ is None:
        logger_ = Context.logger

    # search in defaults
    if logger_ is None:
        # default Logger -> set 1st time
        if Defaults.logger is None:
            Defaults.logger = Logger()

        logger_ = Defaults.logger

    return logger_, tuple(args_), kwargs_


def __function__begin(logger: Logger, type_: str, func: Callable, prefix: str):
    logger.log(
        obj=f'{func.__qualname__}: {{',
        prefix=f'{prefix}.' if prefix is not None else '',
        type_=type_, date_=True, indent=True
    )
    logger.indent += 1
    pass


def __function__end(logger: Logger, type_: str):
    logger.indent -= 1
    logger.log(
        obj='}',
        type_=type_, date_=False, indent=True
    )
    pass


def __params__begin(logger: Logger, type_: str, name: str):
    logger.log(
        obj=f'{name}: {{',
        type_=type_, date_=False, indent=True
    )
    logger.indent += 1
    pass


def __param(logger: Logger, type_: str, name: str, value: Any):
    try:
        logger.log(
            obj=value,
            prefix=name,
            type_=type_, date_=False, indent=True
        )
    except Exception as exc:
        logger.log(
            obj=f'warning: Not initialized! > {exc}',
            prefix=name,
            type_=type_, date_=False, indent=True
        )
    pass


def __params__end(logger: Logger, type_: str):
    logger.indent -= 1
    logger.log(
        obj='}',
        type_=type_, date_=False, indent=True
    )
    pass


def __trace__begin(
    logger: Logger,
    func: Callable,
    prefix: str,
    args: tuple[Any,...],
    kwargs: dict[str,Any]
):
    """ Logs function entry and all parameters. """
    __function__begin(logger=logger, type_=Types.debug, func=func, prefix=prefix)

    # args
    if len(args) > 0:
        __params__begin(logger=logger, type_=Types.debug, name='args')
        for i, v in enumerate(args):
            __param(logger=logger, type_=Types.debug, name=f'[{i}]: ', value=v)
        __params__end(logger=logger, type_=Types.debug)

    # kwargs
    if len(kwargs) > 0:
        __params__begin(logger=logger, type_=Types.debug, name='kwargs')
        for k, v in kwargs.items():
            __param(logger=logger, type_=Types.debug, name=f'[{k}]: ', value=v)
        __params__end(logger=logger, type_=Types.debug)
    pass


def __trace__exception(
    logger: Logger,
    func: Callable,
    prefix: str,
    args: tuple[Any,...],
    kwargs: dict[str,Any],
    exc: Exception
):
    """ Specialized logging for when things go wrong. """
    __function__begin(logger=logger, type_=Types.error, func=func, prefix=prefix)

    # args
    if len(args) > 0:
        __params__begin(logger=logger, type_=Types.error, name='args')
        for i, v in enumerate(args):
            __param(logger=logger, type_=Types.error, name=f'[{i}]: ', value=v)
        __params__end(logger=logger, type_=Types.error)

    # kwargs
    if len(kwargs) > 0:
        __params__begin(logger=logger, type_=Types.error, name='kwargs')
        for k, v in kwargs.items():
            __param(logger=logger, type_=Types.error, name=f'{k}: ', value=v)
        __params__end(logger=logger, type_=Types.error)

    __param(logger=logger, type_=Types.error, name='exc:', value=exc)
    __function__end(logger=logger, type_=Types.error)

    # fix: close the 'exec' block from debug trace that was left open
    __params__end(logger=logger, type_=Types.debug)
    pass


def __execute__sync(
    logger: Logger,
    func: Callable,
    prefix: str,
    *args, **kwargs
) -> Any:
    """ Executes logging logic for standard functions. """
    try:
        __trace__begin(logger=logger, func=func, prefix=prefix, args=args, kwargs=kwargs)

        __params__begin(logger=logger, type_=Types.debug, name='exec')
        result = func(*args, **kwargs)
        __params__end(logger=logger, type_=Types.debug)

        __param(logger=logger, type_=Types.debug, name='result: ', value=result)
        return result

    except Exception as exc:
        __trace__exception(logger=logger, func=func, prefix=prefix, args=args, kwargs=kwargs, exc=exc)
        raise

    finally:
        __function__end(logger=logger, type_=Types.debug)
    pass


async def __execute__async(
    logger: Logger,
    func: Callable,
    prefix: str,
    *args, **kwargs
) -> Any:
    """ Executes logging logic for async functions (coroutines). """
    try:
        __trace__begin(logger=logger, func=func, prefix=prefix, args=args, kwargs=kwargs)

        __params__begin(logger=logger, type_=Types.debug, name='exec')
        result = await func(*args, **kwargs)
        __params__end(logger=logger, type_=Types.debug)

        __param(logger=logger, type_=Types.debug, name='result: ', value=result)
        return result

    except Exception as exc:
        __trace__exception(logger=logger, func=func, prefix=prefix, args=args, kwargs=kwargs, exc=exc)
        raise

    finally:
        __function__end(logger=logger, type_=Types.debug)
    pass



# Decorator #####

def logging(func: Callable = None, prefix: str = None) -> Any:
    """
    Universal decorator for logging called function, params and errors.

    :param func: called function
    :param prefix: prefix (__name__ - file name to be used, if None -> function.class.name)
    :return:
    """

    def wrapper(func_: Callable):
        if not inspect.iscoroutinefunction(func_):
            @wraps(func_)
            def wrapper__sync(*args, **kwargs):
                logger_, args_, kwargs_ = __get_logger_and_cleanup_params(func=func_, args=args, kwargs=kwargs)
                wrapper__sync.logger = logger_
                return __execute__sync(logger_, func_, prefix, *args_, **kwargs_)
            return wrapper__sync
        else:
            @wraps(func_)
            async def wrapper__async(*args, **kwargs):
                logger_, args_, kwargs_ = __get_logger_and_cleanup_params(func=func_, args=args, kwargs=kwargs)
                wrapper__async.logger = logger_
                return await __execute__async(logger_, func_, prefix, *args_, **kwargs_)
            return wrapper__async

    if func is None:
        return wrapper

    return wrapper(func_=func)
