import copy
from collections import OrderedDict

from django.db.models import Subquery
from django.db.models.constants import LOOKUP_SEP
from django_filters import filterset, rest_framework
from django_filters.utils import get_model_field

from . import filters, utils


def related(filterset, filter_name):
    """
    Return a related filter_name, using the filterset relationship if present.
    """
    if not filterset.relationship:
        return filter_name
    return LOOKUP_SEP.join([filterset.relationship, filter_name])


class FilterSetMetaclass(filterset.FilterSetMetaclass):
    def __new__(cls, name, bases, attrs):
        new_class = super(FilterSetMetaclass, cls).__new__(cls, name, bases, attrs)

        new_class.auto_filters = [
            name for name, f in new_class.declared_filters.items()
            if isinstance(f, filters.AutoFilter)]
        new_class.related_filters = [
            name for name, f in new_class.declared_filters.items()
            if isinstance(f, filters.RelatedFilter)]

        # see: :meth:`rest_framework_filters.filters.RelatedFilter.bind`
        for name in new_class.related_filters:
            new_class.declared_filters[name].bind(new_class)

        # If model is defined, process auto filters
        if new_class._meta.model is not None:
            cls.expand_auto_filters(new_class)

        return new_class

    @classmethod
    def expand_auto_filters(cls, new_class):
        """
        Resolve `AutoFilter`s into their per-lookup filters. `AutoFilter`s are
        a declarative alternative to the `Meta.fields` dictionary syntax, and
        use the same machinery internally.
        """
        # get reference to opts/declared filters
        orig_meta, orig_declared = new_class._meta, new_class.declared_filters

        # override opts/declared filters w/ copies
        new_class._meta = copy.deepcopy(new_class._meta)
        new_class.declared_filters = new_class.declared_filters.copy()

        for name in new_class.auto_filters:
            f = new_class.declared_filters[name]

            # Remove auto filters from declared_filters so that they *are* overwritten
            # RelatedFilter is an exception, and should *not* be overwritten
            if not isinstance(f, filters.RelatedFilter):
                del new_class.declared_filters[name]

            # Use meta.fields to generate auto filters
            new_class._meta.fields = {f.field_name: f.lookups or []}
            for gen_name, gen_f in new_class.get_filters().items():
                # get_filters() generates param names from the model field name
                # Replace the field name with the parameter name from the filerset
                gen_name = gen_name.replace(f.field_name, name, 1)
                new_class.base_filters[gen_name] = gen_f

        # restore reference to opts/declared filters
        new_class._meta, new_class.declared_filters = orig_meta, orig_declared


class SubsetDisabledMixin:
    """
    Used to disable filter subsetting (see: :meth:`FilterSet.disable_subset`).
    """
    @classmethod
    def get_filter_subset(cls, params, rel=None):
        return cls.base_filters


