from __future__ import annotations

import waffle

from django.conf import settings
from django.contrib.auth import get_user_model
from django.db import IntegrityError, transaction
from django.db.models import Q, Prefetch
from django.db.models.expressions import Exists, OuterRef
from functools import cached_property
from typing import List, Optional

from intranet.femida.src.applications.helpers import active_applications_query
from intranet.femida.src.attachments.models import Attachment
from intranet.femida.src.candidates.choices import (
    CANDIDATE_STATUSES,
    CONTACT_TYPES,
    DUPLICATION_CASE_STATUSES,
    REFERENCE_STATUSES,
)
from intranet.femida.src.candidates.models import (
    Candidate,
    CandidateProfession,
    CandidateResponsible,
    CandidateSkill,
    CandidateCity,
    CandidateContact,
    DuplicationCase,
    ConsiderationHistory,
    CandidateLanguageTag,
)
from intranet.femida.src.candidates.signals import candidate_modified
from intranet.femida.src.core.controllers import update_instance
from intranet.femida.src.core.models import LanguageTag
from intranet.femida.src.core.signals import post_update
from intranet.femida.src.interviews.choices import APPLICATION_STATUSES
from intranet.femida.src.interviews.models import Application
from intranet.femida.src.offers.choices import FORM_TYPES, OFFER_STATUSES
from intranet.femida.src.permissions.helpers import get_manager
from intranet.femida.src.vacancies.choices import VACANCY_ROLES
from intranet.femida.src.vacancies.models import VacancyMembership


User = get_user_model()

candidate_is_current_employee_subquery = Exists(
    User.objects
    .filter(
        username=OuterRef('login'),
        is_dismissed=False,
    )
    .values('id')
)


def blank_modify_candidate(candidate):
    candidate.save(update_fields=['modified'])
    candidate_modified.send(sender=Candidate, candidate=candidate)


def _get_candidate_ids_by_applications(user, roles):
    return (
        Application.unsafe.filter(
            status__in=[
                APPLICATION_STATUSES.in_progress,
            ],
            vacancy__in=VacancyMembership.unsafe.filter(
                member=user,
                role__in=roles,
            ).values('vacancy')
        ).values_list('candidate', flat=True)
    )


def get_candidate_ids_for_recruiter(user):
    return set(
        CandidateResponsible.objects
        .filter(
            user=user,
            candidate__status=CANDIDATE_STATUSES.in_progress,
        )
        .values_list('candidate', flat=True)
    )


def get_actual_candidates(user):
    """
    Поскольку множество актуальных кандидатов входит во множество доступных, то будем использовать
    здесь unsafe менеджер
    """
    from intranet.femida.src.permissions.context import context, EmptyContextException

    try:
        context_user = context.user
    except EmptyContextException:
        context_user = None

    # Если пытаемся получить актуальных кандидатов юзера,
    # в контексте которого мы находимся, то нет смысла проверять доступы
    # потому что множество актуальных кандидатов находится внутри доступных.
    manager = get_manager(Candidate, unsafe=(context_user is None or user == context_user))
    queryset = manager.alive()

    roles_for_anyone = [
        VACANCY_ROLES.hiring_manager,
        VACANCY_ROLES.head,
        VACANCY_ROLES.responsible,
    ]
    roles_for_recruiter = roles_for_anyone + [VACANCY_ROLES.main_recruiter, VACANCY_ROLES.recruiter]

    if not user.is_recruiter:
        cand_ids_for_anyone = set(
            _get_candidate_ids_by_applications(user, roles_for_anyone)
        )
        return queryset.filter(id__in=cand_ids_for_anyone)

    cand_ids_for_recruiter = get_candidate_ids_for_recruiter(user)
    cand_ids_for_recruiter |= set(
        _get_candidate_ids_by_applications(user, roles_for_recruiter)
    )
    return queryset.filter(id__in=cand_ids_for_recruiter)


def add_profession_from_vacancy(candidate, vacancy):
    if not vacancy.profession:
        return

    is_addition_needed = (
        vacancy.professional_sphere_id != settings.DEVELOPMENT_PROF_SPHERE_ID
        or not waffle.switch_is_active('disable_profession_for_dev_prof_sphere_addition')
    )
    if is_addition_needed:
        try:
            with transaction.atomic():
                CandidateProfession.objects.create(
                    candidate=candidate,
                    profession=vacancy.profession,
                    professional_sphere=vacancy.professional_sphere,
                )
        except IntegrityError:
            pass


def add_skills_from_vacancy(candidate, vacancy):
    if waffle.switch_is_active('disable_skills_from_vacancy_addition'):
        return

    existing_skill_ids = set(candidate.candidate_skills.values_list('skill_id', flat=True))
    new_skills = []
    for skill in vacancy.skills.all():
        if skill.id not in existing_skill_ids:
            new_skills.append(
                CandidateSkill(
                    candidate=candidate,
                    skill=skill,
                    confirmed_by=[],
                )
            )
    created_candidate_skills = CandidateSkill.objects.bulk_create(new_skills)
    post_update.send(
        sender=CandidateSkill,
        queryset=created_candidate_skills,
    )


