import waffle

from django.db.models import Q
from django.utils import timezone
from django.utils.functional import cached_property

from intranet.femida.src.applications.controllers import bulk_close_applications_by_id
from intranet.femida.src.applications.helpers import active_applications_query
from intranet.femida.src.candidates.choices import (
    CANDIDATE_STATUSES,
    CHALLENGE_STATUSES,
    CHALLENGE_TYPES,
    DUPLICATION_CASE_STATUSES,
    DUPLICATION_CASE_RESOLUTIONS,
    REFERENCE_EVENTS,
)
from intranet.femida.src.candidates.considerations.controllers import (
    create_consideration,
    get_relevant_consideration,
    get_or_create_relevant_consideration,
)
from intranet.femida.src.candidates.considerations.helpers import archive_consideration
from intranet.femida.src.candidates.controllers import (
    update_candidate_responsibles,
    update_or_create_candidate,
)
from intranet.femida.src.candidates.helpers import blank_modify_candidate, close_candidate
from intranet.femida.src.candidates.models import Verification, Challenge
from intranet.femida.src.candidates.startrek.issues import notify_for_candidate_approval
from intranet.femida.src.candidates.tasks import (
    send_candidate_reference_event_task,
    close_rotation_task,
    cancel_rotation_task,
)
from intranet.femida.src.certifications.helpers import get_considerations_for_certification
from intranet.femida.src.certifications.models import Certification
from intranet.femida.src.communications.controllers import (
    bulk_create_internal_messages,
    update_or_create_note,
    update_or_create_external_message,
)
from intranet.femida.src.core.signals import post_update, post_bulk_create
from intranet.femida.src.core.workflow import Action, Workflow, WorkflowError
from intranet.femida.src.interviews.choices import (
    APPLICATION_STATUSES,
    APPLICATION_SOURCES,
    APPLICATION_RESOLUTIONS,
    APPLICATION_CLOSE_REASONS,
    INTERVIEW_TYPES,
    INTERVIEW_GRADABLE_TYPES,
    INTERVIEW_STATES,
    INTERVIEW_RESOLUTIONS,
)
from intranet.femida.src.interviews.controllers import create_interviews_for_round
from intranet.femida.src.interviews.helpers import (
    get_interview_direction_ids,
    is_unfinished_interview_exists,
)
from intranet.femida.src.interviews.models import (
    Application,
    Interview,
    InterviewRound,
    InterviewRoundTimeSlot,
)
from intranet.femida.src.interviews.tasks import send_interview_survey_task
from intranet.femida.src.notifications.candidates import (
    CandidateCreatedNotification,
    CandidateUpdatedNotification,
    CandidateProposedNotification,
    CandidateOpenedNotification,
    CandidateClosedNotification,
    send_verification_form_to_candidate,
    send_certification_to_candidate,
)
from intranet.femida.src.offers.choices import OFFER_STATUSES
from intranet.femida.src.offers.tasks import generate_candidate_offer_pdf_task
from intranet.femida.src.startrek.utils import IssueDoesNotExist
from intranet.femida.src.utils.switches import (
    is_candidate_approval_with_application_enabled,
    is_candidate_main_recruiter_enabled,
)
from intranet.femida.src.vacancies.choices import ACTIVE_VACANCY_STATUSES, VACANCY_ROLES
from intranet.femida.src.vacancies.helpers import get_suitable_vacancies
from intranet.femida.src.vacancies.ranking import VACANCY_FACTORS, rank_proposals, get_factors
from intranet.femida.src.vacancies.models import Vacancy
from intranet.femida.src.yt.tasks import save_interview_round_data_in_yt

# TODO: Убрать свитч? FEMIDA-7216
#       За подробностями зачем это нужно -- к mmozgaleva@
DISABLE_SURVEY_WAFFLE_SWITCH = 'disable_surveys'


class CreateAction(Action):

    def has_permission(self):
        return (
            self.user.is_recruiter
            or self.user.is_recruiter_assessor
            or self.user.is_robot_femida
        )

    def perform(self, need_notify=True, **params):
        self.instance = update_or_create_candidate(params, initiator=self.user)
        self.extra_data['consideration'] = create_consideration(
            candidate_id=self.instance.id,
            created_by=self.user,
            started=self.instance.created,
        )
        if need_notify:
            notification = CandidateCreatedNotification(self.instance, self.user)
            notification.send()
        return self.instance


