import inspect
import json
import logging
import os
import random
import string
import sys
import threading
import traceback

import haggis.logs
from python_sdk_remote.mini_logger import MiniLogger
from python_sdk_remote.utilities import our_get_env, get_environment_name, obfuscate_log_dict
from user_context_remote.user_context import UserContext

from .Component import Component
from .Fields import Fields
from .LoggerOutputEnum import LoggerOutputEnum
from .MessageSeverity import MessageSeverity
from .SendToLogzIo import SendToLogzIo
from .Writer import Writer
from .debug_mode import DebugMode

logzio_token = our_get_env("LOGZIO_TOKEN")
logzio_url = "https://listener.logz.io:8071"
COMPUTER_LANGUAGE = "Python"
loggers = {}
mandatory_fields_by_class = {}  # used by the MetaLogger
_writer = Writer()  # global writer

# TODO: ipv4 ipv6
# TODO: another severity for custom end
# TODO: save records on buffer before sending to sql to improve performance


# TODO Add optional parameter to logger.start()
#  which called api_call, so logger.start will call api_management
#  to insert into api_call_table all fields including session_id.

session_length = 30
session = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(session_length))
MiniLogger.info(f"Session: {session}")


class Logger(logging.Logger):
    @staticmethod
    def __get_base_logger(mandatory_fields, **kwargs) -> None:
        # This method is called by the metaclass to get the mandatory fields from the base classes in inheritance
        # This is needed because the metaclass is called before the __init__ method
        # Alternatively, we can return a logging.getLogger(), as those are not used in such cases,
        # but we might miss cases where the user forget to provide the mandatory fields
        mandatory_fields_to_cache = {k: v for k, v in kwargs['object'].items() if k in mandatory_fields}
        if mandatory_fields_to_cache and 'class' in kwargs.get('object', {}):
            mandatory_fields_by_class[kwargs['object']['class']] = mandatory_fields_to_cache
        if 'bases' in kwargs.get('object', {}):
            for base in kwargs['object']['bases']:
                cached_mandatory_fields = mandatory_fields_by_class.get(base.__name__, {})
                if cached_mandatory_fields:
                    kwargs['object'].update(cached_mandatory_fields)
                    break

            if not all(k in kwargs.get('object', {}) for k in mandatory_fields):
                # (I am not so sure if it is needed)
                for base in kwargs['object']['bases']:
                    if hasattr(base, 'logger'):  # if some base has logger attribute, use its mandatory fields
                        kwargs['object'].update({k: v for k, v in base.logger.additional_fields.items()})

    @staticmethod
    def create_logger(**kwargs):
        mandatory_fields = ('component_id', 'component_name', 'component_category', 'developer_email')
        Logger.__get_base_logger(mandatory_fields, **kwargs)

        if not all(k in kwargs.get('object', {}) for k in mandatory_fields):
            raise Exception("Please insert component_id, component_name, component_category and developer_email "
                            f"in the object sent to the logger (not {kwargs.get('object', {})})")

        # get cached logger if exists or create a new one
        component_id = int(kwargs['object']['component_id'])
        component_category = kwargs['object']['component_category']
        unique_logger_key = (component_id, component_category)

        if unique_logger_key in loggers and not kwargs.get('ignore_cached_logger', False):
            return loggers.get(unique_logger_key)
        else:
            logger = Logger(**kwargs)
            loggers[unique_logger_key] = logger
            return logger

    def __init__(self, *,
                 handler: logging.Handler = logging.StreamHandler(stream=sys.stdout),
                 formatter: logging.Formatter = None,
                 level: int or str = None,
                 **kwargs) -> None:
        self._is_write_to_sql = False  # Overide LOGGER_IS_WRITE_TO_SQL manually

        self.debug_mode = DebugMode(logger_minimum_severity=level)
        self.component_id = int(kwargs['object']['component_id'])
        self.logger_table_fields = {}
        self.update_logger_table_fields()
        self.user_context = None
        self.additional_fields = kwargs['object'].copy()
        self.additional_fields["session"] = session

        self.logger = self.initiate_logger(handler=handler, formatter=formatter,
                                           level=self.debug_mode.logger_minimum_severity)
        self.logger.name = kwargs['object']['component_name']

        super().__init__(name=self.logger.name)

    @staticmethod
    def initiate_logger(*,
                        handler: logging.Handler = logging.StreamHandler(stream=sys.stdout),
                        formatter: logging.Formatter = None,
                        level: int = None) -> logging.Logger:

        # logging levels: INFO = 20, DEBUG = 10
        haggis.logs.add_logging_level(level_name="VERBOSE", level_num=logging.DEBUG + 1, method_name="verbose")
        haggis.logs.add_logging_level(level_name="INIT", level_num=logging.DEBUG + 2, method_name="init")
        haggis.logs.add_logging_level(level_name="START", level_num=logging.DEBUG + 3, method_name="start")
        haggis.logs.add_logging_level(level_name="END", level_num=logging.DEBUG + 4, method_name="end")

        if not formatter:
            if isinstance(handler, logging.StreamHandler) and stream_supports_colour(handler.stream):
                formatter = _ColourFormatter()
            else:
                dt_fmt = '%H:%M:%S'  # '%Y-%m-%d %H:%M:%S'
                formatter = logging.Formatter('[{asctime}] [{levelname:<8}]: {message}', dt_fmt, style='{')
        handler.setFormatter(formatter)
        logger = logging.getLogger(__name__)
        logger.propagate = False  # Prevents the log messages from being displayed multiple times.

        our_levels_to_logging = {
            MessageSeverity.Debug: logging.DEBUG,
            MessageSeverity.Verbose: logging.VERBOSE,  # noqa
            MessageSeverity.Init: logging.INIT,  # noqa
            MessageSeverity.Start: logging.START,  # noqa
            MessageSeverity.End: logging.END,  # noqa
            MessageSeverity.Information: logging.INFO,
            MessageSeverity.Warning: logging.WARNING,
            MessageSeverity.Error: logging.ERROR,
            MessageSeverity.Exception: logging.ERROR,
            MessageSeverity.Critical: logging.CRITICAL,
        }
        # Debug = 100, Verbose = 200. If level = 101 we want the one that is bigger than 101, i.e. Verbose
        level = min(our_levels_to_logging.keys(),
                    key=lambda x: x.value if x.value >= level else MessageSeverity.Debug.value)

        logger.setLevel(our_levels_to_logging[MessageSeverity(level)])
        logger.addHandler(handler)
        return logger

    def __log(self, *, function, message_severity: MessageSeverity, log_message, **kwargs):
        kwargs = kwargs or {}
        if isinstance(kwargs.get('object'), Exception):
            kwargs['object'] = {"exception": kwargs['object']}
        kwargs = obfuscate_log_dict(kwargs)
        depth = 3 if not "is_meta_logger" in kwargs else 4  # 1 for the meta wrapper
        extra_kwargs = {}
        if 'extra_kwargs' in kwargs.get('object', {}):  # from meta logger
            extra_kwargs = kwargs['object']['extra_kwargs']
            kwargs['object'].pop('extra_kwargs', None)
        function_name = kwargs.get('object', {}).get('function_name', extra_kwargs.get(
            'path', self.get_current_function_name(depth=depth)))
        if self.debug_mode.is_logger_output(component_id=self.component_id, logger_output=LoggerOutputEnum.Console,
                                            severity_level=message_severity.value):
            if log_message or kwargs:  # filter empty logger.start() and logger.end()
                log_with_function = function_name + ((" " + log_message) if log_message else "")
                function(log_with_function +
                         (" | " if log_with_function and kwargs else "") +
                         ("kwargs=" + str(kwargs.get('object', kwargs)) if kwargs else ""))

        log_object = {
            'severity_id': message_severity.value,
            'severity_name': message_severity.name
        }
        if log_message:
            log_object['log_message'] = log_message

        if isinstance(kwargs.get('object', {}).get('exception'), Exception):
            exception = kwargs['object']['exception']
            stack_trace = traceback.format_exception(type(exception), exception, exception.__traceback__)
            del kwargs['object']['exception']
            kwargs['object']['error_stack'] = str(stack_trace)
            kwargs['object']['severity_id'] = MessageSeverity.Exception.value
            kwargs['object']["is_assertion_error"] = isinstance(exception, AssertionError)

        if 'object' not in kwargs:
            kwargs['object'] = {}
        kwargs['object'].update(log_object)
        if extra_kwargs:
            kwargs['object'].update(extra_kwargs)
        kwargs = self.insert_to_payload_extra_vars(**kwargs)
        self.insert_to_object(**kwargs)

        kwargs['object']['function_name'] = function_name

        if "locals_before_exception" in kwargs['object']:
            kwargs['object']['locals_before_exception'] = json.dumps({
                k: str(v) for k, v in kwargs['object']['locals_before_exception'].items()})

        if self._is_write_to_sql or self.debug_mode.is_logger_output(component_id=self.component_id,
                                                                     logger_output=LoggerOutputEnum.MySQLDatabase,
                                                                     severity_level=message_severity.value):
            _writer.add_message_and_payload(str(log_message), **kwargs)
        if self.debug_mode.is_logger_output(component_id=self.component_id, logger_output=LoggerOutputEnum.Logzio,
                                            severity_level=message_severity.value):
            SendToLogzIo.send_to_logzio(kwargs['object'])

    def init(self, log_message: str = None, **kwargs):
        # TODO: Why do we need it?
        self.__log(function=self.logger.init,  # noqa
                   message_severity=MessageSeverity.Init, log_message=log_message, **kwargs)

    def start(self, log_message: str = None, **kwargs):
        self.__log(function=self.logger.start,  # noqa
                   message_severity=MessageSeverity.Start, log_message=log_message, **kwargs)

    def end(self, log_message: str = None, **kwargs):
        self.__log(function=self.logger.end,  # noqa
                   message_severity=MessageSeverity.End, log_message=log_message, **kwargs)

    def info(self, log_message: str = None, **kwargs):
        self.__log(function=self.logger.info, message_severity=MessageSeverity.Information,
                   log_message=log_message, **kwargs)

    def error(self, log_message: str = None, **kwargs):
        self.__log(function=self.logger.error, message_severity=MessageSeverity.Error, log_message=log_message,
                   **kwargs)

    def warning(self, log_message: str = None, **kwargs):
        self.__log(function=self.logger.warning, message_severity=MessageSeverity.Warning, log_message=log_message,
                   **kwargs)

    def debug(self, log_message: str = None, **kwargs):
        self.__log(function=self.logger.debug, message_severity=MessageSeverity.Debug, log_message=log_message,
                   **kwargs)

    def critical(self, log_message: str = None, **kwargs):
        self.__log(function=self.logger.critical, message_severity=MessageSeverity.Critical, log_message=log_message,
                   **kwargs)

    def verbose(self, log_message: str = None, **kwargs):
        self.__log(function=self.logger.verbose,  # noqa
                   message_severity=MessageSeverity.Verbose, log_message=log_message, **kwargs)

    def exception(self, log_message: str = None, **kwargs):
        """This method should be called only in tests / top level functions / api calls.
        (not when invoking `raise` after catching an exception,
            otherwise the traceback will be wrong and logged multiple times)
        """
        exception_object = None
        if 'object' in kwargs:
            if isinstance(kwargs['object'], Exception):
                exception_object = kwargs['object']
            elif 'exception' in kwargs['object']:
                exception_object = kwargs['object']['exception']
        # We use logger.error because we don't want to print the traceback multiple times.
        self.__log(function=self.logger.error, message_severity=MessageSeverity.Exception, log_message=log_message,
                   **kwargs)
        if exception_object:
            raise exception_object
        else:
            raise Exception(log_message)

    def _insert_variables(self, **kwargs):
        object_data = kwargs.get("object", {})
        self.logger_table_fields.update(object_data)
        self.additional_fields.update(object_data)

    def insert_to_object(self, **kwargs):
        object_data = kwargs.get("object", {})
        object_data.update({field: field_value for field, field_value in self.logger_table_fields.items()
                            if field_value is not None})

    def update_logger_table_fields(self) -> None:
        # LOGGER_IS_WRITE_TO_SQL can be True, False or None.
        logger_table_fields = Fields.get_logger_table_fields()
        if logger_table_fields:
            for field in logger_table_fields:
                self.logger_table_fields[field] = None

    def clean_variables(self):
        for field in self.logger_table_fields:
            self.logger_table_fields[field] = None
        self.additional_fields.clear()

    def insert_to_payload_extra_vars(self, **kwargs):
        try:
            self.user_context = UserContext()
        except Exception as exception:
            MiniLogger.exception("Error while trying to login using user identification and password", exception)
        message = kwargs['object'].pop('message', None)
        depth = 4 if not "is_meta_logger" in kwargs else 5  # 1 for the meta wrapper
        kwargs['object']['filename'] = kwargs['object'].get('filename', self.get_filename(depth=depth))
        kwargs['object']['path'] = kwargs['object'].get('path', self.get_path(depth=depth))
        kwargs['object']['class_name'] = kwargs['object'].get('class_name', self.get_calling_class(depth=depth))
        kwargs['object']['line_number'] = self.get_calling_line_number(depth=depth)
        kwargs['object']['environment'] = get_environment_name()
        kwargs['object']['computer_language'] = COMPUTER_LANGUAGE
        kwargs['object']['thread_id'] = threading.get_native_id()
        kwargs['object']['process_id'] = os.getpid()

        if self.user_context is not None:
            kwargs['object']['real_name'] = self.user_context.get_real_name()
            kwargs['object']['user_identifier'] = our_get_env("PRODUCT_USER_IDENTIFIER")
            kwargs['object']['created_effective_profile_id'] = self.user_context.get_effective_profile_id()
            kwargs['object']['created_effective_user_id'] = self.user_context.get_effective_user_id()
            kwargs['object']['created_real_user_id'] = self.user_context.get_real_user_id()
            kwargs['object']['created_user_id'] = self.user_context.get_real_user_id()
            kwargs['object']['updated_effective_profile_id'] = self.user_context.get_effective_profile_id()
            kwargs['object']['updated_effective_user_id'] = self.user_context.get_effective_user_id()
            kwargs['object']['updated_real_user_id'] = self.user_context.get_real_user_id()
            kwargs['object']['updated_user_id'] = self.user_context.get_real_user_id()

        kwargs['object'].update({field: field_value for field, field_value in self.logger_table_fields.items()
                                 if field_value is not None})
        kwargs['object'].update(self.additional_fields)
        component_json = self.get_component_json(self.component_id)
        if component_json:
            for field in component_json.keys():
                if field not in kwargs['object']:
                    field_value = component_json[field]
                    if field_value is not None:
                        kwargs['object'][field] = field_value
        if message is not None:
            kwargs['object']['message'] = message
        object_data = kwargs.get("object", {})
        object_data["record"] = json.dumps({key: str(value) for key, value in object_data.items()
                                            if key not in self.logger_table_fields})
        object_data = {key: value for key, value in object_data.items() if key in self.logger_table_fields}
        kwargs["object"] = object_data
        return kwargs

    @staticmethod
    def get_filename(depth: int) -> str:
        return os.path.basename(Logger.get_path(depth))

    @staticmethod
    def get_path(depth: int) -> str:
        return inspect.stack()[depth].filename

    @staticmethod
    def get_current_function_name(depth: int):
        stack = inspect.stack()
        # 0 = 'get_current_function_name', 1 = '__log', 2 = start/end/info...
        caller_frame = stack[depth]
        function_name = caller_frame.function
        return function_name

    @staticmethod
    def get_calling_class(depth: int) -> str:
        stack = inspect.stack()
        calling_module = inspect.getmodule(stack[depth].frame)
        return calling_module.__name__ if calling_module else None

    @staticmethod
    def get_calling_line_number(depth: int) -> int:
        stack = inspect.stack()
        calling_frame = stack[depth]
        return calling_frame.lineno

    def get_component_json(self, component_id: int) -> dict:
        component_json = Component.get_details_by_component_id(component_id)
        self.logger_table_fields.update(component_json)
        return component_json

    def is_component_complete(self):
        return (getattr(self, 'component_name') is None
                or getattr(self, 'component_type') is None
                or getattr(self, 'component_category') is None
                or getattr(self, 'testing_framework') is None
                or getattr(self, 'api_type') is None
                )