def add_cities_from_vacancy(candidate, vacancy):
    if waffle.switch_is_active('disable_cities_from_vacancy_addition'):
        return

    existing_city_ids = set(candidate.target_cities.values_list('id', flat=True))
    created_candidate_cities = CandidateCity.objects.bulk_create([
        CandidateCity(
            candidate=candidate,
            city=city,
        )
        for city in vacancy.cities.all()
        if city.id not in existing_city_ids
    ])
    post_update.send(
        sender=CandidateCity,
        queryset=created_candidate_cities,
    )


def get_candidates_with_last_active_application_for_vacancy(vacancy):
    """
    Отдаем кандидатов, у которых существуют активные претенденства на искомую вакансию и не
    существует активных претендентств на какие-либо другие.
    """
    active_applications_qs = Application.unsafe.filter(active_applications_query)

    actual_vacancy_candidates_ids = list(
        active_applications_qs
        .filter(vacancy=vacancy)
        .values_list('candidate_id', flat=True)
    )
    if not actual_vacancy_candidates_ids:
        return Candidate.unsafe.none()
    return (
        Candidate.unsafe
        .filter(id__in=actual_vacancy_candidates_ids)
        .exclude(
            id__in=(
                active_applications_qs
                .filter(candidate_id__in=actual_vacancy_candidates_ids)
                .exclude(vacancy=vacancy)
                .values('candidate_id')
            )
        )
    )


def get_candidates_modified_after_dt(from_dt):
    return (
        Candidate.unsafe.alive()
        .filter(
            Q(modified__gt=from_dt)
            | Q(id__in=Application.unsafe.filter(modified__gt=from_dt).values('candidate_id'))
            | Q(id__in=CandidateContact.objects.filter(modified__gt=from_dt).values('candidate_id'))
        )
    )


def get_reference_award_tag(grade, reference_status):
    """
    Отдаёт тег, который означает награду по рекомендации,
    в зависимости от грейда сотрудника, нанятого по этой рекомендации.
    """
    if reference_status == REFERENCE_STATUSES.approved_without_benefits:
        return 'сувениры'
    for _max, tag in settings.REFERENCE_GRADE_TO_AWARD_TAG:
        if grade <= _max:
            return tag


def _get_candidate_prefetch(qs, lookup, prefix='', to_attr=None):
    prefix = '' if not prefix else prefix + '__'
    return Prefetch(prefix + lookup, qs, to_attr=to_attr)


def get_candidate_professions_prefetch(prefix='', to_attr=None):
    """
    Отдаёт оптимизированнй Prefetch профессий кандидата
    - всё по профессиям получается из БД одним запросом:
    """
    qs = CandidateProfession.objects.select_related('profession__professional_sphere')
    return _get_candidate_prefetch(qs, 'candidate_professions', prefix, to_attr)


def get_candidate_unique_attachments_prefetch(prefix='', to_attr=None):
    qs = (
        Attachment.objects
        .distinct('sha1')
        .order_by('sha1', '-created')
    )
    return _get_candidate_prefetch(qs, 'attachments', prefix, to_attr)


def close_candidate(candidate):
    """
    Закрывает кандидата и затирает необходимые поля
    """
    update_instance(candidate, data={
        'status': CANDIDATE_STATUSES.closed,
        'source': '',
        'source_description': '',
    })
    candidate.responsibles.clear()
    if 'responsibles_by_role' in candidate.__dict__:
        del candidate.responsibles_by_role


def _get_main_contact(candidate, contact_type):
    contact = (
        candidate.contacts
        .filter(
            type=contact_type,
            is_active=True,
        )
        .order_by('-is_main')
        .first()
    )
    if contact:
        return contact.normalized_account_id or contact.account_id


def get_main_email(candidate):
    return _get_main_contact(candidate, CONTACT_TYPES.email)


def get_main_phone(candidate):
    return _get_main_contact(candidate, CONTACT_TYPES.phone)


def get_active_duplication_case_ids(candidate_id: int) -> List[int]:
    return list(
        DuplicationCase.objects
        .filter(status=DUPLICATION_CASE_STATUSES.new)
        .filter(
            Q(first_candidate_id=candidate_id)
            | Q(second_candidate_id=candidate_id)
        )
        .values_list('id', flat=True)
    )


def get_extended_status_changed_at_subquery():
    return (
        ConsiderationHistory.objects
        .filter(consideration_id=OuterRef('id'))
        .order_by('-changed_at')
        .values_list('changed_at', flat=True)
    )


def get_active_tags(candidate: Candidate) -> list[str]:
    return [t.tag.name for t in candidate.candidate_tags.all() if t.is_active]