class UpdateAction(Action):

    def has_permission(self):
        return self.user.is_recruiter or self.user.is_recruiter_assessor

    def perform(self, **params):
        new_first_name = params.get('first_name')
        new_last_name = params.get('last_name')
        is_name_updated = bool(
            new_first_name and self.instance.first_name != new_first_name
            or new_last_name and self.instance.last_name != new_last_name
        )
        self.instance = update_or_create_candidate(
            data=params,
            initiator=self.user,
            instance=self.instance,
        )
        if is_name_updated:
            generate_candidate_offer_pdf_task.delay(self.instance.id)
        notification = CandidateUpdatedNotification(self.instance, self.user)
        notification.send()
        return self.instance


class OpenAction(Action):

    valid_statuses = (
        CANDIDATE_STATUSES.closed,
    )

    def has_permission(self):
        return self.user.is_recruiter or self.user.is_robot_femida

    def perform(self, **params):
        self.instance.status = CANDIDATE_STATUSES.in_progress
        self.instance.save()

        # FEMIDA-4914: Поле больше не используется, по пока как и раньше
        # обнуляем его при открытии
        params['talent_pool_rate'] = None
        if is_candidate_main_recruiter_enabled():
            params.setdefault('main_recruiter', self.user)

        self.instance = update_or_create_candidate(
            data=params,
            instance=self.instance,
            initiator=self.user,
        )
        last_consideration = self.instance.considerations.filter(is_last=True).last()
        if last_consideration:
            last_consideration.is_last = False
            last_consideration.save(update_fields=['is_last'])

        consideration = create_consideration(
            candidate_id=self.instance.id,
            created_by=self.user,
            started=self.instance.modified,
        )
        self.extra_data['consideration'] = consideration
        notification = CandidateOpenedNotification(self.instance, self.user)
        notification.send()
        return self.instance


class CreateProposalsAction(Action):

    valid_statuses = (
        CANDIDATE_STATUSES.in_progress,
    )

    def has_permission(self):
        return self.user.is_recruiter

    @cached_property
    def consideration(self):
        return get_or_create_relevant_consideration(self.instance, self.user)

    def _get_vacancies(self, **search_data):
        qs = (
            Vacancy.objects
            .filter(
                status__in=ACTIVE_VACANCY_STATUSES._db_values,
                is_hidden=False,
            )
            .exclude(id__in=self.consideration.applications.values('vacancy_id'))
        )
        qs = get_suitable_vacancies(qs, **search_data)

        for factor in VACANCY_FACTORS:
            qs = factor.annotate_qs(qs)

        return qs

    @cached_property
    def _is_first_proposal(self):
        return not Application.unsafe.filter(
            consideration_id=self.consideration.id,
            source=APPLICATION_SOURCES.proposal,
        ).exists()

    def _get_interview_direction_ids(self):
        """
        Отдаёт множество ID всех отделов-направлений,
        в которых проводились секции с данным кандидатом
        в текущем рассмотрении
        """
        return get_interview_direction_ids(
            self.consideration.interviews
            .alive()
            .gradable()
        )

    def _get_factors(self, vacancy, filter_params):
        factors = get_factors(vacancy, filter_params)
        factors['is_first_proposal'] = self._is_first_proposal
        factors['vacancy_skills'] = [s.name for s in vacancy.skills.all()]
        factors['filter_skills'] = [s.name for s in filter_params.get('skills', [])]
        return factors

    def _create_applications(self, vacancies, filter_params):
        filter_params['interview_direction_ids'] = self._get_interview_direction_ids()
        # Массово создаем прет-ва на vacancies
        applications = [
            Application(
                candidate=self.instance,
                consideration=self.consideration,
                vacancy=vacancy,
                created_by=self.user,
                status=APPLICATION_STATUSES.in_progress,
                source=APPLICATION_SOURCES.proposal,
                proposal_factors=self._get_factors(vacancy, filter_params),
            )
            for vacancy in vacancies
        ]
        if waffle.switch_is_active('enable_proposals_ranking'):
            applications = rank_proposals(applications)

        Application.objects.bulk_create(applications)
        post_bulk_create.send(
            sender=Application,
            queryset=applications,
        )
        return applications

    def perform(self, **params):
        interviews = params.pop('interviews')
        comment = params.pop('comment')

        cities = params.get('cities')
        professions = params.get('professions')
        skills = params.get('skills')
        pro_level_min = params.get('pro_level_min')
        pro_level_max = params.get('pro_level_max')

        vacancies = self._get_vacancies(**params)
        applications = self._create_applications(
            vacancies=vacancies,
            filter_params=params,
        )

        self.extra_data['created_applications_count'] = len(applications)
        if not applications:
            self.raise_error(
                code='vacancies_not_found',
                fail_silently=True,
            )

        if comment:
            messages = bulk_create_internal_messages(
                applications=applications,
                text=comment,
                initiator=self.user,
            )
            message = messages[0] if messages else None
        else:
            message = None

        self.instance = update_or_create_candidate(
            data={
                'target_cities': cities,
                'skills': skills,
                'new_professions': professions,
            },
            initiator=self.user,
            instance=self.instance,
        )

        notification = CandidateProposedNotification(
            instance=self.instance,
            initiator=self.user,
            applications=applications,
            pro_level_min=pro_level_min,
            pro_level_max=pro_level_max,
            professions=professions,
            interviews=interviews,
            message=message,
        )
        notification.send()

        return self.instance


