import os
import waffle

from functools import cached_property
from typing import Set

from dateutil.relativedelta import relativedelta
from django.contrib.auth import get_user_model
from django.db.models import Q, Exists, OuterRef
from django.utils import timezone

from intranet.femida.src.candidates.choices import (
    CANDIDATE_STATUSES,
    ROTATION_STATUSES,
    SUBMISSION_STATUSES,
)
from intranet.femida.src.candidates.managers import CandidateManager
from intranet.femida.src.core.switches import TemporarySwitch
from intranet.femida.src.offers.choices import SOURCES
from intranet.femida.src.utils.cache import memoize
from intranet.femida.src.vacancies.choices import OPEN_VACANCY_STATUSES, CLOSED_VACANCY_STATUSES

from .base import PermQuery, PermManager, PermQueryBuilder


User = get_user_model()


@memoize(5 * 60)
def _get_relevant_prof_sphere_ids(user) -> Set[int]:
    from intranet.femida.src.vacancies.controllers import (
        get_aa_prof_sphere_ids,
        get_relevant_prof_sphere_ids_qs,
    )
    result = set(get_relevant_prof_sphere_ids_qs(user).distinct())
    if user.is_aa:
        result.update(get_aa_prof_sphere_ids(user.aa_type))
    return result


def _is_query_prof_sphere_experiment_enabled():
    """
    Функция отдаёт состояние эксперимента по ускорению подзапроса для
    доступа к кандидату через проф.сферы.

    Текущая версия подзапроса устроена таким образом:
    1. Мы получаем все проф.сферы, к которым текущий пользователь имеет отношение
    2. Получаем всех кандидатов, которые относятся к этим проф.сферам
    3. Проверяем, входит ли наш кандидат в подмножество из п.2

    Экспериментальная версия подзапроса устроена так:
    1. Получаем все профессии, к которым текущий пользователь имеет отношение
    2. Для каждого кандидата получаем его профессии
    3. Смотрим, есть ли среди них профессии из п.1

    Эксперимент включается/отключаетя через переменную окружения.
    Соответственно, для этого нужен редеплой.
    Сделано так специально, чтобы максимально исключить сайд-эффекты,
    в виде походов в БД и кэш (это бы происходило при использовании waffle)
    """
    return int(os.getenv('CANDIDATE_QUERY_PROF_SPHERE_EXPERIMENT', '0'))


def _is_cache_prof_sphere_experiment_enabled():
    """
    Функция отдаёт состояние эксперимента по ускорению запросов по кандидатам.
    В этом эксперименте получаем проф.сферы, относящиеся к текущему пользователю,
    не через подзапрос, а через дополнительный запрос.
    Но также, используем кэш, чтобы для каждого пользоавателя получать проф.сферы не чаще,
    чем раз в 5 минут.
    """
    return int(os.getenv('CANDIDATE_CACHE_PROF_SPHERE_EXPERIMENT', '0'))


