import logging
from django.utils import timezone

from intranet.femida.src.applications.helpers import active_applications_query
from intranet.femida.src.candidates import choices
from intranet.femida.src.candidates.choices import (
    SUBMISSION_SOURCES,
    EXTERNAL_SUBMISSION_SOURCES,
    CONSIDERATION_RESOLUTIONS,
    CANDIDATE_STATUSES,
)
from intranet.femida.src.candidates.contacts import normalize_contact
from intranet.femida.src.candidates.deduplication import RotationDuplicatesFinder, DuplicatesFinder
from intranet.femida.src.candidates.deduplication.adapters import to_cand_adapter
from intranet.femida.src.candidates.helpers import (
    add_profession_from_vacancy,
    add_skills_from_vacancy,
)
from intranet.femida.src.candidates.considerations.controllers import (
    get_or_create_relevant_consideration,
)
from intranet.femida.src.candidates.helpers import close_candidate
from intranet.femida.src.candidates.considerations.helpers import archive_consideration
from intranet.femida.src.candidates.tasks import start_reference_issue_task, start_rotation_task
from intranet.femida.src.candidates.workflow import CandidateWorkflow
from intranet.femida.src.candidates.submissions.helpers import (
    prepare_candidate_data,
    prepare_message_data,
    extract_contacts,
    get_submission_vacancies_qs,
    get_candidate_source_by_submission,
)
from intranet.femida.src.candidates.models import CandidateAttachment, CandidateContact, Candidate
from intranet.femida.src.communications.choices import MESSAGE_TYPES
from intranet.femida.src.communications.controllers import (
    update_or_create_internal_message,
    update_or_create_external_message,
)
from intranet.femida.src.core.controllers import update_list_of_instances, update_instance
from intranet.femida.src.core.signals import post_bulk_create
from intranet.femida.src.interviews.choices import (
    APPLICATION_SOURCES,
    APPLICATION_STATUSES,
    APPLICATION_RESOLUTIONS,
)
from intranet.femida.src.interviews.models import Application
from intranet.femida.src.vacancies.choices import (
    ACTIVE_VACANCY_STATUSES,
    ACTIVE_VACANCY_STATUSES_FOR_SUBMISSION,
)
from intranet.femida.src.users.models import get_robot_femida
from intranet.femida.src.utils.switches import is_candidate_main_recruiter_enabled


logger = logging.getLogger(__name__)


