"""
Предоставляет инструменты для отправки данных в ErrorBooster через интерфейс sentry-sdk.
https://clubs.at.yandex-team.ru/python/3274

"""
from itertools import chain
from time import time
from typing import Callable, Iterable

from sentry_sdk import Hub, Transport
from sentry_sdk.utils import capture_internal_exceptions
from sentry_sdk.worker import BackgroundWorker


class EventConverter:
    """Преобразует данные события из формата Sentry в формат ErrorBooster."""

    keys = {
        'tags': (
            'dc',
            # Датацентр [vla, man, ...].
            'reqid',
            # ИД запроса requestId.
            'platform',
            # desktop|app|tv|tvapp|station|unsupported;
            #   псевдонимы: touch - touch|touch-phone|phone; pad - pad|touch-pad
            'isInternal',
            # Ошибка внутри сети.
            'isRobot',
            # Ошибка при визите робота.
            'block',
            # Блок или модуль, в котором произошла ошибка, например: stream.
            'service',
            # Если на одной странице живет несколько сервисов, например: Эфир, Дзен на морде.
            'source',
            # Источник ошибки, например: ugc_backend.
            'sourceMethod',
            # Метод источника, например: set_reactions.
            'sourceType',
            # Тип ошибки, например: network, logic.
            'url',
            # URL (полный, со схемой и доменом), на котором возникла ошибка.
            'page',
            # Тип (семейство) страницы, например: главная, корзина, товар, список в категории и тд.
            'region',
            # ID региона из геобазы.
            'slots',
            # Слоты экспериментов в формате
            # https://beta.wiki.yandex-team.ru/JandeksPoisk/combo/logs/#projeksperimenty
            'experiments',
            # Эксперименты, при которых произошло событие.
            # Формат: "<exp_description1>;<exp_description2>;..." (разбивается по ";")
            # Например: aaa=1;bb=7;ccc=yes;ddd
            'useragent',
        ),
        'additional': (
            # Данные события сентри, попадающие в блок additional
            'contexts',
            'modules',
            'extra',
            'breadcrumbs',
        ),
    }

    @classmethod
    def enrich(cls, *, source: dict, destination: dict, supported_keys: Iterable[str]):

        if not source:
            return

        for key in supported_keys:
            value = source.get(key)
            if value:
                destination[key] = value

    @classmethod
    def convert(cls, event: dict) -> dict:

        keys = cls.keys
        enrich = cls.enrich

        additional = {
            'eventid': event['event_id'],
        }
        enrich(source=event, destination=additional, supported_keys=keys['additional'])

        result = {
            'timestamp': event['timestamp_'],
            # штамп возникновения события

            'level': event['level'],
            # уровень ошибки trace|debug|info|error;
            # алиасы warning - [warn|warning]; critical - [critical|fatal]

            'env': event.get('environment', ''),
            # окружение development|testing|prestable|production|pre-production

            'version': event.get('release', ''),
            # версия приложения

            'host': event['server_name'],
            # хост источника ошибки

            'language': 'python',
            # nodejs|go|python

            'additional': additional,
            # произвольный набор полей вида key:string = value:string|object
            # в базе будет хранится как string:string(stringify).
            # Null-значения сюда передавать нельзя!

            'reqid': event.get('request', {}).get('headers', {}).get('X-Request-Id', ''),
            'method': event.get('request', {}).get('method', ''),
            'service': 'lms',
            'ua': event.get('request', {}).get('headers', {}).get('User-Agent', ''),
            'url': event.get('request', {}).get('url', ''),
        }

        user = event.get('user', {})
        if user:
            ip = user.get('ip_address', '')
            if ip:
                result['ip'] = ip  # адрес клиента
            yauid = user.get('yauid', '')
            if yauid:
                result['yandexuid'] = yauid  # уникальный ид пользователя Яндекса

        enrich(source=event.get('tags', {}), destination=result, supported_keys=keys['tags'])

        exceptions = event.get('exception', {})

        if exceptions:
            stack = []
            # Разобранную клиентом сентри трассировку придётся вновь собрать в строку,
            # чтобы её мог переварить бустер.

            for exception in exceptions['values']:
                trace = exception['stacktrace']
                module = exception['module']

                if module:
                    module = f'{module}.'

                message = f"{module}{exception['type']}: {exception['value']}"

                for frame in trace['frames'] or []:

                    path = frame['abs_path']
                    func = frame['function']
                    lineno = frame['lineno']
                    line = frame['context_line']
                    local_vars = frame['vars']

                    stack.append(
                        f'  File "{path}", line {lineno}, in {func}, locals {local_vars} \n    {line.strip()}'
                    )
                    result.update({'file': path, 'method': func})

            if stack:
                result['stack'] = '\n'.join(
                    chain(['Traceback (most recent call last):'], stack, [message])  # noqa
                )

        else:
            message = event['message']

        result['message'] = message
        # текст ошибки, все сэмплы ошибок будут сгруппированы по этому названию
        return result


class ErrorBoosterTransport(Transport):
    """Псевдотранспорт (конвертер + траснсопрт), позволяющий отправлять
    события в ErrorBooster, используя машинерию клиента Sentry.


    .. code-block:: python

        def send_me(event: dict):
            ...

        sentry_sdk.init(
            transport=ErrorBoosterTransport(
                project='myproject',
                sender=send_me,
            ),
            environment='development',
            release='0.0.1',
        )

        with sentry_sdk.configure_scope() as scope:

            scope.user = {'ip_address': '127.0.0.1', 'yandexuid': '1234567'}
            scope.set_tag('source', 'backend1')

            try:
                import json
                json.loads('bogus')

            except:
                sentry_sdk.capture_exception()

    """
    converter_cls = EventConverter

    def __init__(self, *, project: str, sender: Callable, bgworker: bool = True, options=None):
        """

        :param project: Название проекта, для которого записываем события.

        :param sender: Объект, поддерживающий вызов, осуществялющий отправку сообщения события.
            Должен принимать объект сообщения (словарь).

        :param bgworker: Следует ли использовать отдельную фоновую нить для отправки.
            Стоит помнить, что у .init() есть параметр shutdown_timeout, который
            может убивать нить.

        :param options:

        """
        super().__init__(options)

        self.project = project
        self.sender = sender

        worker = None

        if bgworker:
            worker = BackgroundWorker()

        self._worker = worker

        self.hub_cls = Hub

    def _convert(self, event: dict) -> dict:
        converted = self.converter_cls.convert(event)
        converted['project'] = self.project
        return converted

    def _send_event(self, event: dict):
        self.sender(self._convert(event))

    def capture_event(self, event: dict):

        event['timestamp_'] = round(time() * 1000)  # бустер ожидает миллисекунды
        hub = self.hub_cls.current

        def send_event_wrapper():
            with hub:
                with capture_internal_exceptions():
                    self._send_event(event)

        worker = self._worker

        if worker:
            worker.submit(send_event_wrapper)

        else:
            send_event_wrapper()

    def flush(self, timeout, callback=None):
        worker = self._worker
        if worker and timeout > 0:
            worker.flush(timeout, callback)

    def kill(self):
        worker = self._worker
        worker and worker.kill()