class CandidatePermQueryBuilder(PermQueryBuilder):
    """
    https://st.yandex-team.ru/FEMIDA-888
    """
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        ls = lambda s: s.lstrip('__')

        self.id_k = ls(self.prefix + '_id')
        self.is_hidden_k = ls(self.prefix + '__is_hidden')
        self.login_k = ls(self.prefix + '__login')
        self.login_in_k = self.login_k + '__in'
        self.candidate_in_k = ls(self.prefix + '_id__in')
        self.status_k = ls(self.prefix + '__status')
        self.modified_lt_k = ls(self.prefix + '__modified__lt')

    def add_perm_filter(self, query):
        if _is_query_prof_sphere_experiment_enabled():
            query.add_annotation(self.by_professions_annotation, '_suits_by_prof_sphere')
        query = super().add_perm_filter(query)

        # Note: аннотация нам нужна только для фильтрации,
        # в самом селекте она не нужна. Поэтому чтобы не вычислять лишнего
        # на каждый запрос, просто удаляем аннотацию сразу после применения фильтра
        query.annotations.pop('_suits_by_prof_sphere', None)

        return query

    def get_perm_q(self):
        # TODO: убрать свитч после релиза FEMIDA-7431
        if self.disable_employees:
            if self.user.is_hrbp:
                return self._rkn_q & self._hrbp_perm_q

            if self.user.is_recruiting_manager:
                return self._rkn_q & self._recruiting_manager_perm_q

            if self.user.is_recruiter or self.user.is_auditor:
                return self._rkn_q & self._recruiter_perm_q

            if self.user.is_recruiter_assessor:
                return self._rkn_q & self._recruiter_assessor_perm_q

            if self.user.is_aa:
                return self._aa_user_perm_q

            return self._regular_user_perm_q
        else:
            return self.get_perm_old_q()

    # TODO: убрать после релиза FEMIDA-7431
    def get_perm_old_q(self):
        query = self.not_himself_q

        if self.user.is_recruiter or self.user.is_recruiter_assessor:
            if waffle.switch_is_active('is_rkn'):
                query &= self.rkn_q

        if self.user.is_recruiting_manager or self.user.is_hrbp:
            return query

        if self.user.is_recruiter or self.user.is_auditor:
            special_q = self.not_hidden_vacancy_q
        elif self.user.is_recruiter_assessor:
            special_q = (
                self.not_hidden_vacancy_q
                & self.not_hidden_candidate_q
                & self.not_employees_q
            )
        else:
            special_q = (
                self.by_professions_q
                & self.not_hidden_vacancy_q
                & self.not_hidden_candidate_q
            )
            if not self.user.is_aa:
                special_q &= self.not_employees_q

        return (
            query
            & (
                self.vacancies_membership_q
                | self.interviewed_q
                | special_q
            )
        )

    # TODO: после релиза FEMIDA-7431 сюда можно заинлайнить self.rkn_q
    @property
    def _rkn_q(self):
        if self.rkn_is_coming and (self.user.is_recruiter or self.user.is_recruiter_assessor):
            return self.rkn_q
        else:
            return Q()

    @property
    def _hrbp_perm_q(self):
        return self.not_himself_q

    @property
    def _recruiting_manager_perm_q(self):
        return self.not_himself_q & (self.not_employees_and_interns_q | self.rotating_employees_q)

    @property
    def _recruiter_perm_q(self):
        return (
            self.not_himself_q
            & (
                self.vacancies_membership_q
                | self.interviewed_q
                | (
                    self.not_hidden_vacancy_q
                    & (self.not_employees_and_interns_q | self.rotating_employees_q)
                )
            )
        )

    @property
    def _recruiter_assessor_perm_q(self):
        return (
            self.not_himself_q
            & (
                self.vacancies_membership_q
                | self.interviewed_q
                | (self.not_hidden_vacancy_q & self.not_hidden_candidate_q & self.not_employees_q)
            )
        )

    @property
    def _aa_user_perm_q(self):
        return (
            self.not_himself_q
            & (
                self.vacancies_membership_q
                | self.aa_interviewed_q
                | (
                    self.by_professions_q
                    & self.not_hidden_vacancy_q
                    & self.not_hidden_candidate_q
                    & (self.not_employees_and_interns_q | self.rotating_employees_q)
                )
            )
        )

    @property
    def _regular_user_perm_q(self):
        return (
            self.not_himself_q
            & (
                self.vacancies_membership_q
                | self.interviewed_q
                | (
                    self.not_hidden_vacancy_q
                    & self.not_hidden_candidate_q
                    & self.not_employees_q
                    & self.by_professions_q
                )
            )
        )

    @property
    def rotating_employees_q(self):
        from intranet.femida.src.candidates.models import Rotation

        new_rotations_q = Q(status=ROTATION_STATUSES.new)
        approved_rotations_q = Q(
            status=ROTATION_STATUSES.approved,
            submission__status=SUBMISSION_STATUSES.new
        )
        rotation_in_progress_q = Q(
            status=ROTATION_STATUSES.approved,
            submission__status=SUBMISSION_STATUSES.closed,
            submission__candidate__status=CANDIDATE_STATUSES.in_progress,
            submission__candidate__source=SOURCES.rotation
        )

        rotations_q = new_rotations_q | approved_rotations_q | rotation_in_progress_q
        usernames = Rotation.objects.filter(rotations_q).values('created_by__username')
        return Q(**{self.login_in_k: usernames})

    @property
    def rkn_q(self):
        return ~Q(**{
            self.status_k: CANDIDATE_STATUSES.closed,
            self.modified_lt_k: timezone.now() - relativedelta(years=5),
        })

    @property
    def not_himself_q(self):
        return ~Q(**{self.login_k: self.user.username})

    @property
    def interviewed_not_filtered_q(self):
        from intranet.femida.src.interviews.models import Interview

        return Q(
            **{self.candidate_in_k: (
                Interview.unsafe
                .alive()
                .filter(interviewer=self.user)
                .values('candidate')
            )}
        )

    @property
    def not_hidden_vacancy_q(self):
        from intranet.femida.src.applications.models import Application

        return ~Q(
            **{self.candidate_in_k: (
                Application.unsafe
                .exclude(vacancy__status='closed')
                .filter(vacancy__is_hidden=True)
                .values('candidate')
            )}
        )

    @property
    def vacancy_memberships_qs(self):
        from intranet.femida.src.vacancies.models import VacancyMembership

        return VacancyMembership.unsafe.filter(member=self.user)

    @property
    def vacancies_membership_not_filtered_q(self):
        from intranet.femida.src.applications.models import Application

        return Q(
            **{self.candidate_in_k: (
                Application.unsafe
                .filter(self._vacancy_memberships_applications_q)
                .values('candidate')
            )}
        )

    @property
    def _prof_sphere_q(self):
        from intranet.femida.src.vacancies.controllers import (
            get_aa_prof_sphere_ids,
            get_relevant_prof_sphere_ids_qs,
        )

        if _is_cache_prof_sphere_experiment_enabled():
            prof_sphere_ids = _get_relevant_prof_sphere_ids(self.user)
            return Q(professional_sphere__in=prof_sphere_ids)

        _prof_sphere_q = Q(professional_sphere__in=get_relevant_prof_sphere_ids_qs(self.user))
        if self.user.is_aa:
            _prof_sphere_q |= Q(
                professional_sphere__in=get_aa_prof_sphere_ids(self.user.aa_type)
            )

        return _prof_sphere_q

    @property
    def by_professions_q(self):
        from intranet.femida.src.candidates.models import CandidateProfession
        if _is_query_prof_sphere_experiment_enabled():
            return Q(_suits_by_prof_sphere=True)
        return Q(
            **{self.candidate_in_k: (
                CandidateProfession.objects
                .filter(self._prof_sphere_q)
                .values('candidate')
            )}
        )

    @property
    def by_professions_annotation(self):
        from intranet.femida.src.candidates.models import CandidateProfession
        return Exists(
            CandidateProfession.objects
            .filter(self._prof_sphere_q)
            .filter(candidate_id=OuterRef(self.id_k))
            .values('id')
        )

    @property
    def not_employees_and_interns_q(self):
        return self._not_employees_custom_login__in_q(self.login_in_k, ~Q(is_intern=True))

    @property
    def not_employees_q(self):
        return self._not_employees_custom_login__in_q(self.login_in_k)

    @staticmethod
    def _not_employees_custom_login__in_q(login, extra_q=Q()):
        users = User.objects.filter(is_dismissed=False).filter(extra_q)
        return ~Q(
            **{login: users.values('username')}
        )

    @property
    def not_hidden_candidate_q(self):
        return Q(**{self.is_hidden_k: False})

    @property
    def vacancies_membership_q(self):
        """"  См. FEMIDA-7066, HRTOOLS-458, FEMIDA-7431
              Ограничиваем доступ к действующим/ротирующимся сотрудникам.
              Предоставляем доступ ко всей информации только если юзер явно
              добавлен на открытую вакансию, на которую претенденствует сотрудник.
              Рассмотрение сотрудника должно быть открыто.
              Доступ к кандидатам-не-сотрудникам оставляем как раньше.
        """
        from intranet.femida.src.applications.models import Application

        # TODO: убрать свитч после релиза FEMIDA-7431
        if self.disable_employees:
            return Q(
                **{self.candidate_in_k: (
                    Application.unsafe.filter(
                        self._vacancy_applications_q
                        & Q(candidate__status=CANDIDATE_STATUSES.in_progress)
                    ).values('candidate')
                )}
            )
        else:
            """"  См. FEMIDA-7066, HRTOOLS-458
                  Ограничиваем доступ к действующим/ротирующимся сотрудникам.
                  Предоставляем доступ ко всей информации только если юзер явно
                  добавлен на открытую вакансию, на которую претенденствует сотрудник.
                  Доступ к кандидатам-не-сотрудникам оставляем как раньше.
                  Для АА интервьюверов ничего не меняем.
            """
            if not self.user.is_aa:
                return Q(
                    **{self.candidate_in_k: (
                        Application.unsafe.filter(
                            self._vacancy_applications_q
                        ).values('candidate')
                    )}
                )

            else:
                return self.vacancies_membership_not_filtered_q

    @property
    def aa_interviewed_q(self):
        from intranet.femida.src.interviews.models import Interview

        return Q(
            **{self.candidate_in_k: (
                Interview.unsafe
                    .alive()
                    .filter(interviewer=self.user,
                            candidate__status=CANDIDATE_STATUSES.in_progress)
                    .values('candidate')
            )}
        )

    @property
    def interviewed_q(self):
        """"  См. FEMIDA-7066, HRTOOLS-458, FEMIDA-7431
              Оставляем интервьюверам, не добавленных явно на открытую
              вакансию, возможность посмотреть на кандидата, если последний
              претендует на открытую вакансию и его расммотрение не завершено.
        """
        from intranet.femida.src.interviews.models import Interview

        # TODO: убрать свитч после релиза FEMIDA-7431
        if self.disable_employees:
            return Q(
                **{self.candidate_in_k: (
                    Interview.unsafe
                        .alive()
                        .filter(interviewer=self.user,
                                application__vacancy__status__in=OPEN_VACANCY_STATUSES._db_values,
                                candidate__status=CANDIDATE_STATUSES.in_progress)
                        .values('candidate')
                )}
            )
        else:
            """"  См. FEMIDA-7066, HRTOOLS-458
                  Оставляем интервьюверам, не добавленных явно на открытую
                  вакансию, возможность посмотреть на кандидата и все его
                  интервью пока она открыта.
                  Для АА интервьюверов ничего не меняем.
            """
            if not self.user.is_aa:
                from intranet.femida.src.interviews.models import Interview

                return Q(
                    **{self.candidate_in_k: (
                        Interview.unsafe
                            .alive()
                            .filter(interviewer=self.user,
                                    application__vacancy__status__in=OPEN_VACANCY_STATUSES._db_values)
                            .values('candidate')
                    )}
                )
            else:
                return self.interviewed_not_filtered_q

    @property
    def _vacancy_applications_q(self):
        return (
            self._vacancy_memberships_applications_q &
            (
                self._open_vacancy_applications_q |
                self._closed_vacancy_not_employees_applications_q
            )
        )

    @property
    def _vacancy_memberships_applications_q(self):
        return Q(vacancy_id__in=self.vacancy_memberships_qs.values('vacancy'))

    @property
    def _open_vacancy_applications_q(self):
        return Q(vacancy__status__in=OPEN_VACANCY_STATUSES._db_values)

    @property
    def _closed_vacancy_not_employees_applications_q(self):
        return (
            Q(vacancy__status__in=CLOSED_VACANCY_STATUSES._db_values) &
            self._not_employees_custom_login__in_q('candidate__login__in')
        )

    @cached_property
    def disable_employees(self):
        return waffle.switch_is_active(TemporarySwitch.DISABLE_ACCESS_TO_EMPLOYEES)

    @cached_property
    def rkn_is_coming(self):
        return waffle.switch_is_active('is_rkn')


class CandidatePermQuery(PermQuery):

    _perm_query_builder_cls = CandidatePermQueryBuilder


class CandidatePermManager(CandidateManager, PermManager):

    _perm_query_cls = CandidatePermQuery
