# coding: utf-8
"""
This module provides redefined DRF's generic views and viewsets leveraging serializer registration.

One of the main issues with creating traditional DRF APIs is a lot of bloat (and we're writing Python, not Java or C#,
to avoid bloat) that's completely unnecessary in a structured Django project. Therefore this module aims to provide
a better and simpler way to write simple API endpoints - without limiting the ability to create more complex views.
The particular means to that end are:
* :class:`rest_easy.views.ScopeQuerySet` and its subclasses (
  :class:`rest_easy.views.UrlKwargScopeQuerySet` and
  :class:`rest_easy.views.RequestAttrScopeQuerySet`) provide a simple way to scope views and viewsets
  by resource (ie. limiting results to single account, or /resource/<resource_pk>/inner_resource/<inner_resource_pk>/)
* generic views leveraging the above, as well as model-and-schema specification instead of queryset, serializer and
  helper methods - all generic views that were available in DRF as well as GenericAPIView are redefined to support
  this.
* Generic :class:`rest_easy.views.ModelViewSet` which allows for very simple definition of resource
  endpoint.

To make the new views work, all that's required is a serializer:

```python
from users.models import User
from accounts.models import Account
from rest_easy.serializers import ModelSerializer
class UserSerializer(ModelSerializer):
    class Meta:
        model = User
        fields = '__all__'
        schema = 'default'

class UserViewSet(ModelViewSet):
    model = User
    scope = UrlKwargScopeQuerySet(Account)
```

and in urls.py:

```python
from django.conf.urls import url, include
from rest_framework.routers import DefaultRouter
router = DefaultRouter()
router.register(r'accounts/(?P<account_pk>[0-9]+)/users', UserViewSet)
urlpatterns = [url(r'^', include(router.urls))]
```

The above will provide the users scoped by account primary key as resources: with list, retrieve, create, update and
partial update methods, as well as standard HEAD and OPTIONS autogenerated responses.

You can easily add custom paths to viewsets when needed - it's described in DRF documentation.
"""
from __future__ import unicode_literals

from django.conf import settings
from django.db.models import QuerySet, Model
from django.http import Http404
from django.shortcuts import get_object_or_404
from rest_framework.viewsets import ViewSetMixin
from rest_framework import generics, mixins
from six import with_metaclass

from rest_easy.exceptions import RestEasyException
from rest_easy.registers import serializer_register


class ScopeQuerySet(object):
    """
    This class provides a scope-by-parent-element functionality to views and their querysets.

    It works by selecting a proper parent model instance and filtering view's queryset with it automatically.
    """

    def __init__(self, qs_or_obj, parent_field='id', related_field=None, raise_404=False, parent=None,
                 allow_none=False):
        """
        Sets instance properties, infers sane defaults and ensures qs_or_obj is correct.

        :param qs_or_obj: This can be a queryset or a Django model or explicit None (for particular subclasses)
        :param parent_field: the field to filter by in the parent queryset (qs_or_obj), by default 'id'.
        :param related_field: the field to filter by in the view queryset, by default model_name.
        :param raise_404: whether 404 should be raised if parent object cannot be found.
        :param parent: if this object's queryset should be filtered by another parameter, parent attribute should be
         an instance of ScopeQuerySet. This allows for ScopeQuerySetChaining (ie. for messages we might have
         UrlKwargScopeQuerySet(User, parent=UrlKwargScopeQuerySet(Account))  for scoping by user and limiting users
         to an account.
        :param allow_none: if filtering view queryset by object=None should be allowed. If it's false, resulting
         queryset is guaranteed to be empty if parent object can't be found and 404 is not raised.
        """
        if isinstance(qs_or_obj, QuerySet):
            self.queryset = qs_or_obj
        elif issubclass(qs_or_obj, Model):
            self.queryset = qs_or_obj.objects.all()
        elif qs_or_obj is None:
            self.queryset = None
        else:
            raise RestEasyException('Queryset parameter must be an instance of QuerySet or a Model subclass.')

        if related_field is None:
            try:
                related_field = '{}'.format(self.queryset.model._meta.model_name)  # pylint: disable=protected-access
            except AttributeError:
                raise RestEasyException('Either related_field or qs_or_obj must be given.')
        self.parent_field = parent_field
        self.related_field = related_field
        self.raise_404 = raise_404
        self.parent = [parent] if isinstance(parent, ScopeQuerySet) else parent
        self.allow_none = allow_none

    def get_value(self, view):
        """
        Get value used to filter qs_or_objs's field specified for filtering (parent_field in init).
        :param view: DRF view instance - as it provides access to both request and kwargs.
        :return: value to filter by.
        """
        raise NotImplementedError('You need to use ScopeQueryset subclass with get_value implemented.')

    def get_queryset(self, view):
        """
        Obtains parent queryset (init's qs_or_obj) along with any chaining (init's parent) required.
        :param view: DRF view instance.
        :return: queryset instance.
        """
        queryset = self.queryset
        if self.parent:
            queryset = self.parent.child_queryset(queryset, view)
        return queryset

    def get_object(self, view):
        """
        Obtains parent object by which view queryset should be filtered.
        :param view: DRF view instance.
        :return: object (instance of init's qs_or_obj model except shadowed by subclass).
        """
        queryset = self.get_queryset(view)
        queryset = queryset.filter(**{self.parent_field: self.get_value(view)})
        try:
            obj = get_object_or_404(queryset)
        except Http404:
            if self.raise_404:
                raise
            obj = None
        return obj

    def child_queryset(self, queryset, view):
        """
        Performs filtering of the view queryset.
        :param queryset: view queryset instance.
        :param view: view object.
        :return: filtered queryset.
        """
        obj = self.get_object(view)
        if obj is None and not self.allow_none:
            return queryset.none()
        return queryset.filter(**{self.related_field: obj})


