from typing import Container, Mapping

from django.contrib.auth import get_user_model
from django.utils.decorators import classonlymethod
from django.utils.functional import cached_property
from django_tools_log_context import request_log_context
from rest_framework import status
from rest_framework.exceptions import PermissionDenied
from rest_framework.response import Response
from rest_framework.settings import api_settings
from rest_framework.viewsets import ViewSet as DRFViewSet
from ylog.context import put_to_context, pop_from_context

from intranet.femida.src.actionlog.models import actionlog
from intranet.femida.src.core.exceptions import FemidaError
from intranet.femida.src.core.shortcuts import get_object_or_40x
from intranet.femida.src.utils.tvm2_client import get_user_ticket


User = get_user_model()


class ResponseError(FemidaError):
    """
    Ошибка, после которой нужно сразу вернуть ответ
    """
    def __init__(self, **kwargs):
        self.kwargs = kwargs

    @property
    def response(self):
        return Response(**self.kwargs)


def unfold_query_params(query_params, list_fields: Container = None):
    """
    TODO: Надо sform научить работать с QueryDict
    """
    params = {}
    list_fields = list_fields or []
    for field_name in query_params:
        if field_name in list_fields:
            params[field_name] = query_params.getlist(field_name)
        else:
            params[field_name] = query_params.get(field_name)
    return params


def get_base_view_context(view):
    return {
        'request': view.request,
        'user': view.request.user,
        'session_id': view.session_id,
    }


class LogContextMixin:
    response_logs_key = 'response'

    def dispatch(self, request, *args, **kwargs):
        with request_log_context(request, endpoint=self, threshold=0) as log_context:
            response = super().dispatch(request, *args, **kwargs)
            response_context_data = self.get_response_log_context(response)
            if response_context_data:
                self.update_context(log_context, self.response_logs_key, response_context_data)
        return response

    def update_context(self, log_context, key, value: Mapping):
        """
        Изменяет значение контекста по заданному ключу в текущем log_context
        Нужно для добавления в контекст значения, не известного до входа в контекст,
        но необходимого в момент выхода из контекста, поскольку логи,
        в которые мы добавляем контекст, записываются именно в request_log_context.__exit__
        """
        new_value = {}
        # если ключ есть в log_context, значит в нём уже что-то добавляли, надо достать и обновить
        # иначе просто кладём новое значение и записываем в log_context.
        # делать просто pop_from_context нельзя, потому что он может достать что-то из более
        # внешнего контекста с тем же ключом, и потом текущий log_context его ещё и удалит.
        if key in log_context.context:
            new_value = pop_from_context(key)
        new_value.update(value)
        put_to_context(key, new_value)
        log_context.context[key] = new_value

    def get_response_log_context(self, response):
        return {}


class ValidationMixin:

    validator_class = None

    def _get_errors_data(self, validator):
        """
        TODO: Проверить, можем ли мы уже использовать один формат ошибок.
        Если да, то выпилить эту функцию
        https://st.yandex-team.ru/FEMIDA-1109
        """
        data = validator.errors
        for field_name, error in data['errors'].items():
            if not field_name:
                field_name = 'non_field_errors'
            data[field_name] = [i['message'] for i in error]
        return data

    def get_validator_class(self):
        return self.validator_class

    def get_validator_object(self, *args, **kwargs):
        # TODO: Либо нужно в sform добавить возможность прокидывать
        # произвольные параметры, либо придумать что-то другое
        kwargs['context'] = dict(self.get_validator_context(), **get_base_view_context(self))
        validator_class = self.get_validator_class()
        return validator_class(*args, **kwargs) if validator_class else None

    def get_validator_context(self):
        return {}

    def validate(self, validator):
        if not validator.is_valid():
            # Рейзим, чтобы метод можно было вызвать из любого места, а не только из методов,
            # которые должны вернуть Response
            raise ResponseError(
                data=self._get_errors_data(validator),
                status=status.HTTP_400_BAD_REQUEST,
            )


class SerializationMixin:

    list_item_serializer_class = None
    detail_serializer_class = None

    def get_detail_serializer_class(self):
        return self.detail_serializer_class

    def get_detail_serializer_object(self, instance, **kwargs):
        kwargs['context'] = dict(
            self.get_detail_serializer_context(),
            fields=self.optional_fields,
            **get_base_view_context(self)
        )
        serializer_class = self.get_detail_serializer_class()
        self.setup_eager_loading(serializer_class, [instance])
        return serializer_class(instance, **kwargs) if serializer_class else None

    def get_detail_serializer_context(self):
        return {}

    def get_list_item_serializer_class(self):
        return self.list_item_serializer_class

    def get_list_item_serializer_object(self, collection, **kwargs):
        kwargs['context'] = dict(
            self.get_list_item_serializer_context(),
            fields=self.optional_fields,
            **get_base_view_context(self)
        )
        serializer_class = self.get_list_item_serializer_class()
        return serializer_class(collection, many=True, **kwargs)

    def get_list_item_serializer_context(self):
        return {}


