import functools
import inspect
import logging
import time
import traceback
from contextvars import ContextVar

from .utils import to_async

__all__ = ['catch_print_swallow_exc', 'log_execution_time', 'log_call']


def catch_print_swallow_exc(func):
    if inspect.iscoroutinefunction(func):
        @functools.wraps(func)
        async def wrapper(*args, **kwargs):
            try:
                return await func(*args, **kwargs)
            except:
                traceback.print_exc()
        return wrapper
    else:
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            try:
                return func(*args, **kwargs)
            except:
                traceback.print_exc()
        return wrapper



def log_execution_time(func=None, *, level=logging.WARNING):
    def decorator(f):
        if inspect.iscoroutinefunction(f):
            @functools.wraps(f)
            async def wrapper(*args, **kwargs):
                start = time.perf_counter()
                try:
                    return await f(*args, **kwargs)
                except:
                    traceback.print_exc()
                finally:
                    logging.log(level, f"---- {f.__name__} executed in {time.perf_counter() - start:.6f} seconds")
            return wrapper
        else:
            @functools.wraps(f)
            def wrapper(*args, **kwargs):
                start = time.perf_counter()
                try:
                    return f(*args, **kwargs)
                except:
                    traceback.print_exc()
                finally:
                    logging.log(level, f"---- {f.__name__} executed in {time.perf_counter() - start:.6f} seconds")
            return wrapper

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



_call_indent: ContextVar[int] = ContextVar('call_indent', default=0)


def log_call(level=logging.INFO, logger=logging.getLogger()):
    def actual_decorator(func):
        if inspect.iscoroutinefunction(func):
            @functools.wraps(func)
            async def wrapper(*args, **kwargs):
                indent = _call_indent.get()
                i = ' ' * (indent * 8)
                token = _call_indent.set(indent + 1)
                logger.log(level, f"{i}{func.__name__} Start")
                try:
                    res = await func(*args, **kwargs)
                    logger.log(level, f"{i}{func.__name__} Finish")
                    return res
                except:
                    logger.error(f"{i}{func.__name__} Error")
                    raise
                finally:
                    _call_indent.reset(token)
            return wrapper
        else:
            @functools.wraps(func)
            def wrapper(*args, **kwargs):
                indent = _call_indent.get()
                i = ' ' * (indent * 8)
                token = _call_indent.set(indent + 1)
                logger.log(level, f"{i}{func.__name__} Start")
                try:
                    res = func(*args, **kwargs)
                    logger.log(level, f"{i}{func.__name__} Finish")
                    return res
                except:
                    logger.error(f"{i}{func.__name__} Error")
                    raise
                finally:
                    _call_indent.reset(token)
            return wrapper

    return actual_decorator
