from __future__ import annotations  # https://stackoverflow.com/a/33533514

import inspect
import io
import json
import logging
import threading
import typing
from datetime import datetime
from functools import wraps

import sys
from copy import copy
from logbroker.unified_agent.client.python import UnifiedAgentHandler
from traceback import format_exc

from load.projects.cloud.loadtesting import events

META_PREFIX = '--meta-'
LOG_TYPE_KEY = f'{META_PREFIX}log-type'


class Logan(logging.Logger):
    _global_binded = {}

    @classmethod
    def bind_global(cls, k, v):
        cls._global_binded[k] = v

    @classmethod
    def binded_global(cls):
        return copy(cls._global_binded)

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._binded = {}

    def bind(self, k, v):
        if k == LOG_TYPE_KEY:
            if k in self._binded:
                raise ValueError('Log type should not be rewritten. '
                                 f'Already set: {self._binded[k]}. '
                                 f"Can't rewrite to {v}.")
        self._binded[k] = v

    def getChild(self, suffix: str) -> Logan:
        child = logging.Logger.getChild(self, suffix)
        child._binded = copy(self._binded)
        return child

    def entry_logger(self) -> Logan:
        return self._logger_channelled('entryL', 'entry')

    def access_logger(self) -> Logan:
        return self._logger_channelled('accessL', 'access')

    def private_api_logger(self) -> Logan:
        return self._logger_channelled('privateAppL', 'app-private')

    def public_api_logger(self) -> Logan:
        return self._logger_channelled('publicApiL', 'app-public')

    def _logger_channelled(self, name, log_type):
        logger = self.getChild(name)
        logger.bind(LOG_TYPE_KEY, log_type)
        return logger

    def makeRecord(self, name, level, fn, lno, msg, args, exc_info,
                   func=None, extra=None, sinfo=None):
        record = super().makeRecord(name, level, fn, lno, msg, args, exc_info, func, extra, sinfo)
        record.binded = self._binded
        return record

    def send(self, msg, *args, **kwargs):
        """
        like self.info(...) but with no matter to logging level
        """
        self._log(logging.INFO, msg, args, **kwargs)


def parse_level(level: typing.Union[str, int]):
    try:
        return int(level)
    except ValueError:
        return logging._nameToLevel[level.upper()]


def repr_level(level: int) -> typing.Union[str, int]:
    try:
        return logging._levelToName[level]
    except KeyError:
        return level


def _safe_dump(obj) -> str:
    try:
        stream = io.StringIO()
        print(type(obj), obj, file=stream)
        return stream.getvalue()
    except Exception:
        return 'object can not be serialized'


def _binded(record) -> dict:
    binded = Logan.binded_global()
    binded.update(getattr(record, 'binded', {}))
    return binded


def _format_to_dict(record: logging.LogRecord) -> dict:
    data = {
        'msg': logging._defaultFormatter.format(record),
        'time': datetime.fromtimestamp(record.created).isoformat(),  # TODO: record.created пo дефолту не в utc
    }
    if _binded(record).get(LOG_TYPE_KEY, 'lost') not in ['entry', 'access']:
        data.update({
            'level': record.levelname,
            'stack': record.name,
            'file': record.filename,
            'line': record.lineno,
        })
    data.update({k: v
                 for k, v in _binded(record).items()
                 if not k.startswith(META_PREFIX)})
    return data


class UaHandler(UnifiedAgentHandler):
    def __init__(self, *args, **kwargs):
        super(UaHandler, self).__init__(*args, **kwargs)
        self.setLevel(logging.NOTSET)

    def format(self, record: logging.LogRecord) -> str:
        return json.dumps(_format_to_dict(record), default=_safe_dump)

    def emit(self, record):
        try:
            message = self.format(record)
            meta = {
                'log_level': record.levelname,
            }
            meta.update({k[len(META_PREFIX):]: v
                         for k, v in _binded(record).items()
                         if k.startswith(META_PREFIX)}
                        )
            self._session.send(message, record.created, meta)
        except Exception as e:
            # скопированно с родительского класса:
            sys.stderr.write('UnifiedAgentHandler error {0}\n{1}'.format(e, format_exc()))


class LocalLoggingHandler(logging.StreamHandler):
    def __init__(self):
        super(LocalLoggingHandler, self).__init__()
        self.setLevel(logging.NOTSET)

    def format(self, record) -> str:
        msg = super().format(record)
        data = {k: v for k, v in _format_to_dict(record).items() if k != 'msg'}
        return '\n'.join([msg, json.dumps(data, default=_safe_dump, indent=4, sort_keys=True)])


