from operator import attrgetter
from typing import Dict, Iterable

import pytz
from django.core.exceptions import ValidationError
from django.db.models import prefetch_related_objects
from django.template import loader
from django.utils import timezone

from intranet.femida.src.api.candidates.utils import get_skype_accounts
from intranet.femida.src.calendar.helpers import CALENDAR_DT_FORMAT
from intranet.femida.src.calendar.tasks import update_event_task
from intranet.femida.src.candidates.considerations.controllers import (
    update_consideration_extended_status,
)
from intranet.femida.src.communications.choices import MESSAGE_STATUSES
from intranet.femida.src.communications.controllers import send_email_to_candidate
from intranet.femida.src.core.signals import post_update
from intranet.femida.src.interviews.choices import (
    INTERVIEW_ROUND_PLANNERS,
    INTERVIEW_ROUND_STATUSES,
    INTERVIEW_STATES,
    INTERVIEW_TYPES,
)
from intranet.femida.src.interviews.controllers import create_assignments_from_presets
from intranet.femida.src.interviews.models import Interview, InterviewRound
from intranet.femida.src.interviews.yang.forms import YangInterviewRoundForm
from intranet.femida.src.notifications.headers import (
    prefetch_candidate_data_for_headers,
    prefetch_vacancy_data_for_headers,
)
from intranet.femida.src.notifications.interviews import (
    get_headers_prefetched_for_interview,
    notify_about_interview_create,
    InterviewRoundPlanningFinishedNotification,
)
from intranet.femida.src.notifications.utils import get_base_context


def interview_round_finish_planning(interview_round: InterviewRound, raw_data: Dict):
    assert interview_round.status == INTERVIEW_ROUND_STATUSES.planning
    assert interview_round.planner == INTERVIEW_ROUND_PLANNERS.yang

    interviews_by_id = interview_round.interviews.in_bulk()
    validator = YangInterviewRoundForm(
        data=raw_data,
        base_initial={
            'interviews_by_id': interviews_by_id,
        },
    )
    if not validator.is_valid():
        raise ValidationError(validator._errors)
    data = validator.cleaned_data

    interview_round.status = data['status']
    interview_round.save(update_fields=('status', 'modified'))

    events = {i['id']: i.pop('event', None) for i in data['interviews']}
    interviews = _update_interviews(interviews_by_id, data['interviews'])
    assigned_interviews = [i for i in interviews if i.state == INTERVIEW_STATES.assigned]
    create_assignments_from_presets(assigned_interviews)
    if assigned_interviews:
        update_consideration_extended_status(interview_round.consideration)
    _update_calendar_events(interviews, events)
    _send_notifications(interview_round, assigned_interviews, events)


def _update_calendar_events(interviews: Iterable[Interview], events: dict):
    for interview in interviews:
        event = events.get(interview.id)
        if not event:
            continue

        template_name = 'femida/interviews/interview-round-event-description.txt'
        context = get_base_context()
        context['event'] = event
        context['interview'] = interview

        if interview.type == INTERVIEW_TYPES.screening:
            context['skypes'] = get_skype_accounts(interview.candidate)

        data = {
            'name': f'{interview.section} ({interview.candidate.get_full_name()})',
            'description': loader.render_to_string(template_name, context),
            # Обязательные поля api Календаря
            'startTs': event.start_time.strftime(CALENDAR_DT_FORMAT),
            'endTs': event.end_time.strftime(CALENDAR_DT_FORMAT),
        }
        # TODO: если пользователь перенес встречу до запуска отложенной таски,
        #  мы ее вернем на старое время.
        #  Это можно полечить, запоминая sequence эвента при получении, передавая его url на post
        #  и обрабатывая эксепшен EventAlreadyModified
        update_event_task.delay(event.id, data)


def _update_interviews(interviews_by_id, data):
    update_fields = {'modified'}
    for item in data:
        interview_id = item.pop('id')
        interview = interviews_by_id[interview_id]
        for field, value in item.items():
            update_fields.add(field)
            setattr(interview, field, value)
    interviews = sorted(interviews_by_id.values(), key=attrgetter('id'))
    Interview.unsafe.bulk_update(interviews, update_fields)
    post_update.send(sender=Interview, queryset=interviews)
    return interviews


def _send_notifications(interview_round, interviews, events):
    _prefetch_data_for_notifications(interview_round.candidate, interviews)
    notification = InterviewRoundPlanningFinishedNotification(
        instance=interview_round,
        interviews=interviews,
    )
    notification.send()
    for interview in interviews:
        event = events.get(interview.id)
        headers = get_headers_prefetched_for_interview(interview)
        notify_about_interview_create(interview, interview_round.created_by, event, headers)
    if interview_round.status == INTERVIEW_ROUND_STATUSES.planned and interview_round.message:
        _notify_candidate(interview_round.message, interviews)


def _prefetch_data_for_notifications(candidate, interviews):
    prefetch_related_objects(interviews, 'application__vacancy')
    vacancies = [i.application.vacancy for i in interviews if i.application]
    prefetch_related_objects([candidate], 'responsibles')
    prefetch_related_objects(
        vacancies,
        'memberships__member',
        'profession',
    )
    prefetch_candidate_data_for_headers([candidate])
    prefetch_vacancy_data_for_headers(vacancies)
    # Подменяем ссылки на кандидата – используем объект, для которого мы всё запрефетчили
    for interview in interviews:
        assert interview.candidate_id == candidate.id
        interview.candidate = candidate


def _notify_candidate(message, interviews):
    interview_datetime = min(i.event_start_time for i in interviews if i.event_start_time)
    interview_datetime = interview_datetime.astimezone(pytz.timezone('Europe/Moscow'))
    message.text = message.text.format(
        interview_datetime=interview_datetime.strftime('%d.%m.%Y %H:%M'),
    )
    message.status = MESSAGE_STATUSES.sending
    # фронт рисует created или schedule_time в качестве даты отправки письма
    # в данном случае message создаётся за долго до отправки письма
    # поэтому здесь хак: более подходящая дата проставляется в schedule_time
    message.schedule_time = timezone.now()
    message.save(update_fields=['text', 'status', 'modified', 'schedule_time'])
    attachment_ids = list(message.attachments.values_list('id', flat=True))
    send_email_to_candidate(message, attachment_ids)