# Copyright: https://github.com/Rapptz/discord.py/blob/master/discord/utils.py#L1241
def is_docker() -> bool:
    path = '/proc/self/cgroup'
    return os.path.exists('/.dockerenv') or (os.path.isfile(path) and any('docker' in line for line in open(path)))


def stream_supports_colour(stream) -> bool:
    # Pycharm and Vscode support colour in their inbuilt editors
    colors_in_logs = our_get_env("LOGGER_COLORS_IN_LOGS", "")
    if colors_in_logs.lower() == "true":
        return True
    elif colors_in_logs.lower() == "false":
        return False

    if 'PYCHARM_HOSTED' in os.environ or os.environ.get('TERM_PROGRAM') == 'vscode':
        return True

    is_a_tty = hasattr(stream, 'isatty') and stream.isatty()  # TTY = terminal
    if sys.platform != 'win32':
        # Docker does not consistently have a tty attached to it
        return is_a_tty or is_docker()

    # ANSICON checks for things like ConEmu
    # WT_SESSION checks if this is Windows Terminal
    return is_a_tty or ('ANSICON' in os.environ or 'WT_SESSION' in os.environ)


class _ColourFormatter(logging.Formatter):
    # ANSI codes are a bit weird to decipher if you're unfamiliar with them, so here's a refresher
    # It starts off with a format like \x1b[XXXm where XXX is a semicolon separated list of commands
    # The important ones here relate to colour.
    # 30-37 are black, red, green, yellow, blue, magenta, cyan and white in that order
    # 40-47 are the same except for the background
    # 90-97 are the same but "bright" foreground
    # 100-107 are the same as the bright ones but for the background.
    # '1' means bold, '2' means dim, '0' means reset, and '4' means underline.

    def format(self, record):
        level_colours = [
            (logging.DEBUG, '\x1b[40;1m'),  # Debug level in bold black background
            (logging.VERBOSE, '\x1b[36;1m'),  # Verbose level in bold cyan foreground   # noqa
            (logging.INIT, '\x1b[46;1m'),  # Init level in bold cyan background         # noqa
            (logging.START, '\x1b[42;1m'),  # Start level in bold green background      # noqa
            (logging.END, '\x1b[41;1m'),  # End level in bold red background            # noqa
            (logging.INFO, '\x1b[34;1m'),  # Info level in bold blue foreground
            (logging.WARNING, '\x1b[33;1m'),  # Warning level in bold yellow foreground
            (logging.ERROR, '\x1b[31m'),  # Error level in red foreground
            (logging.CRITICAL, '\x1b[41m'),  # Critical level with red background
        ]

        formats = {
            level: logging.Formatter(
                f'\x1b[30;1m%(asctime)s\x1b[0m {colour}%(levelname)-8s\x1b[0m \x1b[0m %(message)s',
                '%H:%M:%S',  # '%Y-%m-%d %H:%M:%S'
            )
            for level, colour in level_colours
        }

        formatter = formats.get(record.levelno, formats[logging.DEBUG])

        # Override the traceback to always print in red
        if record.exc_info:
            text = formatter.formatException(record.exc_info)
            record.exc_text = f'\x1b[31m{text}\x1b[0m'

        output = formatter.format(record)

        # Remove the cache layer
        record.exc_text = None
        return output
