from django.conf import settings
from django.utils import translation

from elasticsearch_dsl import F, query
from elasticsearch_dsl.filter import Bool
from rest_framework.filters import BaseFilterBackend

import mkt
from mkt.api.base import form_errors, get_region_from_request
from mkt.constants.applications import get_device_id
from mkt.features.utils import get_feature_profile


class SearchQueryFilter(BaseFilterBackend):
    """
    A django-rest-framework filter backend that scores the given ES queryset
    with a should query based on the search query found in the current
    request's query parameters.
    """
    def _get_locale_analyzer(self, lang):
        analyzer = mkt.SEARCH_LANGUAGE_TO_ANALYZER.get(lang)
        if (analyzer in mkt.SEARCH_ANALYZER_PLUGINS and
                not settings.ES_USE_PLUGINS):
            analyzer = None
        return analyzer

    def filter_queryset(self, request, queryset, view):

        q = request.GET.get('q', '').lower()
        lang = translation.get_language()
        analyzer = self._get_locale_analyzer(lang)

        if not q:
            return queryset

        should = []
        rules = [
            (query.Match, {'query': q, 'boost': 3, 'analyzer': 'standard'}),
            (query.Match, {'query': q, 'boost': 4, 'type': 'phrase',
                           'slop': 1}),
            (query.Prefix, {'value': q, 'boost': 1.5}),
        ]

        # Only add fuzzy queries if q is a single word. It doesn't make sense
        # to do a fuzzy query for multi-word queries.
        if ' ' not in q:
            rules.append(
                (query.Fuzzy, {'value': q, 'boost': 2, 'prefix_length': 1}))

        # Apply rules to search on few base fields. Some might not be present
        # in every document type / indexes.
        for k, v in rules:
            for field in ('name', 'short_name', 'title', 'app_slug', 'author',
                          'url_tokenized'):
                should.append(k(**{field: v}))

        # Exact matches need to be queried against a non-analyzed field. Let's
        # do a term query on `name.raw` for an exact match against the item
        # name and give it a good boost since this is likely what the user
        # wants.
        should.append(query.Term(**{'name.raw': {'value': q, 'boost': 10}}))
        # Do the same for GUID searches.
        should.append(query.Term(**{'guid': {'value': q, 'boost': 10}}))
        # If query is numeric, check if it is an ID.
        if q.isnumeric():
            should.append(query.Term(**{'id': {'value': q, 'boost': 10}}))

        if analyzer:
            should.append(
                query.Match(**{'name_l10n_%s' % analyzer: {'query': q,
                                                           'boost': 2.5}}))
            should.append(
                query.Match(**{'short_name_l10n_%s' % analyzer: {
                    'query': q,
                    'boost': 2.5}}))

        # Add searches on the description field.
        should.append(
            query.Match(description={'query': q, 'boost': 0.8,
                                     'type': 'phrase'}))

        if analyzer:
            desc_field = 'description_l10n_%s' % analyzer
            desc_analyzer = ('%s_analyzer' % analyzer
                             if analyzer in mkt.STEMMER_MAP else analyzer)
            should.append(
                query.Match(
                    **{desc_field: {'query': q, 'boost': 0.6, 'type': 'phrase',
                                    'analyzer': desc_analyzer}}))

        # Add searches on tag field.
        should.append(query.Term(tags={'value': q}))
        if ' ' not in q:
            should.append(query.Fuzzy(tags={'value': q, 'prefix_length': 1}))

        # The list of functions applied to our `function_score` query.
        functions = [
            query.SF('field_value_factor', field='boost'),
        ]

        # Add a boost for the preferred region, if it exists.
        region = get_region_from_request(request)
        if region:
            functions.append({
                'filter': {'term': {'preferred_regions': region.id}},
                # TODO: When we upgrade to Elasticsearch 1.4, change this
                # to 'weight'.
                'boost_factor': 4,
            })

        return queryset.query('function_score',
                              query=query.Bool(should=should),
                              functions=functions)


class SearchFormFilter(BaseFilterBackend):
    """
    A django-rest-framework filter backend that filters the given queryset
    based on `self.form_class`.

    """
    # A mapping of form fields to Elasticsearch fields for those that differ.
    FORM_TO_FIELD_MAP = {
        'author': 'author.raw',
        'cat': 'category',
        'has_info_request': 'latest_version.has_info_request',
        'has_editor_comment': 'latest_version.has_editor_comment',
        'languages': 'supported_locales',
        'offline': 'is_offline',
        'premium_types': 'premium_type',
        'tag': 'tags'
    }

    def filter_queryset(self, request, queryset, view):
        form = view.form_class(request.GET)
        if not form.is_valid():
            raise form_errors(form)

        self.form_data = form.cleaned_data

        data = {}
        for k, v in self.form_data.items():
            data[self.FORM_TO_FIELD_MAP.get(k, k)] = v

        # Must filters.
        must = []
        for field in self.VALID_FILTERS:
            value = data.get(field)
            if value is not None:
                if type(value) == list:
                    filter_type = 'terms'
                else:
                    filter_type = 'term'
                must.append(F(filter_type, **{field: value}))

        if must:
            return queryset.filter(Bool(must=must))