import logging
import threading
from enum import Enum
from logging import Logger

from messageprocessor import MessageProcessorCallback
from typing import (
    Dict,
    List,
    Optional,
    Type,
)

default_logger = logging.getLogger("MessageProcessor.LoggingProcessorCallback")


class LogConfig(object):
    def __init__(
        self,           # type: LogConfig
        method,         # type: Optional[CallBackMethod]
        event_name,     # type: Optional[str],
        log_level,      # type: int
    ):
        if method is None and event_name is None:
            raise RuntimeError("Either method or event_name must be set")

        self.method = method
        self.event_name = event_name
        self.log_level = log_level

class CallBackMethod(Enum):
    process_begun = "process_begun"
    input_parsed = "input_parsed"
    business_called = "business_called"
    process_finished = "process_finished"
    parse_exception = "parse_exception"
    unhandled_exception = "unhandled_exception"


class LoggingProcessorCallback(MessageProcessorCallback):

    def __init__(
        self,                           # type: LoggingProcessorCallback
        logger=None,                    # type: Optional[Logger]
        ignored_exceptions=None,        # type: Optional[List[Type[Exception]]]
        log_level_exceptions=None,      # type: Optional[Dict[Type[BaseException], int]]
        custom_config=None,             # type: List[LogConfig]
    ):
        # type: (...) -> None

        self.logger = logger if logger is not None else default_logger
        self.ignored_exceptions = set()
        if ignored_exceptions is not None:
            for ex in ignored_exceptions:
                self.ignored_exceptions.add(ex)
        self.log_level_exceptions = log_level_exceptions or {}
        self.per_method_config = {}
        for log_config in custom_config or []:
            if log_config.method is not None and log_config.event_name is not None:
                self.per_method_config[(log_config.method, log_config.event_name)] = log_config.log_level
            if log_config.method is not None:
                self.per_method_config[log_config.method] = log_config.log_level
            if log_config.event_name is not None:
                self.per_method_config[log_config.event_name] = log_config.log_level

        self.local_storage = threading.local()

    def process_begun(self, event_name, request_id, data=None):
        # type: (str, str, bytes) -> None
        self.local_storage.original_thread_name = threading.current_thread().name
        self._set_thread_name(event_name, request_id)

        self._log(
            method=CallBackMethod.process_begun,
            event_name=event_name,
            default_level=logging.DEBUG,
            msg=u"Start handling. Data: " + self._build_str_from_data(data),
        )

    def input_parsed(self, event_name, request_id, input_obj):
        self._set_thread_name(event_name, request_id)

        self._log(
            method=CallBackMethod.input_parsed,
            event_name=event_name,
            default_level=logging.DEBUG,
            msg=u"Input correctly parsed",
        )

    def business_called(self, event_name, request_id, input_obj, output_obj):
        self._log(
            method=CallBackMethod.business_called,
            event_name=event_name,
            default_level=logging.DEBUG,
            msg=u"Business called",
        )

    def process_finished(self, event_name, request_id, input_obj, output_obj):
        self._log(
            method=CallBackMethod.process_finished,
            event_name=event_name,
            default_level=logging.INFO,
            msg=u"Successfully processed",
        )

        self._restore_thread_name()

    def parse_exception(self, event_name, request_id, data, exception):
        self._log(
            method=CallBackMethod.parse_exception,
            event_name=event_name,
            default_level=self._get_exception_log_level(exception=exception),
            msg=u"Parse exception handling",
        )

        self._restore_thread_name()

    def unhandled_exception(self, event_name, request_id, input_obj, exception):
        self._log(
            method=CallBackMethod.unhandled_exception,
            event_name=event_name,
            default_level=self._get_exception_log_level(exception=exception),
            msg=u"Unhandled exception handling",
        )

        self._restore_thread_name()

    @staticmethod
    def _set_thread_name(event_name, request_id):
        threading.current_thread().name = "MessageProcessorThread${}${}".format(event_name, request_id)

    def _restore_thread_name(self):
        if hasattr(self.local_storage, "original_thread_name"):
            threading.current_thread().name = self.local_storage.original_thread_name

    def _log_exception(self, exception, log_level, message):
        type_exception = type(exception)

        if type_exception in self.ignored_exceptions:
            return

        level = self.log_level_exceptions.get(type_exception, log_level)
        self.logger.log(level, message, exc_info=True)

    def _log(
        self,               # type: LoggingProcessorCallback
        method,             # type: CallBackMethod
        event_name,         # type: str
        default_level,      # type: int
        msg,                # type: str
    ):

        log_level = self.per_method_config.get(
            (method, event_name),
            self.per_method_config.get(
                event_name,
                self.per_method_config.get(
                    method,
                    default_level,
                )
            )
        )

        if log_level != logging.NOTSET:
            self.logger.log(log_level, msg)

    def _get_exception_log_level(self, exception):
        type_exception = type(exception)

        if type_exception in self.ignored_exceptions:
            return logging.NOTSET

        return self.log_level_exceptions.get(type_exception, logging.ERROR)

    @staticmethod
    def _build_str_from_data(data):
        if data is None:
            return u"[None]"

        if isinstance(data, bytes):
            try:
                return data.decode("utf-8")
            except UnicodeDecodeError:
                return data.hex()

        return str(data)
