import itertools
from collections import defaultdict
from operator import itemgetter

from django.db.models import Q

from intranet.femida.src.candidates.choices import DUPLICATION_CASE_RESOLUTIONS, CONTACT_TYPES
from intranet.femida.src.candidates.models import (
    Candidate,
    CandidateAttachment,
    CandidateContact,
    DuplicationCase,
)

from .adapters import to_cand_adapter
from .similarity import SimilarityInfo
from .constants import MAYBE_DUPLICATE
from . import strategies


CONSIDERING_CONTACT_TYPES = (
    CONTACT_TYPES.email,
    CONTACT_TYPES.phone,
    CONTACT_TYPES.skype,
    CONTACT_TYPES.hh,
    CONTACT_TYPES.ah,
    CONTACT_TYPES.linkedin,
)


class DuplicatesFinder:
    """
    Поиск дубликатов осуществляется в два этапа:
        1. Сначала достаем из базы предварительное множество дубликатов,
           то есть, возможно, не все из них дубли. Зато проще запрос.
        2. Проходим по полученному множеству и уточняем похожесть кандидатов.
           В итоге для каждого потенциального дубля мы отдаем вердикт о том,
           что кандидат скорее всего дубликат, но требуется
           ручное подтверждение, либо что это 100% (почти) дубликат.
    """
    def __init__(self, base_qs=None):
        self.base_qs = Candidate.unsafe.all() if base_qs is None else base_qs

    def _get_not_duplicate_ids(self, cand):
        """
        Возвращает список id кандидатов, которых нужно исключить из селекта,
        потому что они ранее были помечены как "не дубликаты"
        """
        duplication_cases = (
            DuplicationCase.unsafe
            .filter(
                (
                    Q(first_candidate_id=cand.id)
                    | Q(second_candidate_id=cand.id)
                )
                & Q(resolution=DUPLICATION_CASE_RESOLUTIONS.not_duplicate)
            )
            .values_list('first_candidate_id', 'second_candidate_id')
        )
        return set(i for i in itertools.chain.from_iterable(duplication_cases) if i is not None)

    def _get_candidate_ids_through_contacts(self, cand):
        contacts_by_type = defaultdict(list)
        for contact_type, account_id in cand.contacts:
            if contact_type in CONSIDERING_CONTACT_TYPES:
                contacts_by_type[contact_type].append(account_id)

        contacts_query = Q()
        for contact_type, values in contacts_by_type.items():
            contacts_query |= Q(type=contact_type) & Q(normalized_account_id__in=values)
        if contacts_query:
            return set(
                CandidateContact.objects
                .filter(is_active=True)
                .filter(contacts_query)
                .values_list('candidate_id', flat=True)
            )
        return set()

    def _get_candidate_ids_through_attachments(self, cand):
        return set(
            CandidateAttachment.unsafe
            .filter(attachment__sha1__in=cand.attachment_hashes)
            .values_list('candidate_id', flat=True)
        )

    def get_raw_duplicates_qs(self, cand):
        """
        Возвращаем потенциальных дубликатов candidate
        """
        include_candidate_ids = (
            self._get_candidate_ids_through_contacts(cand)
            | self._get_candidate_ids_through_attachments(cand)
        )

        exclude_candidate_ids = set()
        if cand.id:
            # Если кандидат уже помечен как не дубликат для каких-то кандидатов,
            # то исключаем их из потенциальных дубликатов
            exclude_candidate_ids = self._get_not_duplicate_ids(cand)
            exclude_candidate_ids.add(cand.id)

        ids = include_candidate_ids - exclude_candidate_ids
        if not ids:
            return Candidate.unsafe.none()

        return (
            self.base_qs
            .alive()
            .filter(id__in=ids)
            .prefetch_related(
                'contacts',
                'candidate_attachments__attachment',
            )
        )

    def find(self, candidate, strategy=None, threshold=MAYBE_DUPLICATE):
        """
        :param candidate: Candidate, CandidateSubmission, CandidateAdapter, dict
        :param strategy: Функция, принимает на вход SimilarityInfo, отдает ответ на вопрос,
            является ли кандидат дубликатом (NOT_DUPLICATE, MAYBE_DUPLICATE, DEFINITELY_DUPLICATE)
        :param threshold: Граница, по которой фильтровать итоговый ответ
        :return: (duplicate, SimilarityInfo, [NOT_DUPLICATE|MAYBE_DUPLICATE|DEFINITELY_DUPLICATE])
        """
        strategy = strategy or strategies.base_strategy

        adapted_cand = to_cand_adapter(candidate)
        raw_duplicates_qs = self.get_raw_duplicates_qs(adapted_cand)
        for duplicate in raw_duplicates_qs:
            similarity = SimilarityInfo(adapted_cand, duplicate)
            decision = strategy(similarity)
            if decision >= threshold:
                yield duplicate, similarity, decision

    def find_top3(self, *args, **kwargs):
        """
        Шорткат, который возвращает 3 результата с максимальным score
        """
        return sorted(list(self.find(*args, **kwargs)), key=itemgetter(1), reverse=True)[:3]


class RotationDuplicatesFinder(DuplicatesFinder):
    """
    Поиск дубликатов для откликов-ротаций.
    Таковыми являются любые кандидаты с таким же логином, как у ротирующегося сотрудника.
    """
    def get_raw_duplicates_qs(self, submission_adapter):
        return (
            self.base_qs
            .alive()
            .filter(login=submission_adapter.login)
            .exclude(id=submission_adapter.id)
            .prefetch_related(
                'contacts',
                'candidate_attachments__attachment',
            )
        )

    def find(self, candidate, strategy=None, threshold=MAYBE_DUPLICATE):
        strategy = strategy or strategies.rotation_strategy
        return super().find(candidate, strategy, threshold)
