from functools import reduce
from operator import iand, ior
from string import punctuation

import django
from django.apps import apps
from django.contrib.sites.managers import CurrentSiteManager as DjangoCSM
from django.core.exceptions import ImproperlyConfigured
from django.db.models import CharField, Manager, Q, TextField
from django.db.models.manager import ManagerDescriptor
from django.db.models.query import QuerySet
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _

from mezzanine.conf import settings
from mezzanine.utils.sites import current_site_id
from mezzanine.utils.urls import home_slug

if django.VERSION >= (1, 10):

    class ManagerDescriptor(ManagerDescriptor):
        """
        This class exists purely to skip the abstract model check
        in the __get__ method of Django's ManagerDescriptor.
        """

        def __get__(self, instance, cls=None):
            if instance is not None:
                raise AttributeError(
                    "Manager isn't accessible via %s instances" % cls.__name__
                )

            # In ManagerDescriptor.__get__, an exception is raised here
            # if cls is abstract

            if cls._meta.swapped:
                raise AttributeError(
                    "Manager isn't available; "
                    "'%s.%s' has been swapped for '%s'"
                    % (
                        cls._meta.app_label,
                        cls._meta.object_name,
                        cls._meta.swapped,
                    )
                )

            return cls._meta.managers_map[self.manager.name]


class PublishedManager(Manager):
    """
    Provides filter for restricting items returned by status and
    publish date when the given user is not a staff member.
    """

    def published(self, for_user=None):
        """
        For non-staff users, return items with a published status and
        whose publish and expiry dates fall before and after the
        current date when specified.
        """
        from mezzanine.core.models import CONTENT_STATUS_PUBLISHED

        if for_user is not None and for_user.is_staff:
            return self.all()
        return self.filter(
            Q(publish_date__lte=now()) | Q(publish_date__isnull=True),
            Q(expiry_date__gte=now()) | Q(expiry_date__isnull=True),
            Q(status=CONTENT_STATUS_PUBLISHED),
        )

    def get_by_natural_key(self, slug):
        return self.get(slug=slug)


def search_fields_to_dict(fields):
    """
    In ``SearchableQuerySet`` and ``SearchableManager``, search fields
    can either be a sequence, or a dict of fields mapped to weights.
    This function converts sequences to a dict mapped to even weights,
    so that we're consistently dealing with a dict of fields mapped to
    weights, eg: ("title", "content") -> {"title": 1, "content": 1}
    """
    if not fields:
        return {}
    try:
        int(list(dict(fields).values())[0])
    except (TypeError, ValueError):
        fields = dict(zip(fields, [1] * len(fields)))
    return fields


class SearchableQuerySet(QuerySet):
    """
    QuerySet providing main search functionality for
    ``SearchableManager``.
    """

    def __init__(self, *args, **kwargs):
        self._search_ordered = False
        self._search_terms = set()
        self._search_fields = kwargs.pop("search_fields", {})
        super().__init__(*args, **kwargs)

    def search(self, query, search_fields=None):