class UrlKwargScopeQuerySet(ScopeQuerySet):
    """
    ScopeQuerySet that obtains parent object from url kwargs.
    """

    def __init__(self, *args, **kwargs):
        """
        Adds url_kwarg to :class:`rest_easy.views.ScopeQuerySet` init parameters.

        :param args: same as :class:`rest_easy.views.ScopeQuerySet`.
        :param url_kwarg: name of url field to be obtained from view's kwargs. By default it will be inferred as
         model_name_pk.
        :param kwargs: same as :class:`rest_easy.views.ScopeQuerySet`.
        """
        self.url_kwarg = kwargs.pop('url_kwarg', None)
        super(UrlKwargScopeQuerySet, self).__init__(*args, **kwargs)
        if not self.url_kwarg:
            try:
                self.url_kwarg = '{}_pk'.format(self.queryset.model._meta.model_name)  # pylint: disable=protected-access
            except AttributeError:
                raise RestEasyException('Either related_field or qs_or_obj must be given.')

    def get_value(self, view):
        """
        Obtains value from url kwargs.
        :param view: DRF view instance.
        :return: Value determining parent object.
        """
        return view.kwargs.get(self.url_kwarg)


class RequestAttrScopeQuerySet(ScopeQuerySet):
    """
    ScopeQuerySet that obtains parent object from view's request property.

    It can work two-fold:
    * the request's property contains full object: in this case no filtering of parent's queryset is required. When
     using such approach, is_object must be set to True, and qs_or_obj can be None. Chaining will be disabled since it
     is inherent to filtering process.
    * the request's property contains object's id, uuid, or other unique property. In that case is_object needs to be
     explicitly set to False, and qs_or_obj needs to be a Django model or queryset. Chaining will be performed as
     usually.
    """

    def __init__(self, *args, **kwargs):
        """
        Adds is_object and request_attr  to :class:`rest_easy.views.ScopeQuerySet` init parameters.

        :param args: same as :class:`rest_easy.views.ScopeQuerySet`.
        :param request_attr: name of property to be obtained from view.request.
        :param is_object: if request's property will be an object or a value to filter by. True by default.
        :param kwargs: same as :class:`rest_easy.views.ScopeQuerySet`.
        """
        self.request_attr = kwargs.pop('request_attr', None)
        if self.request_attr is None:
            raise RestEasyException('request_attr must be set explicitly on an {} init.'.format(
                self.__class__.__name__))
        self.is_object = kwargs.pop('is_object', True)
        super(RequestAttrScopeQuerySet, self).__init__(*args, **kwargs)

    def get_value(self, view):
        """
        Obtains value from url kwargs.
        :param view: DRF view instance.
        :return: Value determining parent object.
        """
        return getattr(view.request, self.request_attr, None)

    def get_object(self, view):
        """
        Extends standard get_object's behaviour with handling values that are already objects.
        :param view: DRF view instance.
        :return: object to filter view's queryset by.
        """
        if self.is_object:
            return self.get_value(view)
        return super(RequestAttrScopeQuerySet, self).get_object(view)


class ScopedViewMixin(object):
    """
    This class provides a get_queryset method that works with ScopeQuerySet.

    Queryset obtained from superclass is filtered by view.scope's (if it exists) child_queryset() method.
    """

    def get_queryset(self):
        """
        Calls scope's child_queryset methods on queryset as obtained from superclass.
        :return: queryset.
        """
        queryset = super(ScopedViewMixin, self).get_queryset()
        if hasattr(self, 'scope'):
            if isinstance(self.scope, ScopeQuerySet):
                queryset = self.scope.child_queryset(queryset, self)
            else:
                for scope in self.scope:
                    queryset = scope.child_queryset(queryset, self)
        return queryset


