import logging
import os
import signal
import sys
import threading
from logging.config import dictConfig
from typing import Callable, Any, NoReturn

from clarabot_nano.common.config.constants import CommonConfigConstants
from clarabot_nano.utility import parse_environment_flag


class CustomCallbackHandler(logging.Handler):
    """
    A handler class that calls a custom callback. This is used to send error notifications to the control process.
    """

    def __init__(self, callback: Callable[[str], None], level=logging.ERROR):
        logging.Handler.__init__(self, level)
        self._callback = callback

    def emit(self, record):
        try:
            msg = self.format(record)
            self._callback(msg)
        except RecursionError:  # See issue 36272
            raise
        except Exception:
            self.handleError(record)

    def __repr__(self):
        level = logging.getLevelName(self.level)
        return "<%s (%s)>" % (self.__class__.__name__, level)


def unhandled_main_exc(exc_cls, value, tb):
    logging.getLogger().critical("Unhandled main exception", exc_info=(exc_cls, value, tb))


def unhandled_thread_exc(args):
    if args.thread:
        msg = f"Unhandled thread exception ({args.thread.name})"
    else:
        msg = "Unhandled thread exception"
    logging.getLogger().critical(msg, exc_info=(args.exc_type, args.exc_value, args.exc_traceback))


def setup_named_process_log_record(process_name, with_pid=True, extra_prefix=""):
    logging.logMultiprocessing = False

    if with_pid:
        process_name = f"{extra_prefix}{process_name:<4}|{os.getpid():>5}"

        class NamedProcessLogRecord(logging.LogRecord):
            def __init__(self, *args, **kwargs):
                logging.LogRecord.__init__(self, *args, **kwargs)
                self.processName = process_name

    else:
        process_name = f"{extra_prefix}{process_name:<10}"

        class NamedProcessLogRecord(logging.LogRecord):
            def __init__(self, *args, **kwargs):
                logging.LogRecord.__init__(self, *args, **kwargs)
                self.processName = process_name

    logging.setLogRecordFactory(NamedProcessLogRecord)


class LoggingBootstrap(object):
    """
    We put these bootstrap methods in a class, so they are easy to monkey-patch for testing.
    """

    @staticmethod
    def setup_logging_from_config(config):
        dictConfig(config)
        if parse_environment_flag("CBNANO_DEBUG"):
            # set debug level on every logger we configured
            for key in config["loggers"].keys():
                logging.getLogger(key).setLevel(logging.DEBUG)

    @staticmethod
    def setup_logging(config, process_name, with_pid=True):
        setup_named_process_log_record(process_name, with_pid, "dev-" if CommonConfigConstants.DEVELOPMENT else "")
        LoggingBootstrap.setup_logging_from_config(config)

        # log unhandled errors
        sys.original_excepthook = sys.excepthook
        sys.excepthook = unhandled_main_exc
        threading.original_excepthook = threading.excepthook
        threading.excepthook = unhandled_thread_exc

    @staticmethod
    def setup_custom_sigint_sigterm_handler(fn: Callable[[int, Any], NoReturn]) -> NoReturn:
        """
        Signals are completely different on POSIX and Windows. See docs of clarabot_nano.utility.subprocess
        """
        signal.signal(signal.SIGINT, fn)
        if sys.platform.startswith("win"):
            signal.signal(signal.SIGBREAK, fn)
        else:
            signal.signal(signal.SIGTERM, fn)
