from collections import defaultdict
from typing import Set

from intranet.femida.src.publications.helpers import get_publications_lang
from intranet.femida.src.publications.models import PublicationFacet


class FacetCounter:
    """
    Класс с логикой вычисления счётчиков количества публикаций, подходящих под каждую галочку,
    которые отфильтруются при условии ограничений остальных нажатых галочек.
    Предполагается, что разные галочки внутри одного фасета
    задают ограничение OR и подходящие под них наборы публикаций объединяются,
    а сами фасеты задают ограничение AND между собой и их наборы публикаций пересекаются:
    Result = ({facet_1, val} OR ... OR {facet_1, val}) AND ...
        AND ({facet_n, val} OR ... OR {facet_n, val})
    Фасет без выбранных галочек не накладывает дополнительное ограничение.
    """
    def __init__(self, initial_data, available_publication_ids: Set = None, lang=None):
        self.available_publication_ids = available_publication_ids
        self.lang = lang if lang else get_publications_lang()
        self.publication_ids_by_facet_value = self.get_db_facets_map()

        self.values_by_facet = defaultdict(list)
        for facet, value in self.publication_ids_by_facet_value:
            self.values_by_facet[facet].append(value)

        self.facets = list(self.values_by_facet)

        self.selected_values_by_facet = defaultdict(list)
        for facet, values in initial_data.items():
            if facet in self.facets:
                for value in values:
                    if (facet, value) in self.publication_ids_by_facet_value:
                        self.selected_values_by_facet[facet].append(value)

        self.union_by_facet = self.compute_union_by_facet()

    def get_db_facets_map(self):
        queryset = (
            PublicationFacet.objects
            .filter(lang=self.lang)
            .values_list('facet', 'value', 'publication_ids')
        )
        db_facets_map = {}
        for facet, value, publication_ids in queryset:
            db_facets_map[(facet, value)] = set(publication_ids)
        return db_facets_map

    def compute_union_by_facet(self):
        """
        Считает объединение id объявлений для выбранных пользователем значений фасетов
        """
        result = {}
        for facet in self.facets:
            union = set()
            for value in self.selected_values_by_facet.get(facet, ()):
                union |= self.publication_ids_by_facet_value[(facet, value)]
            result[facet] = union
        return result

    def compute_intersection_of_unions(self, facets):
        if facets:
            sets = (self.union_by_facet[facet] for facet in facets)
            return set.intersection(*sets)

    def compute(self):
        count_by_facet_value = {facet: {} for facet in self.facets}
        selected_facets = set(self.selected_values_by_facet)
        for facet in self.facets:
            other_facets_with_selected_values = selected_facets - {facet}

            other_unions_intersection = (
                self.compute_intersection_of_unions(other_facets_with_selected_values)
            )
            for value in self.values_by_facet[facet]:
                publication_ids = self.publication_ids_by_facet_value[(facet, value)]
                if other_facets_with_selected_values:
                    publication_ids &= other_unions_intersection
                if self.available_publication_ids is not None:
                    publication_ids &= self.available_publication_ids

                count_by_facet_value[facet][value] = len(publication_ids)
        return count_by_facet_value

    def compute_selected_count(self):
        """
        Считает общее количество публикаций по выбранным пользователем галочкам
        (как результат обычного запроса, только по таблице PublicationFacet)
        Если запрос пустой, то есть никаких ограничений не накладывается,
        возвращает None
        """
        if not self.selected_values_by_facet:
            if self.available_publication_ids is not None:
                return len(self.available_publication_ids)
            else:
                return None
        publication_ids = self.compute_intersection_of_unions(self.selected_values_by_facet)
        if self.available_publication_ids is not None:
            publication_ids &= self.available_publication_ids
        return len(publication_ids)