class CloseAction(Action):

    valid_statuses = (
        CANDIDATE_STATUSES.in_progress,
    )

    def has_permission(self):
        return (
            self.user.is_recruiter
            or self.user.is_recruiter_assessor
            or self.user.has_perm('permissions.can_use_api_for_newhire')
        )

    def _process_applications(self, resolution=None):
        applications = self.instance.applications.filter(active_applications_query)
        open_application_ids = list(applications.values_list('id', flat=True))

        if not open_application_ids:
            return
        if not resolution:
            raise WorkflowError('application_resolution_required')

        bulk_close_applications_by_id(
            application_ids=open_application_ids,
            resolution=resolution,
        )

    def _process_references(self, is_hired):
        if is_hired:
            return
        send_candidate_reference_event_task.delay(
            candidate_id=self.instance.id,
            event=REFERENCE_EVENTS.consideration_archived,
        )

    def _process_rotation(self, is_hired):
        rotation = self.wf.consideration.rotation
        if rotation:
            if is_hired:
                close_rotation_task.delay(rotation.pk)
            else:
                cancel_rotation_task.delay(rotation.pk)

    def _process_consideration(self, resolution, is_hired):
        if is_unfinished_interview_exists(consideration=self.wf.consideration):
            raise WorkflowError('candidate_has_unfinished_interviews')
        # При закрытии рассмотрения переносим необходимые поля из кандидата в рассмотрение
        archive_consideration(
            consideration=self.wf.consideration,
            resolution=resolution,
        )
        self._process_references(is_hired)
        self._process_rotation(is_hired)

    def _raise_if_active_offers(self):
        if self.instance.offers.alive().exists():
            raise WorkflowError('candidate_has_active_offers')

    def perform(self, is_hired=False, **params):
        if not is_hired:
            self._raise_if_active_offers()
            # TODO: Порефакторить. Отправку NPS можно вынести в отдельный модуль.
            #       Ожидается, что будет много разных NPS, и будут вызываться из разных мест.
            rejected_offers = list(
                self.instance.offers
                .filter(
                    application__consideration=self.wf.consideration,
                    status=OFFER_STATUSES.rejected,
                )
                .values('newhire_id')
            )
            was_offer_rejected = bool(rejected_offers)
            is_rotation = getattr(self.wf.consideration, 'is_rotation', False)
            # Кандидат может принять оффер, но на работу не выйти. Тогда его закрывают.
            # Исключаем такие случаи, чтобы не отправить им опрос о секциях.
            was_offer_accepted = any(o['newhire_id'] for o in rejected_offers)

            surveys_enabled = not waffle.switch_is_active(DISABLE_SURVEY_WAFFLE_SWITCH)
            is_survey_needed = (
                surveys_enabled
                and not is_rotation
                and not was_offer_accepted
                and self.wf.consideration
            )
            if is_survey_needed:
                send_interview_survey_task.delay(self.wf.consideration.id, was_offer_rejected)

        self._process_applications(params.get('application_resolution'))
        if self.wf.consideration:
            self._process_consideration(
                resolution=params['consideration_resolution'],
                is_hired=is_hired,
            )

        notification = CandidateClosedNotification(self.instance, self.user)
        notification.send()
        close_candidate(self.instance)
        return self.instance