class ChangeLogLevel(events.TimeLimitedEvent):
    def __init__(self, permanent_log_level):
        self._permanent_log_level = permanent_log_level

    def start(self, log_level):
        logging.root.setLevel(log_level)

    def stop(self):
        logging.root.setLevel(self._permanent_log_level)

    def is_in_progress(self):
        return logging.root.level != self._permanent_log_level


class EnableLocalLogging(events.TimeLimitedEvent):
    def __init__(self):
        self._lock = threading.Lock()

    def start(self):

        with self._lock:
            if self._is_in_progress_no_lock():
                return
            logging.root.addHandler(LocalLoggingHandler())

    def stop(self):
        with self._lock:
            local_handlers = [h for h in logging.root.handlers
                              if isinstance(h, LocalLoggingHandler)]
            for h in local_handlers:
                logging.root.removeHandler(h)

    def is_in_progress(self):
        with self._lock:
            return self._is_in_progress_no_lock()

    @staticmethod
    def _is_in_progress_no_lock():
        for h in logging.root.handlers:
            if isinstance(h, LocalLoggingHandler):
                return True
        return False


def patch_format_on_instance(method, owner):
    @wraps(method)
    def pretty_format(record):
        msg = getattr(owner.__class__, method.__name__)(owner, record)
        data = {k: v for k, v in _format_to_dict(record).items() if k != 'msg'}
        return '\n'.join([msg, json.dumps(data, default=_safe_dump, indent=4, sort_keys=True)])

    return pretty_format


class Controller:
    def __init__(self, permanent_log_level, permanent_local_logging, unified_agent_uri, test_mode=False):
        """
        :param test_mode - для запусков внутри тестов. Что pytest, что ya.test добавляют свои хэндлеры.
        """
        self._permanent_log_level = permanent_log_level

        if test_mode:
            for h in logging.root.handlers:
                h.format = patch_format_on_instance(h.format, h)
        else:
            if logging.root.handlers:
                raise Exception('No external handlers allowed!')
            logging.root.addHandler(UaHandler(unified_agent_uri))

        logging.Logger.manager.setLoggerClass(Logan)
        logging.root.manager.setLoggerClass(Logan)
        logging.root.setLevel(permanent_log_level)

        self._set_level = events.EventWithAutostop(
            ChangeLogLevel(permanent_log_level)
        )
        if permanent_local_logging:
            EnableLocalLogging().start()
            self._enable_local_logging = events.EventWithAutostop(
                events.SwitchedOffEvent()
            )
        else:
            self._enable_local_logging = events.EventWithAutostop(
                EnableLocalLogging()
            )

    @property
    def permanent_log_level(self):
        return self._permanent_log_level

    @property
    def current_log_level(self):
        return logging.root.level

    def set_level(self, level, duration):
        return self._set_level.with_stop_after(duration).start(level)

    def reset_level(self):
        return self._set_level.stop()

    def log_level_state(self):
        return self._set_level.state()

    def enable_local_logging(self, duration):
        return self._enable_local_logging.with_stop_after(duration).start()

    def disable_local_logging(self):
        return self._enable_local_logging.stop()

    def local_logging_state(self):
        return self._enable_local_logging.state()


def lookup_logger(child_name=None):
    stack = []
    frame = inspect.currentframe()
    while frame := frame.f_back:
        if _logger := frame.f_locals.get('logger', None):
            if isinstance(_logger, logging.Logger):
                if len(stack) == 0:
                    # идиотская ситуация, когда объявленна переменная logging, но хочется воспользоваться этой функцией
                    return _logger
                return _logger.getChild(child_name or '.'.join(stack[::-1]))
        if _self := frame.f_locals.get('self', None):
            try:
                # не хочу разбираться кто там из них будет обычным аттрибутом, кто пропертёй, кто дескриптором данных
                # поэтому https://docs.python.org/2/glossary.html#term-eafp
                _logger = _self.logger
            except AttributeError:
                pass
            else:
                if isinstance(_logger, logging.Logger):
                    return _logger.getChild(child_name or '.'.join(
                        [frame.f_code.co_name] + stack[::-1]
                    ))
        stack.append(frame.f_code.co_name)

    if len(stack) > 0:
        stack[-1] = 'root'  # show `root` instead or `<module>` startup
    return logging.root.getChild(child_name or '.'.join(stack[::-1]))