class SubmissionController:

    def __init__(self, submission, initiator=None):
        self.submission = submission
        self.consideration = None
        self.initiator = initiator or get_robot_femida()

        valid_vacancy_statuses = ACTIVE_VACANCY_STATUSES_FOR_SUBMISSION
        if submission.is_internship:
            valid_vacancy_statuses = ACTIVE_VACANCY_STATUSES

        self.vacancies = (
            get_submission_vacancies_qs(self.submission)
            .filter(status__in=valid_vacancy_statuses._db_values)
            .select_related(
                'profession',
                'professional_sphere',
            )
            .prefetch_related(
                'memberships__member',
                'skills',
                'cities',
            )
        )

    @property
    def candidate(self):
        return self.submission.candidate

    @property
    def vacancies_recruiters(self):
        recruiters = set()
        for vacancy in self.vacancies:
            recruiters.add(vacancy.main_recruiter)
        return recruiters

    @property
    def responsibles(self):
        return (
            self.vacancies_recruiters
            if self.submission.is_internship
            else self.vacancies_recruiters | {self.initiator}
        )

    def create_candidate(self, is_rejection=False):
        candidate_data = prepare_candidate_data(self.submission)
        candidate_data['responsibles'] = self.responsibles

        if is_candidate_main_recruiter_enabled() and self.submission.is_internship:
            candidate_data['main_recruiter'] = next(iter(self.responsibles))

        # Непосредственно создаем кандидата
        cand_wf = CandidateWorkflow(instance=None, user=self.initiator)
        action = cand_wf.get_action('create')
        candidate = action.perform(need_notify=False, **candidate_data)
        self.consideration = action.extra_data['consideration']

        self._close_submission(candidate, is_rejection)
        # Если это быстрый отказ, то сразу закрываем кандидата и архивируем рассмотрение
        if is_rejection:
            self._close_candidate(candidate)
        self._create_challenge()
        self._create_applications(is_rejection)
        self._update_reference_issue()
        self._start_rotation()
        return self.candidate

    def merge_into_candidate(self, candidate, is_rejection=False):
        is_closed = candidate.status == CANDIDATE_STATUSES.closed

        candidate_data = {
            'source': get_candidate_source_by_submission(self.submission),
            'responsibles': None if is_rejection and not is_closed else self.responsibles,
        }

        if is_candidate_main_recruiter_enabled() and self.submission.is_internship:
            candidate_data['main_recruiter'] = next(iter(self.responsibles))

        self.consideration = get_or_create_relevant_consideration(
            candidate=candidate,
            initiator=self.initiator,
            candidate_data=candidate_data,
        )
        self._close_submission(candidate, is_rejection)
        # Если это быстрый отказ, и кандидат уже был закрыт, то создаём новое рассмотрение
        # и сразу же закрываем кандидата и архивируем рассмотрение.
        if is_rejection and is_closed:
            self._close_candidate(candidate)
        self._create_missing_attachment()
        self._create_missing_contacts()
        self._create_challenge()
        self._create_applications(is_rejection)
        self._update_reference_issue()
        self._start_rotation()
        return self.candidate

    def reject(self):
        self._close_submission()

    def _close_submission(self, candidate=None, is_rejection=False):
        submission_data = {
            'candidate': candidate,
            'responsible': self.initiator,
            'status': choices.SUBMISSION_STATUSES.closed,
            'closed_at': timezone.now(),
            'is_fast_rejection': is_rejection,
        }
        for field_name, value in submission_data.items():
            setattr(self.submission, field_name, value)
        self.submission.save(update_fields=list(submission_data) + ['modified'])
        self.submission.vacancies.set(self.vacancies)

    def _close_candidate(self, candidate):
        archive_consideration(
            consideration=self.consideration,
            resolution=CONSIDERATION_RESOLUTIONS.rejected_by_resume,
        )
        close_candidate(candidate)

    def _update_reference_issue(self):
        """
        Отправляет в REFERENCE-тикет события по отклику
        """
        reference = self.submission.reference
        if not reference:
            return
        if not reference.is_approved:
            logger.warning(
                'Posting to issue skipped. Reference %d has to be approved (actual status: %s)',
                reference.id,
                reference.status,
            )
            return
        # Note: ожидаем, что референс не мог протухнуть до обработки отклика
        start_reference_issue_task.delay(self.submission.id)

    def _start_rotation(self):
        rotation = self.submission.rotation
        if not rotation:
            return
        consideration_data = {
            'rotation': rotation,
            'is_rotation': True,
        }
        update_instance(self.consideration, consideration_data)
        start_rotation_task.delay(rotation.id)

    def create_initial_communication(self, application_id):
        message_data = prepare_message_data(self.candidate, self.submission)
        if message_data['text']:
            message_data['application_id'] = application_id
            if self.submission.source == choices.SUBMISSION_SOURCES.reference:
                message_data['author'] = self.submission.reference.created_by
            elif self.submission.source == choices.SUBMISSION_SOURCES.rotation:
                message_data['author'] = self.submission.rotation.created_by
            else:
                message_data['author'] = None
            return update_or_create_internal_message(
                data=message_data,
                initiator=self.initiator,
                need_notify=not self.submission.is_internship,
                need_modify_candidate=not self.submission.is_internship,
            )

    def create_rejection_message(self, text, schedule_time=None):
        message_params = {
            'candidate': self.candidate,
            'email': self.submission.email,
            'subject': 'От компании Яндекс',
            'type': MESSAGE_TYPES.outcoming,
            'text': text,
            'schedule_time': schedule_time,
        }
        update_or_create_external_message(data=message_params, initiator=self.initiator)

    def _create_challenge(self):
        if not self.submission.parsed_questions:
            is_parsing_error = (
                self.submission.source in EXTERNAL_SUBMISSION_SOURCES
                and self.submission.raw_questions  # содержит ли форма технические вопросы
            )
            if is_parsing_error:
                logger.error('Failed to parse questions for submission %d', self.submission.id)
            return

        self.submission.challenges.create(
            candidate=self.candidate,
            consideration=self.consideration,
            answers=self.submission.parsed_questions,
            type=choices.CHALLENGE_TYPES.quiz,
            status=choices.CHALLENGE_STATUSES.pending_review,
            created_by=self.initiator,
        )

    def _create_applications(self, is_rejection=False):
        vacancy_to_application_map = dict(
            self.candidate.applications
            .filter(active_applications_query)
            .values_list('vacancy_id', 'id')
        )

        applications_to_create = []
        application_ids = []
        for vacancy in self.vacancies:
            if vacancy.id not in vacancy_to_application_map:
                application_data = {
                    'candidate': self.candidate,
                    'consideration': self.consideration,
                    'vacancy': vacancy,
                    'submission': self.submission,
                    'created_by': self.initiator,
                    'source': APPLICATION_SOURCES.submission,
                }
                if self.submission.is_internship:
                    application_data['status'] = APPLICATION_STATUSES.in_progress
                    application_data['resolution'] = APPLICATION_RESOLUTIONS.test_task_sent
                if is_rejection:
                    application_data['status'] = APPLICATION_STATUSES.closed
                    application_data['resolution'] = APPLICATION_RESOLUTIONS.rejected_by_resume
                application = Application(**application_data)
                applications_to_create.append(application)
                add_profession_from_vacancy(self.candidate, vacancy)
                add_skills_from_vacancy(self.candidate, vacancy)
            else:
                application_ids.append(vacancy_to_application_map[vacancy.id])

        created_applications = Application.objects.bulk_create(applications_to_create)
        application_ids.extend(a.id for a in created_applications)
        post_bulk_create.send(
            sender=Application,
            queryset=created_applications,
        )

        for application_id in application_ids:
            self.create_initial_communication(application_id)

    def _create_missing_attachment(self):
        attachment = self.submission.attachment
        if not attachment:
            return
        if CandidateAttachment.unsafe.filter(attachment__sha1=attachment.sha1).exists():
            return

        CandidateAttachment.objects.create(
            candidate=self.candidate,
            attachment=attachment,
            type=choices.ATTACHMENT_TYPES.resume,
        )

    def _create_missing_contacts(self):
        candidate_adapter = to_cand_adapter(self.candidate)
        submission_contacts = extract_contacts(self.submission)
        existing_contacts = self.candidate.contacts.all()
        existing_main_contact_types = {c.type for c in existing_contacts if c.is_main}

        data = []
        for contact in submission_contacts:
            _type, account_id = contact['type'], contact['account_id']
            normalized_account_id = normalize_contact(_type, account_id) or account_id
            raw_contact = (_type, normalized_account_id)

            if raw_contact not in candidate_adapter.contacts:
                data.append({
                    'type': _type,
                    'account_id': account_id,
                    'normalized_account_id': normalized_account_id,
                    'is_main': _type not in existing_main_contact_types,
                    'source': choices.CONTACT_SOURCES.forms,
                    'candidate': self.candidate,
                })

        update_list_of_instances(
            model=CandidateContact,
            queryset=existing_contacts,
            data=data,
            delete_missing=False,
        )


def get_submission_duplicate_triples(submission):
    if submission.source == SUBMISSION_SOURCES.rotation:
        finder_class = RotationDuplicatesFinder
    else:
        finder_class = DuplicatesFinder

    finder = finder_class(base_qs=Candidate.objects.all())
    return finder.find_top3(submission)