class FilterSet(rest_framework.FilterSet, metaclass=FilterSetMetaclass):

    def __init__(self, data=None, queryset=None, *, relationship=None, **kwargs):
        self.base_filters = self.get_filter_subset(data or {}, relationship)

        super().__init__(data, queryset, **kwargs)

        self.relationship = relationship
        self.related_filtersets = self.get_related_filtersets()
        self.filters = self.get_request_filters()

    @classmethod
    def get_fields(cls):
        fields = super(FilterSet, cls).get_fields()

        for name, lookups in fields.items():
            if lookups == filters.ALL_LOOKUPS:
                field = get_model_field(cls._meta.model, name)
                fields[name] = utils.lookups_for_field(field)

        return fields

    @classmethod
    def get_filter_subset(cls, params, rel=None):
        """
        Returns the subset of filters that should be initialized by the
        FilterSet, dependent on the requested `params`. This helps minimize
        the cost of initialization by reducing the number of deepcopy ops.

        The `rel` argument is used for related filtersets to strip the param
        of its relationship prefix. See `.get_param_filter_name()` for info.
        """
        # Determine names of filters from query params and remove empty values.
        # param names that traverse relations are translated to just the local
        # filter names. eg, `author__username` => `author`. Empty values are
        # removed, as they indicate an unknown field eg, author__foobar__isnull
        filter_names = {cls.get_param_filter_name(param, rel) for param in params}
        filter_names = {f for f in filter_names if f is not None}
        return OrderedDict(
            (k, v) for k, v in cls.base_filters.items() if k in filter_names
        )

    @classmethod
    def disable_subset(cls, *, depth=0):
        """
        Disable filter subsetting, allowing the form to render the filterset.
        Note that this decreases performance and should only be used when
        rendering a form, such as with DRF's browsable API.
        """
        if not issubclass(cls, SubsetDisabledMixin):
            cls = type('SubsetDisabled%s' % cls.__name__,
                       (SubsetDisabledMixin, cls), {})

        # recursively disable subset for related filtersets
        if depth > 0:
            # shallow copy to prevent modifying original `base_filters`
            cls.base_filters = cls.base_filters.copy()

            # deepcopy RelateFilter to prevent modifying original `.filterset`
            for name in cls.related_filters:
                f = copy.deepcopy(cls.base_filters[name])
                f.filterset = f.filterset.disable_subset(depth=depth - 1)
                cls.base_filters[name] = f

        return cls

    @classmethod
    def get_param_filter_name(cls, param, rel=None):
        """
        Get the filter name for the request data parameter.

        ex::

            # regular attribute filters
            >>> FilterSet.get_param_filter_name('email')
            'email'

            # exclusion filters
            >>> FilterSet.get_param_filter_name('email!')
            'email'

            # related filters
            >>> FilterSet.get_param_filter_name('author__email')
            'author'

            # attribute filters based on relationship
            >>> FilterSet.get_param_filter_name('author__email', rel='author')
            'email'

        """
        # strip the rel prefix from the param name.
        prefix = '%s%s' % (rel or '', LOOKUP_SEP)
        if rel and param.startswith(prefix):
            param = param[len(prefix):]

        # Attempt to match against filters with lookups first. (username__endswith)
        if param in cls.base_filters:
            return param

        # Attempt to match against exclusion filters
        if param[-1] == '!' and param[:-1] in cls.base_filters:
            return param[:-1]

        # Match against relationships. (author__username__endswith).
        # Preference more specific filters. eg, `note__author` over `note`.
        for name in reversed(sorted(cls.related_filters)):
            # we need to match against '__' to prevent eager matching against
            # like names. eg, note vs note2. Exact matches are handled above.
            if param.startswith("%s%s" % (name, LOOKUP_SEP)):
                return name

    def get_request_filters(self):
        """
        Build a set of filters based on the request data. This currently
        includes only filter exclusion/negation.
        """
        # build the compiled set of all filters
        requested_filters = OrderedDict()
        for filter_name, f in self.filters.items():
            requested_filters[filter_name] = f

            # exclusion params
            exclude_name = '%s!' % filter_name
            if related(self, exclude_name) in self.data:
                # deepcopy the *base* filter to prevent copying of model & parent
                f_copy = copy.deepcopy(self.base_filters[filter_name])
                f_copy.parent = f.parent
                f_copy.model = f.model
                f_copy.exclude = not f.exclude

                requested_filters[exclude_name] = f_copy

        return requested_filters

    def get_related_filtersets(self):
        """
        Get the related filterset instances for all related filters.
        """
        related_filtersets = OrderedDict()

        for related_name in self.related_filters:
            if related_name not in self.filters:
                continue

            f = self.filters[related_name]
            related_filtersets[related_name] = f.filterset(
                data=self.data,
                queryset=f.get_queryset(self.request),
                relationship=related(self, related_name),
                request=self.request,
                prefix=self.form_prefix,
            )

        return related_filtersets

    def filter_queryset(self, queryset):
        queryset = super(FilterSet, self).filter_queryset(queryset)
        queryset = self.filter_related_filtersets(queryset)
        return queryset

    def filter_related_filtersets(self, queryset):
        """
        Filter the provided `queryset` by the `related_filtersets`. It is
        recommended that you override this method to change the filtering
        behavior across relationships.
        """
        for related_name, related_filterset in self.related_filtersets.items():
            # Related filtersets should only be applied if they had data.
            prefix = '%s%s' % (related(self, related_name), LOOKUP_SEP)
            if not any(value.startswith(prefix) for value in self.data):
                continue

            field_name = self.filters[related_name].field_name
            lookup_expr = LOOKUP_SEP.join([field_name, 'in'])
            subquery = Subquery(related_filterset.qs.values('pk'))
            queryset = queryset.filter(**{lookup_expr: subquery})

        return queryset

    def get_form_class(self):
        class Form(super(FilterSet, self).get_form_class()):
            def add_prefix(form, field_name):
                field_name = related(self, field_name)
                return super(Form, form).add_prefix(field_name)

            def clean(form):
                cleaned_data = super(Form, form).clean()

                # when prefixing the errors, use the related filter name,
                # which is relative to the parent filterset, not the root.
                for related_filterset in self.related_filtersets.values():
                    for key, error in related_filterset.form.errors.items():
                        self.form.errors[related(related_filterset, key)] = error

                return cleaned_data
        return Form
