"""Search indexes filters."""
from elasticsearch_dsl.query import Q
from django_elasticsearch_dsl_drf.filter_backends import SearchFilterBackend, \
    FacetedSearchFilterBackend, GeoSpatialFilteringFilterBackend
from search_indexes.utils import OBJECT_FIELD_PROPERTIES
from six import iteritems
from search_indexes.documents import TagCategoryDocument
from tag.models import TagCategory


class CustomGeoSpatialFilteringFilterBackend(GeoSpatialFilteringFilterBackend):
    """Automatically adds centering and sorting within bounding box."""

    @staticmethod
    def calculate_center(first, second):
        if second[1] < first[1]:
            res_longtitude = first[1] + (360 + abs(first[1]) - abs(second[1])) / 2
        else:
            res_longtitude = first[1] + (second[1] - first[1]) / 2

        # return (first[0] + second[0]) / 2, result_part
        return (first[0] + second[0]) / 2, res_longtitude

    def filter_queryset(self, request, queryset, view):
        ret = super().filter_queryset(request, queryset, view)
        bb = request.query_params.get('location__geo_bounding_box')
        if bb:
            center = self.calculate_center(*map(lambda point: list(map(float, point.split(','))), bb.split('__')))
            request.GET._mutable = True
            request.query_params.update({
                'ordering': f'location__{center[0]}__{center[1]}__km'
            })
            request.GET._mutable = False
        return ret


class CustomFacetedSearchFilterBackend(FacetedSearchFilterBackend):

    def __init__(self):
        self.facets_computed = {}

    def aggregate(self, request, queryset, view):
        """Aggregate.

        :param request:
        :param queryset:
        :param view:
        :return:
        """
        def make_filter(cur_facet):
           def _filter(x):
               return cur_facet['facet']._params['field'] != next(iter(x._params))
           return _filter

        def make_tags_filter(cur_facet, tags_to_remove_ids):
            def _filter(x):
                if hasattr(x, '_params') and (x._params.get('must') or x._params.get('should')):
                    ret = []
                    for t in ['must', 'should']:
                        terms = x._params.get(t)
                        if terms:
                            for term in terms:
                                if cur_facet['facet']._params['field'] != next(iter(term._params)):
                                    return True # different fields. preserve filter
                                else:
                                    ret.append(next(iter(term._params.values())) not in tags_to_remove_ids)
                    return all(ret)
                if cur_facet['facet']._params['field'] != next(iter(x._params)):
                    return True # different fields. preserve filter
                else:
                    return next(iter(x._params.values())) not in tags_to_remove_ids
            return _filter

        __facets = self.construct_facets(request, view)
        setattr(view.paginator, 'facets_computed', {})
        for __field, __facet in iteritems(__facets):
            agg = __facet['facet'].get_aggregation()
            agg_filter = Q('match_all')
            if __facet['global']:
                queryset.aggs.bucket(
                    '_filter_' + __field,
                    'global'
                ).bucket(__field, agg)
            else:
                if __field != 'tag':
                    qs = queryset.__copy__()
                    qs.query = queryset.query._clone()
                    filterer = make_filter(__facet)
                    for param_type in ['must', 'must_not', 'should']:
                        if qs.query._proxied._params.get(param_type):
                            qs.query._proxied._params[param_type] = list(
                                filter(
                                    filterer, qs.query._proxied._params[param_type]
                                )
                            )
                    sh = qs.query._proxied._params.get('should')
                    if (not sh or not len(sh)) \
                            and qs.query._proxied._params.get('minimum_should_match'):
                        qs.query._proxied._params.pop('minimum_should_match')
                    facet_name = '_filter_' + __field
                    qs.aggs.bucket(
                        facet_name,
                        'filter',
                        filter=agg_filter
                   ).bucket(__field, agg)
                    view.paginator.facets_computed.update({facet_name: qs.execute().aggregations[facet_name]})
                else:
                    tag_facets = []
                    preserve_ids = []
                    facet_name = '_filter_' + __field
                    all_tag_categories = TagCategoryDocument.search() \
                        .filter('term', public=True) \
                        .filter(Q('term', value_type=TagCategory.LIST) | Q('match', index_name='wine-color'))
                    for category in all_tag_categories:
                        tags_to_remove = list(map(lambda t: str(t.id), category.tags))
                        qs = queryset.__copy__()
                        qs.query = queryset.query._clone()
                        filterer = make_tags_filter(__facet, tags_to_remove)
                        for param_type in ['must', 'should']:
                            if qs.query._proxied._params.get(param_type):
                                if qs.query._proxied._params.get(param_type):
                                    qs.query._proxied._params[param_type] = list(
                                        filter(
                                            filterer, qs.query._proxied._params[param_type]
                                        )
                                    )
                        sh = qs.query._proxied._params.get('should')
                        if (not sh or not len(sh)) \
                                and qs.query._proxied._params.get('minimum_should_match'):
                            qs.query._proxied._params.pop('minimum_should_match')
                        qs.aggs.bucket(
                            facet_name,
                            'filter',
                            filter=agg_filter
                        ).bucket(__field, agg)
                        tag_facets.append(qs.execute().aggregations[facet_name])
                        preserve_ids.append(list(map(int, tags_to_remove)))
                    view.paginator.facets_computed.update({facet_name: self.merge_buckets(tag_facets, preserve_ids)})
        return queryset

    @staticmethod
    def merge_buckets(buckets: list, preserve_ids: list):
        """Reduces all buckets preserving class"""
        result_bucket = buckets[0]
        result_bucket.tag.buckets = list(filter(lambda x: x['key'] in preserve_ids[0], result_bucket.tag.buckets._l_))
        for bucket, ids in list(zip(buckets, preserve_ids))[1:]:
            for tag in bucket.tag.buckets._l_:
                if tag['key'] in ids:
                    result_bucket.tag.buckets.append(tag)
        return result_bucket


