import logging
import sys
import traceback
import unicodedata
from logging import Logger
import time

from rich.console import Console


STACK_TRACE_OCCURRENCE_THRESHOLD = 1


class LogUtil(object):
    def __init__(self, logger: Logger, quiet: bool = False):
        self._logger = logger
        self._quiet = quiet
        self._console = Console(file=sys.stderr)
        self._exception_counter = {str: int}

    def print_and_log(self, msg: str, level=logging.INFO, **kwargs):
        """
        Prints msg to console (if `quiet` param is False) and logs msg to main log file.
        Exceptions to get logged should be passed as the named parameter "err"
        """
        err = kwargs.get("err", None)
        if err is not None:
            kwargs.pop(
                "err"
            )  # remove from the dictionary. print() below is not expecting it.

        if not self._quiet:
            self._console.print(msg + self.format_exception(err), **kwargs)
        LogUtil.log_message(
            self._logger, msg + self.format_exception(err), level, self._logger
        )

    def log(self, msg: str, level=logging.INFO, err=None):
        LogUtil.log_message(
            self._logger, msg + self.format_exception(err), level, self._logger
        )

    def timer(self, name: str, log_level: int = logging.DEBUG):
        return C42Timer(self, name, log_level)

    @staticmethod
    def log_message(
        logger: Logger, msg: str, level=logging.INFO, error_logger: Logger = None
    ):
        try:
            logger.log(level, msg)

        except UnicodeEncodeError:
            if error_logger is not None:
                # log the raw bytes at debug
                byte_data = bytearray(msg, "utf-8")
                byte_hex_str = "".join("{:02x}".format(x) for x in byte_data)
                error_logger.log(logging.DEBUG, byte_hex_str)

            # perhaps if we normalize the string, we will be able to log it
            normalized_message = unicodedata.normalize("NFC", msg)
            logger.log(level, normalized_message)

    def format_exception(self, err):
        if err is None:
            return ""

        error_type = type(err).__name__
        if error_type not in self._exception_counter:
            self._exception_counter[error_type] = 0

        stack_trace_details = ""
        count = self._exception_counter[error_type]
        if count < STACK_TRACE_OCCURRENCE_THRESHOLD:
            stack_trace_details += "\n"
            stack_trace_details += traceback.format_exc()

        self._exception_counter[error_type] += 1
        return f" Error=[{type(err).__name__} - {err}]{stack_trace_details}"


class C42Timer(object):
    def __init__(self, log_util: LogUtil, name: str, log_level: int = logging.DEBUG):
        self._log_util = log_util
        self._name = name
        self._log_level = log_level

    def __enter__(self):
        self.start = time.time()

    def __exit__(self, exc_type, exc_value, traceback):
        self.stop = time.time()
        self._log_util.print_and_log(
            "{0} took {1:.2f} seconds".format(self._name, (self.stop - self.start)),
            self._log_level,
        )