class DefaultViewSetActions(dict):
    """
    Словарь, который всегда True.
    Нужен, чтобы иметь возможность не прокидывать
    словарь в as_view для ViewSet
    """
    def __bool__(self):
        return True


class ViewSet(DRFViewSet):

    @property
    def optional_fields(self):
        if not hasattr(self, '_optional_fields'):
            self._optional_fields = self.request.query_params.get('_fields', [])
            if self._optional_fields:
                self._optional_fields = self._optional_fields.split(',')
        return self._optional_fields

    @classonlymethod
    def as_view(cls, actions=None, **initkwargs):
        actions = DefaultViewSetActions() if actions is None else actions
        view = super().as_view(actions, **initkwargs)
        if isinstance(actions, DefaultViewSetActions):
            del view.actions
        return view


class BaseView(LogContextMixin, ValidationMixin, SerializationMixin, ViewSet):

    model_class = None
    pagination_class = api_settings.DEFAULT_PAGINATION_CLASS

    def filter_queryset(self, queryset):
        return queryset

    def get_queryset(self):
        return self.model_class.objects.all()

    def setup_eager_loading(self, serializer_class, collection):
        setup_eager_loading = getattr(serializer_class, 'setup_eager_loading', None)
        if setup_eager_loading is not None:
            collection = setup_eager_loading(collection, self.optional_fields)
        return collection

    def get_object(self):
        if not hasattr(self, '_instance'):
            id_field = 'pk'
            object_keys = ['uuid', 'slug']
            for key in object_keys:
                if key in self.kwargs:
                    id_field = key
            queryset = self.get_queryset()
            self._instance = get_object_or_40x(queryset, **{id_field: self.kwargs[id_field]})
            self.check_object_permissions(self.request, self._instance)
        return self._instance

    def list(self, request, *args, **kwargs):
        queryset = self.filter_queryset(self.get_queryset())
        serializer_class = self.get_list_item_serializer_class()
        queryset = self.setup_eager_loading(serializer_class, queryset)
        paginator = self.pagination_class()
        self.page = paginator.paginate_queryset(queryset, request, view=self)
        serializer = self.get_list_item_serializer_object(self.page)
        return paginator.get_paginated_response(serializer.data)

    def retrieve(self, request, *args, **kwargs):
        instance = self.get_object()
        serializer = self.get_detail_serializer_object(instance)
        return Response(serializer.data)

    def create(self, request, *args, **kwargs):
        validator = self.get_validator_object(request.data)
        self.validate(validator)
        instance = self.perform_create(data=validator.cleaned_data)
        serializer = self.get_detail_serializer_object(instance)
        data = serializer.data if serializer else {}
        return Response(data, status=status.HTTP_201_CREATED)

    def perform_create(self, data):
        raise NotImplementedError

    def update(self, request, partial=False, *args, **kwargs):
        instance = self.get_object()
        serializer = self.get_detail_serializer_object(instance)
        initial_data = serializer.data if serializer else {}
        validator = self.get_validator_object(request.data, initial=initial_data, partial=partial)
        self.validate(validator)
        instance = self.perform_update(
            data=validator.cleaned_data,
            instance=instance,
        )
        serializer = self.get_detail_serializer_object(instance)
        data = serializer.data if serializer else {}
        return Response(data, status=status.HTTP_200_OK)

    def partial_update(self, request, *args, **kwargs):
        return self.update(request, partial=True, *args, **kwargs)

    def perform_update(self, data, instance):
        raise NotImplementedError

    def destroy(self, request, *args, **kwargs):
        instance = self.get_object()
        self.perform_destroy(instance)
        return Response(status=status.HTTP_204_NO_CONTENT)

    def perform_destroy(self, instance):
        raise NotImplementedError

    @property
    def session_id(self):
        return self.request.COOKIES.get('Session_id')

    @cached_property
    def client_ip(self):
        x_forwarded_for = self.request.META.get('HTTP_X_FORWARDED_FOR')
        if x_forwarded_for:
            ip = x_forwarded_for.split(',')[0]
        else:
            ip = self.request.META.get('HTTP_X_REAL_IP') or self.request.META.get('REMOTE_ADDR')
        return ip

    @cached_property
    def user_ticket(self):
        return get_user_ticket(self.request)


