import contextlib
import functools
import logging
from typing import Any, Callable, Dict, Iterator, Optional, Tuple, Type, TypeVar, cast

from .toolz import assoc

DEBUG2_LEVEL_NUM = 8

TLogger = TypeVar("TLogger", bound=logging.Logger)


class cached_show_debug2_property:
    def __init__(self, func: Callable[[TLogger], bool]):
        # type ignored b/c arg1 expects Callable[..., Any]
        functools.update_wrapper(self, func)  # type: ignore
        self._func = func

    def __get__(self, obj: Optional[TLogger], cls: Type[logging.Logger]) -> Any:
        if obj is None:
            return self

        result = self._func(obj)
        obj.__dict__[self._func.__name__] = result
        return result


class ExtendedDebugLogger(logging.Logger):
    """
    Logging class that can be used for lower level debug logging.
    """

    @cached_show_debug2_property
    def show_debug2(self) -> bool:
        return self.isEnabledFor(DEBUG2_LEVEL_NUM)

    def debug2(self, message: str, *args: Any, **kwargs: Any) -> None:
        if self.show_debug2:
            self.log(DEBUG2_LEVEL_NUM, message, *args, **kwargs)
        else:
            # When we find that `DEBUG2` isn't enabled we completely replace
            # the `debug2` function in this instance of the logger with a noop
            # lambda to further speed up
            self.__dict__["debug2"] = lambda message, *args, **kwargs: None


def setup_DEBUG2_logging() -> None:
    """
    Installs the `DEBUG2` level logging levels to the main logging module.
    """
    if not hasattr(logging, "DEBUG2"):
        logging.addLevelName(DEBUG2_LEVEL_NUM, "DEBUG2")
        setattr(logging, "DEBUG2", DEBUG2_LEVEL_NUM)  # typing: ignore


@contextlib.contextmanager
def _use_logger_class(logger_class: Type[logging.Logger]) -> Iterator[None]:
    original_logger_class = logging.getLoggerClass()
    logging.setLoggerClass(logger_class)
    try:
        yield
    finally:
        logging.setLoggerClass(original_logger_class)


def get_logger(name: str, logger_class: Type[TLogger] = None) -> TLogger:
    if logger_class is None:
        return cast(TLogger, logging.getLogger(name))
    else:
        with _use_logger_class(logger_class):
            # The logging module caches logger instances.  The following code
            # ensures that if there is a cached instance that we don't
            # accidentally return the incorrect logger type because the logging
            # module does not *update* the cached instance in the event that
            # the global logging class changes.
            #
            # types ignored b/c mypy doesn't identify presence of manager on logging.Logger
            manager = logging.Logger.manager  # type: ignore
            if name in manager.loggerDict:
                if type(manager.loggerDict[name]) is not logger_class:
                    del manager.loggerDict[name]
            return cast(TLogger, logging.getLogger(name))


def get_extended_debug_logger(name: str) -> ExtendedDebugLogger:
    return get_logger(name, ExtendedDebugLogger)


THasLoggerMeta = TypeVar("THasLoggerMeta", bound="HasLoggerMeta")


class HasLoggerMeta(type):
    """
    Metaclass which using the `__qualname__` to assign a logger instance to the
    class who's name corresponds to the import path for the class.
    """

    logger_class = logging.Logger

    def __new__(
        mcls: Type[THasLoggerMeta],
        name: str,
        bases: Tuple[Type[Any]],
        namespace: Dict[str, Any],
    ) -> type:
        if "logger" in namespace:
            # If a logger was explicitly declared we shouldn't do anything to
            # replace it.
            return super().__new__(mcls, name, bases, namespace)
        if "__qualname__" not in namespace:
            raise AttributeError("Missing __qualname__")

        with _use_logger_class(mcls.logger_class):
            logger = logging.getLogger(namespace["__qualname__"])

        return super().__new__(mcls, name, bases, assoc(namespace, "logger", logger))

    @classmethod
    def replace_logger_class(
        mcls: Type[THasLoggerMeta], value: Type[logging.Logger]
    ) -> Type[THasLoggerMeta]:
        return type(mcls.__name__, (mcls,), {"logger_class": value})

    @classmethod
    def meta_compat(
        mcls: Type[THasLoggerMeta], other: Type[type]
    ) -> Type[THasLoggerMeta]:
        return type(mcls.__name__, (mcls, other), {})


class _BaseHasLogger(metaclass=HasLoggerMeta):
    # This class exists to a allow us to define the type of the logger.  Once
    # python3.5 is deprecated this can be removed in favor of a simple type
    # annotation on the main class.
    logger = logging.Logger("")  # type: logging.Logger


class HasLogger(_BaseHasLogger):
    pass


HasExtendedDebugLoggerMeta = HasLoggerMeta.replace_logger_class(ExtendedDebugLogger)


class _BaseHasExtendedDebugLogger(metaclass=HasExtendedDebugLoggerMeta):  # type: ignore
    # This class exists to a allow us to define the type of the logger.  Once
    # python3.5 is deprecated this can be removed in favor of a simple type
    # annotation on the main class.
    logger = ExtendedDebugLogger("")  # type: ExtendedDebugLogger


class HasExtendedDebugLogger(_BaseHasExtendedDebugLogger):
    pass