class CloseForRecruiterAction(Action):

    valid_statuses = (
        CANDIDATE_STATUSES.in_progress,
    )

    def has_permission(self):
        return self.user.is_recruiter or self.user.is_recruiter_assessor

    def perform(self, **params):
        self._process_applications(params['application_resolution'])

        recruiters = set(self.instance.recruiters) - {self.user}
        responsibles_by_role = {
            'main_recruiter': params.get('main_recruiter'),
            'recruiters': recruiters,
        }
        update_candidate_responsibles(self.instance, responsibles_by_role)
        return self.instance

    def _process_applications(self, application_resolution):
        applications = self.instance.applications.filter(active_applications_query)
        applications = applications.filter(
            Q(
                vacancy__memberships__member=self.user,
                vacancy__memberships__role__in=(
                    VACANCY_ROLES.recruiter,
                    VACANCY_ROLES.main_recruiter,
                ),
            )
            | Q(created_by=self.user)
        ).distinct()
        open_application_ids = list(applications.values_list('id', flat=True))

        if not open_application_ids:
            return

        bulk_close_applications_by_id(
            application_ids=open_application_ids,
            resolution=application_resolution,
        )


class SendForApprovalAction(Action):

    def has_permission(self):
        return self.user.is_recruiter

    def is_status_correct(self):
        if self.wf.consideration is None:
            return False
        return (
            not is_unfinished_interview_exists(consideration=self.wf.consideration)
            and self._interviews_queryset.gradable().exists()
        )

    @cached_property
    def _interviews_queryset(self):
        if not self.wf.consideration:
            return Interview.unsafe.none()
        now = timezone.now()
        year_ago = now.replace(year=now.year - 1)
        return (
            Interview.unsafe
            .filter(
                state=Interview.STATES.finished,
                candidate_id=self.wf.consideration.candidate_id,
                finished__gte=year_ago,
            )
            .select_related('consideration')
            .prefetch_related('assignments__problem')
        )

    @cached_property
    def _challenges_queryset(self):
        if not self.wf.consideration:
            return Challenge.objects.none()
        now = timezone.now()
        year_ago = now.replace(year=now.year - 1)
        return (
            Challenge.objects
            .filter(
                type=CHALLENGE_TYPES.contest,
                status=CHALLENGE_STATUSES.finished,
                candidate_id=self.wf.consideration.candidate_id,
                finished__gte=year_ago,
            )
            .select_related('consideration')
        )

    def _close_irrelevant_applications(self, application):
        """
        Закрывает все прет-ва по оставшимся вакансиям с финалами
        """
        if not is_candidate_approval_with_application_enabled():
            return

        # Финальные секции по всем вакансиям, кроме выбранной кандидатом
        irrelevant_final_interviews = (
            Interview.unsafe
            .filter(
                type=INTERVIEW_TYPES.final,
                state=Interview.STATES.finished,
                consideration=self.wf.consideration,
            )
            .exclude(application=application)
            .exclude(application__status=APPLICATION_STATUSES.closed)
        )

        resolutions_map = {
            INTERVIEW_RESOLUTIONS.hire: APPLICATION_RESOLUTIONS.offer_rejected,
            INTERVIEW_RESOLUTIONS.nohire: APPLICATION_RESOLUTIONS.did_not_pass_assessments,
        }

        for interview_resolution, application_resolution in resolutions_map.items():
            application_ids = list(
                irrelevant_final_interviews
                .filter(resolution=interview_resolution)
                .values_list('application_id', flat=True)
            )
            bulk_close_applications_by_id(
                application_ids=application_ids,
                resolution=application_resolution,
                notify=True,
                reason=APPLICATION_CLOSE_REASONS.offer_rejected,
                initiator=self.user,
            )

    def perform(self, **params):
        interviews_query = Q(type__in=INTERVIEW_GRADABLE_TYPES._db_values)
        application = None
        self.instance.startrek_key = params.get('startrek_key')

        if is_candidate_approval_with_application_enabled():
            application = params['application']
            self.instance.startrek_key = application.vacancy.startrek_key
            interviews_query |= Q(
                type=INTERVIEW_TYPES.final,
                application=application,
            )

        self.instance.save()

        try:
            notify_for_candidate_approval(
                candidate=self.instance,
                interviews=self._interviews_queryset.filter(interviews_query),
                challenges=self._challenges_queryset,
                initiator=self.user,
            )
        except IssueDoesNotExist as exc:
            raise WorkflowError(exc.message)

        self._close_irrelevant_applications(application)

        return self.instance