class CustomSearchFilterBackend(SearchFilterBackend):
    """Custom SearchFilterBackend."""

    @staticmethod
    def search_among_all_locales(view, search_kwargs: dict):
        if hasattr(view,  'search_fields') and hasattr(view, 'translated_search_fields'):
            all_supported_locales = OBJECT_FIELD_PROPERTIES.keys()
            fields = search_kwargs.copy().keys()
            for field in fields:
                if field in view.translated_search_fields:
                    value = search_kwargs.pop(field)
                    search_kwargs.update({f'{field}.{locale}': value for locale in all_supported_locales})


    def construct_search(self, request, view):
        """Construct search.

        We have to deal with two types of structures:

        Type 1:

        >>> search_fields = (
        >>>     'title',
        >>>     'description',
        >>>     'summary',
        >>> )

        Type 2:

        >>> search_fields = {
        >>>     'title': {'boost': 2},
        >>>     'description': None,
        >>>     'summary': None,
        >>> }

        :param request: Django REST framework request.
        :param queryset: Base queryset.
        :param view: View.
        :type request: rest_framework.request.Request
        :type queryset: elasticsearch_dsl.search.Search
        :type view: rest_framework.viewsets.ReadOnlyModelViewSet
        :return: Updated queryset.
        :rtype: elasticsearch_dsl.search.Search
        """
        query_params = self.get_search_query_params(request)
        __queries = []
        for search_term in query_params:
            __values = self.split_lookup_name(search_term, 1)
            __len_values = len(__values)
            if __len_values > 1:
                field, value = __values
                if field in view.search_fields:
                    # Initial kwargs for the match query
                    field_kwargs = {field: {'query': value}}
                    # In case if we deal with structure 2
                    if isinstance(view.search_fields, dict):
                        extra_field_kwargs = view.search_fields[field]
                        if extra_field_kwargs:
                            field_kwargs[field].update(extra_field_kwargs)
                    # The match query
                    self.search_among_all_locales(view, field_kwargs)
                    if len(field_kwargs.keys()) > 1:
                        for k, v in field_kwargs.items():
                            __queries.append(
                                Q("match", **{k: v})
                            )
                            __queries.append(
                                Q('wildcard',
                                  **{k: {
                                      'value': f'*{search_term.lower()}*',
                                      'boost': v.get('boost', 1) + 30
                                      }
                                  }
                                )
                            )
                    else:
                        __queries.append(
                            Q("match", **field_kwargs)
                        )
                        __queries.append(
                            Q('wildcard', **{field: {'value': f'*{search_term.lower()}*',
                                                     'boost': field_kwargs[field].get('boost', 1) + 30}})
                        )
            else:
                for field in view.search_fields:
                    # Initial kwargs for the match query
                    field_kwargs = {field: {'query': search_term}}

                    # In case if we deal with structure 2
                    if isinstance(view.search_fields, dict):
                        extra_field_kwargs = view.search_fields[field]
                        if extra_field_kwargs:
                            field_kwargs[field].update(extra_field_kwargs)

                    # The match query
                    self.search_among_all_locales(view, field_kwargs)
                    if len(field_kwargs.keys()) > 1:
                        for k, v in field_kwargs.items():
                            __queries.append(
                                Q("match", **{k: v})
                            )
                            __queries.append(
                                Q('wildcard',
                                  **{k: {
                                      'value': f'*{search_term.lower()}*',
                                      'boost': v.get('boost', 1) + 30
                                      }
                                  }
                                )
                            )
                    else:
                        __queries.append(
                            Q("match", **field_kwargs)
                        )
                        __queries.append(
                            Q('wildcard', **{field: {'value': f'*{search_term.lower()}*',
                                                     'boost': field_kwargs[field].get('boost', 1) + 30}})
                        )
        return __queries