"""Views for the ``mtp.tasks`` app"""

from django.http import Http404, HttpResponseForbidden, JsonResponse

import django_filters
from django_filters.views import BaseFilterView

from mtp.apps.common.views_utils import LoginRequiredMixin
from .models import DjangoTask, TASK_STATES


class BadStateFilter(ValueError):
    """Exception used in ``TaskFilterSet`` when a state or subset doesn't exist"""
    pass


class TaskFilterSet(django_filters.FilterSet):
    """Filter for DjangoTask class

    Attributes
    ----------
    state: django_filters.CharFilter
        Allow to filter on a state constant from ``TASK_STATES``
    state_in: django_filters.CharFilter
        Allow to filter on a states subset from ``TASK_STATES``

    Notes
    -----

    This class does not set ``DjangoTask`` as the ``model`` in its ``Meta`` class.
    It allows to define a view on a subclass of ``DjangoTask``, by simply creating a view based
    on ``TaskListJsonView`` and defining the correct model, without having to define a new
    ``FilterSet`` too.

    """

    state = django_filters.CharFilter(method='filter_state')
    state_in = django_filters.CharFilter(method='filter_state_in')

    class Meta:
        model = DjangoTask
        fields = '__all__'

    def filter_state(self, queryset, name, value):  # pylint: disable=unused-argument
        """Filter the `queryset` on a state constant provided by `value`

        Parameters
        ----------
        value: str
            Must be a constant from ``TASK_STATES``, used to filter the django tasks on this state

        For the other parameters, see ``django_filters.Filter`` (``method`` attribute)

        Returns
        -------
        django.db.models.QuerySet
            The given queryset, filtered on the ``state`` field

        Raises
        ------
        BadStateFilter
            If the given `value` is not a valid constant in ``TASK_STATES``

        """

        if value not in TASK_STATES.constants:
            raise BadStateFilter('`%s` is not a valid state' % value)

        state = TASK_STATES.for_constant(value).value
        return queryset.filter(state=state)

    def filter_state_in(self, queryset, name, value):  # pylint: disable=unused-argument
        """Filter the `queryset` on a states subset name provided by `value`

        Parameters
        ----------
        value: str
            Must be the name of a subset from ``TASK_STATES``, used to filter the django tasks on
            this subset.

        For the other parameters, see ``django_filters.Filter`` (``method`` attribute)

        Returns
        -------
        django.db.models.QuerySet
            The given queryset, filtered on the ``state`` field

        Raises
        ------
        BadStateFilter
            If the given `value` is not a valid subset in ``TASK_STATES``

        """

        if value not in TASK_STATES.subsets:
            raise BadStateFilter('`%s` is not a valid states subset' % value)

        subset = getattr(TASK_STATES, value)
        return queryset.filter(state__in=subset.values.keys())


class TaskListJsonView(LoginRequiredMixin, BaseFilterView):
    """A simple list view for ajax only that will return a json view of tasks

    It's a ``FilterView``, using ``TaskFilterSet`` to allow filtering tasks by state (querystring
    argument ``state``) or by a subset of states (querystring argument ``state_in``).
    The resulting queryset is also filtered on the current user (to only return tasks created by
    the current user).

    Notes
    -----
    To use this view for a subclass of ``DjangoTask``, simply inherit from this view and set
    the ``model`` attribute. And override ``render_task_to_dict`` if more data needs to be returned
    in addition to the default task id and state.

    """

    http_method_names = ['get', 'head', 'options']
    filterset_class = TaskFilterSet
    model = DjangoTask

    def dispatch(self, request, *args, **kwargs):
        """Forbid calls to this view in non-ajax mode

        For the parameters, see ``django.views.generic.View"""

        if not request.is_ajax():
            # 403 because of http://stackoverflow.com/a/5298527
            return HttpResponseForbidden()

        return super().dispatch(request, *args, **kwargs)

    def get_queryset(self):
        """Filter the queryset on the current request user."""

        return super().get_queryset().filter(user=self.request.user)

    def get(self, request, *args, **kwargs):
        """Handle bad states in the filter by returning a 404.

        For the parameters, see ``django.views.generic.View``"""

        try:
            return super().get(request, *args, **kwargs)
        except BadStateFilter as exc:
            raise Http404(str(exc))

    def render_task_to_dict(self, task):
        """Convert a task to a dict to be converted into Json to be put in the response

        Parameters
        ----------
        task: DjangoTask
            The task to convert to a dict

        Returns
        -------
        dict
            A dict with data from the task, that will be converted into Json

        """

        return {
            'task_id': task.celery_task_id,
            'state': TASK_STATES.for_value(task.state).constant,
        }

    def render_json(self, context):
        """Prepare the json to return in ``render_to_response.

        Parameters
        ----------
        context : ``django.template.Context`` instance.
            The final context generated by the view, to use to render the json.

        Returns
        -------
        dict
            A dict that will be converted to json and returned as the request response
            It contains a ``tasks`` key, which is a list of json objects
            converted from the dict returned by ``render_task_to_dict``, for each task.
        """

        return {
            'tasks': [self.render_task_to_dict(task) for task in context['object_list']],
        }

    def render_to_response(self, context, **response_kwargs):  # pylint:disable=unused-argument
        """Returns the tasks from the filtered queryset as JSON

        Returns
        -------
        JsonResponse
            The response is the result of ``render_json``, converted from dict to json.

        """

        return JsonResponse(self.render_json(context))