class CreateNoteAction(Action):

    def has_permission(self):
        return self.user.is_recruiter or self.user.is_recruiter_assessor

    def perform(self, **params):
        params['candidate'] = self.instance
        return update_or_create_note(data=params, initiator=self.user)


class OutcomingMessageCreateAction(Action):

    def has_permission(self):
        return self.user.is_recruiter or self.user.is_recruiter_assessor

    def perform(self, **params):
        return update_or_create_external_message(data=params, initiator=self.user)


class CloseIrrelevantApplicationsAction(Action):

    valid_statuses = (
        CANDIDATE_STATUSES.in_progress,
    )

    def has_permission(self):
        return (
            self.user.is_recruiter
            and self.has_interviews
            and self.irrelevant_applications.exists()
        )

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        considering_interview_types = (
            INTERVIEW_TYPES.screening,
            INTERVIEW_TYPES.aa,
            INTERVIEW_TYPES.regular,
            INTERVIEW_TYPES.final,
        )

        relevant_application_ids = list(
            Interview.unsafe
            .exclude(state=INTERVIEW_STATES.cancelled)
            .filter(
                consideration=self.wf.consideration,
                type__in=considering_interview_types,
            )
            .values_list('application_id', flat=True)
        )
        self.has_interviews = bool(relevant_application_ids)

        relevant_application_ids = filter(None, relevant_application_ids)
        self.irrelevant_applications = (
            Application.unsafe
            .filter(
                active_applications_query,
                consideration=self.wf.consideration,
            )
            .exclude(id__in=relevant_application_ids)
        )

    def perform(self, **params):
        applications = self.irrelevant_applications
        application_ids = [i.id for i in applications]
        applications.update(
            status=APPLICATION_STATUSES.closed,
            resolution=APPLICATION_RESOLUTIONS.team_was_not_selected,
            modified=timezone.now(),
        )
        post_update.send(
            sender=Application,
            queryset=Application.unsafe.filter(id__in=application_ids),
        )
        blank_modify_candidate(self.instance)
        return self.instance


class CreateVerificationAction(Action):

    valid_statuses = (
        CANDIDATE_STATUSES.in_progress,
    )

    def _has_actual_verifications(self):
        return self.instance.verifications.alive().exists()

    def _has_active_applications(self):
        return (
            Application.unsafe  # полагаемся на проверку прав на кандидата
            .filter(
                active_applications_query,
                consideration=self.wf.consideration,
            )
            .exists()
        )

    def has_permission(self):
        return (
            self.user.is_recruiter
            and not self._has_actual_verifications()
            and self._has_active_applications()
        )

    def perform(self, sender=None, **params):
        subject = params.pop('subject')
        text = params.pop('text')
        receiver = params.pop('receiver')

        verification = Verification.objects.create(
            candidate=self.instance,
            created_by=self.user,
            **params
        )
        send_verification_form_to_candidate(
            verification_id=verification.id,
            subject=subject,
            text=text,
            receiver=receiver,
            sender=sender or self.user.email,
        )
        blank_modify_candidate(self.instance)
        return verification