class ViewEasyMetaclass(type):
    """
    This metaclass sets default queryset on a model-and-schema based views and fills in concrete views with bases.

    It's required for compatibility with some of DRF's elements, like routers.
    """
    resolved_bases = None

    @classmethod
    def get_additional_bases(mcs):
        """
        Looks for additional view bases in settings.REST_EASY_VIEW_BASES
        :return:
        """
        if mcs.resolved_bases is None:
            mcs.resolved_bases = []
            from importlib import import_module
            for base in getattr(settings, 'REST_EASY_VIEW_BASES'):
                mod, cls = base.rsplit('.', 1)
                mcs.resolved_bases.append(getattr(import_module(mod), cls))

        return mcs.resolved_bases

    def __new__(mcs, name, bases, attrs):
        """
        Create the class.
        """
        if ('queryset' not in attrs or attrs['queryset'] is None) and 'model' in attrs:
            attrs['queryset'] = attrs['model'].objects.all()
        if (not attrs.get('__abstract__', False)) and 'parent' in attrs and isinstance(attrs['parent'], ScopeQuerySet):
            attrs['parent'] = [attrs['parent']]
        bases = tuple(mcs.get_additional_bases() + list(bases))
        return super(ViewEasyMetaclass, mcs).__new__(mcs, name, bases, attrs)


class ChainingCreateUpdateMixin(object):
    """
    Chain-enabled versions of perform_create and perform_update.
    """
    def perform_create(self, serializer, **kwargs):  # pylint: disable=no-self-use
        """
        Extend default implementation with kwarg chaining.
        """
        return serializer.save(**kwargs)

    def perform_update(self, serializer, **kwargs):  # pylint: disable=no-self-use
        """
        Extend default implementation with kwarg chaining.
        """
        return serializer.save(**kwargs)


class GenericAPIViewBase(ScopedViewMixin, generics.GenericAPIView):
    """
    Provides a base for all generic views and viewsets leveraging registered serializers and ScopeQuerySets.

    Adds additional DRF-verb-wise override for obtaining serializer class: serializer_schema_for_verb property.
    It should be a dictionary of DRF verbs and serializer schemas (they work in conjunction with model property).
    The priority for obtaining serializer class is:
    * get_serializer_class override
    * serializer_class property
    * model + serializer_schema_for_verb[verb] lookup in :class:`rest_easy.registers.SerializerRegister`
    * model + schema lookup in :class:`rest_easy.registers.SerializerRegister`
    """
    serializer_schema_for_verb = {}

    def get_drf_verb(self):
        """
        Obtain the DRF verb used for a request.
        """
        method = self.request.method.lower()
        if method == 'get':
            if self.lookup_url_kwarg in self.kwargs:
                return 'retrieve'
            return 'list'
        mapping = {
            'post': 'create',
            'put': 'update',
            'patch': 'partial_update',
            'delete': 'destroy'
        }
        return mapping[method]

    def get_serializer_name(self, verb=None):
        """
        Obtains registered serializer name for this view.

        Leverages :class:`rest_easy.registers.SerializerRegister`. Works when either of or both model
        and schema properties are available on this view.

        :return: registered serializer key.
        """
        model = getattr(self, 'model', None)
        schema = None
        if not model and not hasattr(self, 'schema') and (verb and verb not in self.serializer_schema_for_verb):
            raise RestEasyException('Either model or schema fields need to be set on a model-based GenericAPIView.')
        if verb:
            schema = self.serializer_schema_for_verb.get(verb, None)
        if schema is None:
            schema = getattr(self, 'schema', 'default')
        return serializer_register.get_name(model, schema)

    def get_serializer_class(self):
        """
        Gets serializer appropriate for this view.

        Leverages :class:`rest_easy.registers.SerializerRegister`. Works when either of or both model
        and schema properties are available on this view.

        :return: serializer class.
        """

        if hasattr(self, 'serializer_class') and self.serializer_class:
            return self.serializer_class

        serializer = serializer_register.lookup(self.get_serializer_name(verb=self.get_drf_verb()))
        if serializer:
            return serializer

        raise RestEasyException('Serializer for model {} and schema {} cannot be found.'.format(
            getattr(self, 'model', '[no model]'),
            getattr(self, 'schema', '[no schema]')
        ))


class GenericAPIView(with_metaclass(ViewEasyMetaclass, GenericAPIViewBase)):
    """
    Base view with compat metaclass.
    """
    __abstract__ = True


class CreateAPIView(ChainingCreateUpdateMixin,
                    mixins.CreateModelMixin,
                    GenericAPIView):
    """
    Concrete view for creating a model instance.
    """

    def post(self, request, *args, **kwargs):
        """
        Shortcut method.
        """
        return self.create(request, *args, **kwargs)