def is_russian_speaking_candidate(consideration_id):
    """
    Вычисляет, является ли кандидат русскоговорящим.
    Пока вычисляем не в вакууме, а только в рамках конкретного рассмотрения.

    Сейчас в Фемиде нет явной разметки, указывающей на то, какими языками владеют кандидаты.
    Считаем, что кандидат является русскоговорящим,
    если в рамках заданного рассмотрения ему не выставляли международный оффер,
    и он не претендовал на международную вакансию.

    Это не надёжный, но пока, кажется, самый удачный способ предположить,
    что кандидат владеет русским языком.
    """
    has_international_applications = (
        Application.unsafe
        .filter(consideration_id=consideration_id)
        .filter(
            Q(offers__form_type=FORM_TYPES.international)
            & ~Q(offers__status__in=(OFFER_STATUSES.rejected, OFFER_STATUSES.deleted))
            | Q(vacancy__geography_international=True)
        )
        .exists()
    )
    return not has_international_applications


class CandidateLanguageTagEqContext:
    def __init__(self, tag: CandidateLanguageTag = None, candidate_id: int = None,
                 tag_id: int = None, is_main: bool = False):
        self._tag = tag or CandidateLanguageTag(
            candidate_id=candidate_id,
            tag_id=tag_id,
            is_main=is_main,
        )

    def __eq__(self, other: CandidateLanguageTagEqContext) -> bool:
        return (self._tag.candidate_id == other._tag.candidate_id and
                self._tag.tag_id == other._tag.tag_id)

    def main_eq(self, other: CandidateLanguageTagEqContext):
        return self._tag.is_main == other._tag.is_main

    @property
    def unwrap(self) -> CandidateLanguageTag:
        return self._tag


class CandidatePolyglot:

    def __init__(self, candidate: Candidate):
        self._candidate = candidate
        self._main_language = None
        self._spoken_languages = []

    def update_known_languages(self, main_language: Optional[LanguageTag],
                               spoken_languages: List[LanguageTag]):
        self._main_language = main_language
        self._spoken_languages = spoken_languages or []

        # Обязательно сначала удаляем неиспользуемые языки,
        # так как среди них может быть и бывший родной язык
        self._remove_unused()
        self._update_and_create()

    @cached_property
    def _all_new_candidate_tags(self) -> List[CandidateLanguageTagEqContext]:
        if self._main_language:
            candidate_main_tags = [CandidateLanguageTagEqContext(
                candidate_id=self._candidate.id,
                tag_id=self._main_language.id,
                is_main=True
            )]
        else:
            candidate_main_tags = []
        spoken_candidate_tags = [
            CandidateLanguageTagEqContext(
                candidate_id=self._candidate.id,
                tag_id=tag.id,
                is_main=False)
            for tag in self._spoken_languages
        ]

        # Родной язык обязательно должен быть в конце списка, так как это используется
        # в остальной логике.
        return spoken_candidate_tags + candidate_main_tags

    def _remove_unused(self) -> None:
        for old_tag in self._candidate_current_language_tags():
            new_tag = self._search_tag_in_list(old_tag, self._all_new_candidate_tags)
            if new_tag is None:
                old_tag.unwrap.delete()

    def _update_and_create(self) -> None:
        # self._all_new_candidate_tags содержит новый родной язык в конце списка,
        # таким образом к моменту его обработки изначальный родной язык будет либо удален,
        # либо помечен неродным, либо будет совпадать
        # Родной язык может быть только один
        old_tags = self._candidate_current_language_tags()
        used_tags = []
        for new_candidate_tag in self._all_new_candidate_tags:
            old_tag = self._search_tag_in_list(new_candidate_tag, old_tags)
            if old_tag:
                if not old_tag.main_eq(new_candidate_tag):
                    old_tag.unwrap.is_main = new_candidate_tag.unwrap.is_main
                    old_tag.unwrap.save()
            else:
                used_tag = self._search_tag_in_list(new_candidate_tag, used_tags)
                if used_tag:
                    if not used_tag.main_eq(new_candidate_tag):
                        used_tag.unwrap.is_main = new_candidate_tag.unwrap.is_main
                        used_tag.unwrap.save()
                else:
                    new_candidate_tag.unwrap.save()
                    used_tags.append(new_candidate_tag)

    @staticmethod
    def _search_tag_in_list(needle_tag: CandidateLanguageTagEqContext,
                            tags_list: List[CandidateLanguageTagEqContext],
                            ) -> Optional[CandidateLanguageTagEqContext]:
        for tag in tags_list:
            if needle_tag == tag:
                return tag
        return None

    def _candidate_current_language_tags(self) -> List[CandidateLanguageTagEqContext]:
        return [CandidateLanguageTagEqContext(tag=tag) for tag in self._candidate.candidate_language_tags.all()]