class InterviewRoundCreateAction(Action):

    valid_statuses = (
        CANDIDATE_STATUSES.in_progress,
    )

    def has_permission(self):
        return (
            (
                self.user.is_recruiter
                or self.user.is_recruiter_assessor
            )
            and not self.wf.has_active_offers
        )

    def is_visible(self):
        return self.is_available() and waffle.switch_is_active('show_interview_bulk_create_button')

    def is_available(self):
        return super().is_visible() and waffle.switch_is_active('enable_interview_bulk_create')

    def perform(self, **params):
        time_slots = params.pop('time_slots')
        interviews = params.pop('interviews')
        message = params.pop('message')
        if params.pop('need_notify_candidate'):
            params['message'] = update_or_create_external_message(message, initiator=self.user)

        params['consideration'] = get_or_create_relevant_consideration(
            candidate=self.instance,
            initiator=self.user,
            candidate_data={
                'responsibles': [self.user],
            },
        )

        interview_round = InterviewRound.objects.create(
            candidate=self.instance,
            created_by=self.user,
            **params
        )

        time_slots = InterviewRoundTimeSlot.objects.bulk_create(
            InterviewRoundTimeSlot(round=interview_round, **time_slot)
            for time_slot in time_slots
        )
        post_bulk_create.send(sender=InterviewRoundTimeSlot, queryset=time_slots)

        create_interviews_for_round(interviews, interview_round)
        save_interview_round_data_in_yt.delay(interview_round.id)
        return interview_round


class CertificationCreateAction(Action):

    def has_permission(self):
        return (
            self.user.is_recruiter
            and get_considerations_for_certification(self.instance).exists()
        )

    def perform(self, **params):
        certification = Certification.objects.create(
            consideration=params['consideration'],
            created_by=self.user,
        )
        send_certification_to_candidate(
            certification=certification,
            receiver=params['receiver'],
        )
        return certification


class CandidateWorkflow(Workflow):

    ACTION_MAP = {
        'create': CreateAction,
        'update': UpdateAction,
        'open': OpenAction,
        'create_proposals': CreateProposalsAction,
        'send_for_approval': SendForApprovalAction,
        'close': CloseAction,
        'close_for_recruiter': CloseForRecruiterAction,
        'close_irrelevant_applications': CloseIrrelevantApplicationsAction,
        'create_verification': CreateVerificationAction,
        'note_create': CreateNoteAction,
        'outcoming_message_create': OutcomingMessageCreateAction,
        'interview_round_create': InterviewRoundCreateAction,
        'certification_create': CertificationCreateAction,
    }

    @cached_property
    def consideration(self):
        if self.instance is None:
            return
        if 'consideration' in self.kwargs:
            return self.kwargs['consideration']
        return get_relevant_consideration(self.instance.id)

    def get_actions_visibility(self):
        actions = super().get_actions_visibility()
        actions['interview_create'] = (
            (self.user.is_recruiter or self.user.is_recruiter_assessor)
            and self.instance.status == CANDIDATE_STATUSES.in_progress
            and not self.has_active_offers
        )

        # FIXME: пока не придумал, как лучше разрулить циклический импорт
        from intranet.femida.src.applications.workflow import ApplicationWorkflow
        application_wf = ApplicationWorkflow(
            instance=None,
            user=self.user,
            candidate=self.instance,
        )
        application_bulk_create_action = application_wf.get_action('bulk_create')
        actions['application_create'] = application_bulk_create_action.is_visible()
        return actions

    @cached_property
    def has_active_offers(self):
        return self.instance.offers.filter(status__in=(
            OFFER_STATUSES.sent,
            OFFER_STATUSES.accepted,
        )).exists()


#####################
# DUPLICATION CASES #
#####################

class CancelAction(Action):

    valid_statuses = (
        DUPLICATION_CASE_STATUSES.new,
    )

    def perform(self, **params):
        self.instance.status = DUPLICATION_CASE_STATUSES.closed
        self.instance.resolution = DUPLICATION_CASE_RESOLUTIONS.not_duplicate
        self.instance.managed_by = self.user
        self.instance.save()
        return self.instance


class MarkUnclearAction(Action):

    valid_statuses = (
        DUPLICATION_CASE_STATUSES.new,
    )

    def perform(self, **params):
        self.instance.resolution = DUPLICATION_CASE_RESOLUTIONS.unclear
        self.instance.managed_by = self.user
        self.instance.save()
        return self.instance


class DuplicationCaseWorkflow(Workflow):

    ACTION_MAP = {
        'cancel': CancelAction,
        'mark_unclear': MarkUnclearAction,
    }
