import inspect

from functools import wraps


class BaseProvider:
    """
    Базовый провайдер данных для контекста, классы-провайдеры данных для контекста должны
    наследоваться от BaseProvider
    """

    REQUIRED_KWARGS = {
        'http': ['request'],
        'arq': ['arq_task', 'arq_context'],
    }

    def __init__(self, **kwargs):
        self.kwargs = kwargs

    @staticmethod
    def _is_context_field(class_member):
        return (
            inspect.ismethod(class_member)
            and getattr(class_member, 'context_key', None) is not None
        )

    def get_required_kwargs(self, tag=None):
        required_kwargs = set()
        if tag is None:
            for _, value in self.REQUIRED_KWARGS.items():
                required_kwargs.update(value)
        else:
            required_kwargs.update(self.REQUIRED_KWARGS[tag])
        return required_kwargs

    def validate(self, tag=None):
        required_kwargs = self.get_required_kwargs(tag)
        for key in required_kwargs:
            if key not in self.kwargs:
                return False
        return True

    def collect_data(self, tag=None):
        data = {}
        if not self.validate(tag):
            return data
        for name, attribute in inspect.getmembers(self, predicate=self._is_context_field):
            key = getattr(attribute, 'context_key', None)
            tags = getattr(attribute, 'context_collector_tags', list())
            if tag is None or tag in tags:
                data[key] = attribute()
        return data


def context_field(*tags):
    """
    Отмечает метод класса как тот, который отдает определенную информацию для контекста.
    Название декорированного метода будет являться ключом в словаре-контексте,
    возвращаемое значение - значением в словаре.
    Позволяет задавать список контекстов в рамках которых будет вызван декорированный метод,
    например, в случае необходимости собрать контекст для логгирования вызова ручки нужен один набор
    данных, в случае если нужно будет залоггировать выполнение фоновой задачи (arq) - нужен будет
    другой набор данных.
    """

    def wrapper(f):
        @wraps(f)
        def inner(*args, **kwargs):
            try:
                return f(*args, **kwargs)
            except Exception:
                return None
        inner.context_key = f.__name__
        inner.context_collector_tags = tags
        return inner
    return wrapper