class BaseFormViewMixin:

    form_serializer_class = None

    def get_form_serializer_class(self):
        return self.form_serializer_class

    def get_form_serializer_object(self, instance, **kwargs):
        kwargs['context'] = dict(
            self.get_form_serializer_context(),
            **get_base_view_context(self)
        )
        serializer_class = self.get_form_serializer_class()
        return serializer_class(instance, **kwargs)

    def get_form_serializer_context(self):
        return {}

    def get_initial_data(self):
        return {}

    def get_form(self, request, *args, **kwargs):
        """
        Данные для формы создания/редактирования ресурса
        """
        initial_data = self.get_initial_data()
        form = self.get_validator_object(initial=initial_data, partial_fields=self.optional_fields)
        return Response(form.as_dict())

    @classonlymethod
    def as_form_view(cls, **initkwargs):
        initkwargs.setdefault('http_method_names', ['get', 'options'])
        actions = {'get': 'get_form'}
        return cls.as_view(actions, **initkwargs)

    @property
    def session_id(self):
        return self.request.COOKIES.get('Session_id')


class ValidatorSwitcherViewMixin:
    """
    Миксин для выбора формы в зависимости от параметра
    """
    validator_classes_map = {}

    def get_key(self):
        return self.get_object().type

    def get_validator_class(self):
        return self.validator_classes_map.get(
            self.get_key(),
            self.validator_class,
        )


class QueryParamValidatorSwitcherViewMixin(ValidatorSwitcherViewMixin):
    def get_key(self):
        return self.request.query_params.get('type', '')


class InstanceFormViewMixin(BaseFormViewMixin):
    """
    Вьюха с формой предзаполненной данными из БД
    """
    def get_initial_data(self):
        return self.get_form_serializer_object(self.get_object()).data


class QueryParamsFormViewMixin(BaseFormViewMixin):
    """
    FIXME: deprecated
    Вьюха с формой предзаполненной данными из GET-параметров
    """
    def get_query_params(self):
        return self.request.query_params

    def get_initial_data(self):
        # Это какая-то дичь. Приходится дважды инстанцировать форму.
        # Первый раз для валидации query-параметров.
        # Второй раз для прокидывания этих параметров в качестве initial
        form = self.get_validator_object(data=self.get_query_params())
        if form.is_valid():
            return self.get_form_serializer_object(form.cleaned_data).data
        else:
            raise ResponseError(
                data=form.errors,
                status=status.HTTP_400_BAD_REQUEST,
            )


class BaseFormView(LogContextMixin, ValidationMixin, BaseFormViewMixin, ViewSet):

    def get(self, request, *args, **kwargs):
        return self.get_form(request, *args, **kwargs)


class QueryParamsFormView(LogContextMixin, ValidationMixin, QueryParamsFormViewMixin, ViewSet):

    def get(self, request, *args, **kwargs):
        return self.get_form(request, *args, **kwargs)


class WorkflowViewMixin:

    workflow_class = None
    actionlog_name = None
    action_name = None

    @cached_property
    def workflow(self):
        return self.workflow_class(
            instance=self.get_object(),
            user=self.request.user,
            **self.get_wf_context_data()
        )

    def get_action(self, action_name):
        attr_name = '_action_%s' % action_name
        if not hasattr(self, attr_name):
            setattr(self, attr_name, self.workflow.get_action(action_name))
        return getattr(self, attr_name)

    def perform_action(self, action_name, **params):
        self.action = self.get_action(action_name)
        if not self.action.is_available():
            raise PermissionDenied('workflow_error')

        validator_class = self.get_validator_class()
        if validator_class is None:
            data = {}
        else:
            validator = self.get_validator_object(self.request.data)
            self.validate(validator)
            data = validator.cleaned_data

        data.update(params)
        instance = self.action.perform(**data)
        serializer = self.get_detail_serializer_object(instance)
        response_data = serializer.data if serializer is not None else {}
        response_data.update(self.get_additional_response_data())
        return Response(response_data, status=status.HTTP_200_OK)

    def get_wf_context_data(self):
        return {}

    def get_additional_response_data(self):
        return {}


class WorkflowView(WorkflowViewMixin, BaseView):

    def post(self, request, *args, **kwargs):
        with actionlog.init(self.actionlog_name, request.user):
            return self.perform_action(self.action_name)


class CursorPaginationCountListViewMixin:

    def is_count_required_for_list(self):
        return 'cursor' not in self.request.query_params

    def get_count(self):
        return self.filter_queryset(self.get_queryset()).count()

    def list(self, request, *args, **kwargs):
        response = super().list(request, *args, **kwargs)
        response.data['count'] = self.get_count() if self.is_count_required_for_list() else None
        return response

    def get_response_log_context(self, response):
        context = super().get_response_log_context(response)
        if 'count' in response.data:
            context['count'] = response.data['count']
        return context
