from django.http import Http404
from django.conf import settings
from django.core.paginator import Page


from rest_framework.settings import api_settings
from rest_framework.pagination import PaginationSerializer
from rest_framework.serializers import BaseSerializer
from rest_framework.filters import OrderingFilter
from rest_framework.filters import DjangoFilterBackend

from django_elasticsearch.models import EsIndexable


from elasticsearch import NotFoundError
try:
    from elasticsearch import ConnectionError
except ImportError:
    from urllib3.connection import ConnectionError
from elasticsearch import TransportError


class ElasticsearchPaginationSerializer(PaginationSerializer):
    @property
    def data(self):
        if self._data is None:
            if type(self.object) is Page:
                page = self.object
                self._data = {
                    'count': page.paginator.count,
                    'previous': self.fields['previous'].to_native(page),
                    'next': self.fields['next'].to_native(page),
                    'results': page.object_list
                }

        return super(ElasticsearchPaginationSerializer, self).data


class FakeSerializer(BaseSerializer):
    @property
    def base_fields(self):
        return {}

    @property
    def data(self):
        self._data = super(FakeSerializer, self).data
        if type(self._data) == list:  # better way ?
            self._data = {
                'count': self.object.count(),
                'results': self._data
            }
        return self._data

    def to_native(self, obj):
        return obj


class ElasticsearchFilterBackend(OrderingFilter, DjangoFilterBackend):
    def filter_queryset(self, request, queryset, view):
        model = queryset.model

        if view.action == 'list':
            if not issubclass(model, EsIndexable):
                raise ValueError("Model {0} is not indexed in Elasticsearch. "
                                 "Make it indexable by subclassing "
                                 "django_elasticsearch.models.EsIndexable."
                                 "".format(model))
            search_param = getattr(view, 'search_param', api_settings.SEARCH_PARAM)
            query = request.QUERY_PARAMS.get(search_param, '')

            # order of precedence : query params > class attribute > model Meta attribute
            ordering = self.get_ordering(request)
            if not ordering:
                ordering = self.get_default_ordering(view)

            filterable = getattr(view, 'filter_fields', [])
            filters = dict([(k, v)
                            for k, v in request.GET.iteritems()
                            if k in filterable])

            q = queryset.query(query).filter(**filters)
            if ordering:
                q = q.order_by(*ordering)

            return q
        else:
            return super(ElasticsearchFilterBackend, self).filter_queryset(
                request, queryset, view
            )


class IndexableModelMixin(object):
    """
    Use EsQueryset and ElasticsearchFilterBackend if available
    """
    filter_backends = [ElasticsearchFilterBackend,]
    FILTER_STATUS_MESSAGE_OK = 'Ok'
    FILTER_STATUS_MESSAGE_FAILED = 'Failed'

    def __init__(self, *args, **kwargs):
        self.es_failed = False
        super(IndexableModelMixin, self).__init__(*args, **kwargs)

    def get_object(self):
        try:
            return super(IndexableModelMixin, self).get_object()
        except NotFoundError:
            raise Http404

    def get_serializer_class(self):
        if self.action in ['list', 'retrieve'] and not self.es_failed:
            # let's return the elasticsearch response as it is.
            return FakeSerializer
        return super(IndexableModelMixin, self).get_serializer_class()

    def get_pagination_serializer(self, page):
        if not self.es_failed:
            context = self.get_serializer_context()
            return ElasticsearchPaginationSerializer(instance=page, context=context)
        return super(IndexableModelMixin, self).get_pagination_serializer(page)

    def get_queryset(self):
        if self.action in ['list', 'retrieve'] and not self.es_failed:
            return self.model.es.search("")
        # db fallback
        return super(IndexableModelMixin, self).get_queryset()

    def filter_queryset(self, queryset):
        if self.es_failed:
            for backend in api_settings.DEFAULT_FILTER_BACKENDS:
                queryset = backend().filter_queryset(self.request, queryset, self)
            return queryset
        else:
            return super(IndexableModelMixin, self).filter_queryset(queryset)

    def list(self, request, *args, **kwargs):
        r = super(IndexableModelMixin, self).list(request, *args, **kwargs)
        if not self.es_failed:
            if getattr(self.object_list, 'facets', None):
                r.data['facets'] = self.object_list.facets

            if getattr(self.object_list, 'suggestions', None):
                r.data['suggestions'] = self.object_list.suggestions

        return r

    def dispatch(self, request, *args, **kwargs):
        try:
            r = super(IndexableModelMixin, self).dispatch(request, *args, **kwargs)
        except (ConnectionError, TransportError), e:
            # reset object list
            self.queryset = None
            self.es_failed = True
            # db fallback
            r = super(IndexableModelMixin, self).dispatch(request, *args, **kwargs)
            if settings.DEBUG and isinstance(r.data, dict):
                r.data["filter_fail_cause"] = str(e)

        # Add a failed message in case something went wrong with elasticsearch
        # for example if the cluster went down.
        if isinstance(r.data, dict) and self.action in ['list', 'retrieve']:
            r.data['filter_status'] = (self.es_failed
                                       and self.FILTER_STATUS_MESSAGE_FAILED
                                       or self.FILTER_STATUS_MESSAGE_OK)
        return r