class ListAPIView(mixins.ListModelMixin,
                  GenericAPIView):
    """
    Concrete view for listing a queryset.
    """

    def get(self, request, *args, **kwargs):
        """
        Shortcut method.
        """
        return self.list(request, *args, **kwargs)


class RetrieveAPIView(mixins.RetrieveModelMixin,
                      GenericAPIView):
    """
    Concrete view for retrieving a model instance.
    """

    def get(self, request, *args, **kwargs):
        """
        Shortcut method.
        """
        return self.retrieve(request, *args, **kwargs)


class DestroyAPIView(mixins.DestroyModelMixin,
                     GenericAPIView):
    """
    Concrete view for deleting a model instance.
    """

    def delete(self, request, *args, **kwargs):
        """
        Shortcut method.
        """
        return self.destroy(request, *args, **kwargs)


class UpdateAPIView(ChainingCreateUpdateMixin,
                    mixins.UpdateModelMixin,
                    GenericAPIView):
    """
    Concrete view for updating a model instance.
    """

    def put(self, request, *args, **kwargs):
        """
        Shortcut method.
        """
        return self.update(request, *args, **kwargs)

    def patch(self, request, *args, **kwargs):
        """
        Shortcut method.
        """
        return self.partial_update(request, *args, **kwargs)


class ListCreateAPIView(ChainingCreateUpdateMixin,
                        mixins.ListModelMixin,
                        mixins.CreateModelMixin,
                        GenericAPIView):
    """
    Concrete view for listing a queryset or creating a model instance.
    """

    def get(self, request, *args, **kwargs):
        """
        Shortcut method.
        """
        return self.list(request, *args, **kwargs)

    def post(self, request, *args, **kwargs):
        """
        Shortcut method.
        """
        return self.create(request, *args, **kwargs)


class RetrieveUpdateAPIView(ChainingCreateUpdateMixin,
                            mixins.RetrieveModelMixin,
                            mixins.UpdateModelMixin,
                            GenericAPIView):
    """
    Concrete view for retrieving, updating a model instance.
    """

    def get(self, request, *args, **kwargs):
        """
        Shortcut method.
        """
        return self.retrieve(request, *args, **kwargs)

    def put(self, request, *args, **kwargs):
        """
        Shortcut method.
        """
        return self.update(request, *args, **kwargs)

    def patch(self, request, *args, **kwargs):
        """
        Shortcut method.
        """
        return self.partial_update(request, *args, **kwargs)


class RetrieveDestroyAPIView(mixins.RetrieveModelMixin,
                             mixins.DestroyModelMixin,
                             GenericAPIView):
    """
    Concrete view for retrieving or deleting a model instance.
    """

    def get(self, request, *args, **kwargs):
        """
        Shortcut method.
        """
        return self.retrieve(request, *args, **kwargs)

    def delete(self, request, *args, **kwargs):
        """
        Shortcut method.
        """
        return self.destroy(request, *args, **kwargs)


class RetrieveUpdateDestroyAPIView(ChainingCreateUpdateMixin,
                                   mixins.RetrieveModelMixin,
                                   mixins.UpdateModelMixin,
                                   mixins.DestroyModelMixin,
                                   GenericAPIView):
    """
    Concrete view for retrieving, updating or deleting a model instance.
    """

    def get(self, request, *args, **kwargs):
        """
        Shortcut method.
        """
        return self.retrieve(request, *args, **kwargs)

    def put(self, request, *args, **kwargs):
        """
        Shortcut method.
        """
        return self.update(request, *args, **kwargs)

    def patch(self, request, *args, **kwargs):
        """
        Shortcut method.
        """
        return self.partial_update(request, *args, **kwargs)

    def delete(self, request, *args, **kwargs):
        """
        Shortcut method.
        """
        return self.destroy(request, *args, **kwargs)


class GenericViewSet(ViewSetMixin, GenericAPIView):
    """
    The GenericViewSet class does not provide any actions by default,
    but does include the base set of generic view behavior, such as
    the `get_object` and `get_queryset` methods.
    """
    pass


class ReadOnlyModelViewSet(mixins.RetrieveModelMixin,
                           mixins.ListModelMixin,
                           GenericViewSet):
    """
    A viewset that provides default `list()` and `retrieve()` actions.
    """
    pass


class ModelViewSet(ChainingCreateUpdateMixin,
                   mixins.CreateModelMixin,
                   mixins.RetrieveModelMixin,
                   mixins.UpdateModelMixin,
                   mixins.DestroyModelMixin,
                   mixins.ListModelMixin,
                   GenericViewSet):
    """
    A viewset that provides default `create()`, `retrieve()`, `update()`,
    `partial_update()`, `destroy()` and `list()` actions.
    """
    pass
