import logging
from collections import defaultdict

from django.db import connection, models
from django.db.models import Exists, OuterRef
from django.db.models.constants import LOOKUP_SEP

from oscar.apps.catalogue import managers as oscar
from oscar.core.loading import get_model

logger = logging.getLogger(__name__)


class AttributeFilter(dict):
    def __init__(self, filter_kwargs):
        super(AttributeFilter, self).__init__()

        num_fields = 0
        for key, value in filter_kwargs.items():
            num_fields += 1
            if LOOKUP_SEP in key:
                field_name, lookup = key.split(LOOKUP_SEP, 1)
                self[field_name] = (lookup, value)
            else:
                self[key] = (None, value)

        self.num_fields = num_fields

    def field_names(self):
        return self.keys()

    def _selector(self, attribute_type):
        if attribute_type == "option" or attribute_type == "multi_option":
            return "attribute_values__value_%s__option" % attribute_type
        else:
            return "attribute_values__value_%s" % attribute_type

    def _select_value(self, types, lookup, value):
        _filter = models.Q()
        for _type in types:
            sel = self._selector(_type)
            if lookup is not None:
                sel = "%s%s%s" % (sel, LOOKUP_SEP, lookup)

            kwargs = dict()
            kwargs[sel] = value
            _filter |= models.Q(**kwargs)

        return _filter

    def fast_query(self, attribute_types, queryset):
        qs = queryset
        typedict = defaultdict(list)

        for code, attribute_type in attribute_types:
            typedict[code].append(attribute_type)

        for code, (lookup, value) in self.items():
            selected_values = self._select_value(typedict[code], lookup, value)
            if (
                not selected_values
            ):  # if no value clause can be formed, no result can be formed.
                return queryset.none()

            qs = qs.filter(selected_values, attribute_values__attribute__code=code)

        return qs


class ProductQuerySet(oscar.ProductQuerySet):
    def count(self):
        if not connection.features.allows_group_by_selected_pks:
            # the count will be super slow if all values are grouped by
            minimum_queryset = self.values("id")
            return super(ProductQuerySet, minimum_queryset).count()

        return super(ProductQuerySet, self).count()

    def base_queryset(self):
        """
        Applies select_related and prefetch_related for commonly related
        models to save on queries
        """
        Option = self.model.product_options.rel.model
        product_class_options = Option.objects.filter(
            productclass=models.OuterRef("product_class")
        )
        product_options = Option.objects.filter(product=models.OuterRef("pk"))
        return (
            self.select_related("product_class")
            .prefetch_related(
                "children",
                "product_options",
                "product_class__options",
                "stockrecords",
                "images",
            )
            .annotate(
                has_product_class_options=models.Exists(product_class_options),
                has_product_options=models.Exists(product_options),
            )
        )

    def filter_by_attributes(self, **filter_kwargs):
        attribute_filter = AttributeFilter(filter_kwargs)

        ProductAttribute = self.model.attributes.rel.model
        attribute_types = ProductAttribute.objects.values_list("code", "type").filter(
            code__in=attribute_filter.field_names()
        )

        return attribute_filter.fast_query(attribute_types, self)

    def browsable(self):
        """
        Excludes products without categories. They are used in product bundles.
        """
        ProductCategory = get_model("catalogue", "ProductCategory")
        qs = super().browsable()
        # Products without categories are used for product options only.
        return qs.filter(Exists(ProductCategory.objects.filter(product=OuterRef("pk"))))


ProductManager = ProductQuerySet.as_manager
