import logging
from base64 import b64decode
from typing import List

from django.conf import settings
from django.core.files.base import ContentFile
from django.utils.html import linebreaks

from intranet.femida.src.candidates.contacts import get_or_create_contact
from intranet.femida.src.candidates.choices import CONTACT_SOURCES, CONTACT_TYPES
from intranet.femida.src.candidates.helpers import blank_modify_candidate
from intranet.femida.src.candidates.models import CandidateContact, Candidate
from intranet.femida.src.core.signals import post_bulk_create
from intranet.femida.src.notifications.communications import (
    InternalMessageCreatedNotification,
    ExternalMessageCreatedNotification,
    NoteCreatedNotification,
)
from intranet.femida.src.notifications.utils import get_communication_message_id
from intranet.femida.src.attachments.models import Attachment
from intranet.femida.src.applications.models import Application
from intranet.femida.src.core.controllers import update_instance, update_list_of_instances
from intranet.femida.src.tasks import send_email
from intranet.femida.src.wf.models import wiki_format
from intranet.femida.src.communications.choices import MESSAGE_TYPES, MESSAGE_STATUSES
from intranet.femida.src.communications.models import Message, MessageAttachment
from intranet.femida.src.communications.tasks import set_message_status_and_notify_task


logger = logging.getLogger(__name__)


def _update_message_attachments(instance, attachments):
    data = [{
        'attachment': a,
        'message': instance,
        'content_type': '',
    } for a in attachments]
    update_list_of_instances(
        model=MessageAttachment,
        queryset=instance.message_attachments.all(),
        data=data,
    )


def update_or_create_external_message(data, initiator=None, instance=None):
    """
    Создает или обновляет существующее сообщение instance (только перечисленные поля).
    Обновление возможно, если сообщение является отложенныи и еще не взято на отправку.
    """
    attachment_ids = [a.id for a in data.get('attachments', [])]

    instance, is_creation = _process_instance_and_attachments_external(data, initiator, instance)

    # Создание/получение сообщения считаем совершением действия над кандидатом
    if is_creation:
        blank_modify_candidate(instance.candidate)

    if instance.type == MESSAGE_TYPES.outcoming and instance.status == MESSAGE_STATUSES.sending:
        send_email_to_candidate(instance, attachment_ids)
    if instance.type == MESSAGE_TYPES.incoming:
        notification = ExternalMessageCreatedNotification(instance, initiator)
        notification.send()

    return instance


def delete_external_message(instance):
    instance.status = MESSAGE_STATUSES.deleted
    instance.save(update_fields=['status'])


def _process_instance_and_attachments_external(data, initiator, instance):
    attachments = data.pop('attachments', None)
    is_creation = instance is None

    if data['type'] == MESSAGE_TYPES.outcoming:
        data.setdefault('status', (
            MESSAGE_STATUSES.scheduled if data.get('schedule_time') else MESSAGE_STATUSES.sending
        ))

    if not is_creation:
        data['is_changed'] = True
        update_instance(instance, data)
    else:
        data['author'] = initiator
        instance = Message(**data)

        if instance.type == MESSAGE_TYPES.outcoming:
            instance.separator_message_id = get_communication_message_id(instance.candidate)
        # Handle replies from already merged candidates
        if instance.candidate.is_duplicate:
            instance.candidate = instance.candidate.original

        instance.save(force_insert=True)

    # Обновляем аттачи, если они были переданы.
    # В том числе затираем существующие, отсутствующие в переданном qs.
    if attachments is not None:
        _update_message_attachments(instance, attachments)

    return instance, is_creation


def _process_instance_and_attachments(data, initiator, instance):
    attachments = data.pop('attachments', None)
    is_creation = instance is None
    if not is_creation:
        data['is_changed'] = True
        data.pop('author', None)
        update_instance(instance, data)
    else:
        if 'author' not in data:
            data['author'] = initiator
        instance = Message(**data)
        instance.save(force_insert=True)

    # Обновляем аттачи, если они были переданы.
    # В том числе затираем существующие, отсутствующие в переданном qs.
    if attachments is not None:
        _update_message_attachments(instance, attachments)

    return instance, is_creation


# TODO: переместить в applications в рамках FEMIDA-3828
def update_or_create_internal_message(data, initiator=None, instance=None,
                                      need_notify=True, need_modify_candidate=True):
    instance, is_creation = _process_instance_and_attachments(data, initiator, instance)

    if is_creation:
        # У message.application нет constraint, поэтому теоретически может
        # быть DoesNotExist.
        try:
            application = instance.application
        except Application.DoesNotExist:
            application = None

        if application:
            application.save(update_fields=['modified'])
            if need_modify_candidate:
                blank_modify_candidate(application.candidate)
            if need_notify:
                notification = InternalMessageCreatedNotification(instance, initiator=initiator)
                notification.send()

    return instance


def update_or_create_note(data, initiator=None, instance=None,
                          need_notify=True, need_modify_candidate=True):
    data['type'] = MESSAGE_TYPES.note
    instance, is_creation = _process_instance_and_attachments(data, initiator, instance)

    if is_creation and need_notify:
        notification = NoteCreatedNotification(instance, initiator)
        notification.send()

    if need_modify_candidate:
        blank_modify_candidate(instance.candidate)

    return instance


def bulk_create_internal_messages(applications, text, html=None, initiator=None):
    """
    Массовое создание внутренней коммуникации.
    Делаем один запрос в базу и один раз ходим в wiki-formatter (или вообще не ходим)
    """
    if html is None:
        html = wiki_format(text) or text

    messages = []
    for application in applications:
        messages.append(Message(
            application=application,
            candidate=application.candidate,
            text=text,
            html=html,
            subject='',
            author=initiator,
            type=MESSAGE_TYPES.internal,
        ))
    messages = Message.objects.bulk_create(messages)
    post_bulk_create.send(sender=Message, queryset=messages)
    return messages


def send_email_to_candidate(message: Message, attachment_ids: List[int]):
    tasks_chain = (
        send_email.si(
            subject=message.subject,
            body=linebreaks(message.text),
            to=[message.email],
            from_email=settings.JOB_EMAIL_VERBOSE,
            message_id=message.separator_message_id,
            attachment_ids=attachment_ids,
            is_external=True,
        )
        | set_message_status_and_notify_task.s(message.id)
    )
    tasks_chain.delay()


class MessageAlreadyProcessedError(Exception):
    pass


def _identify_candidate_by_email(data):
    if 'from' not in data:
        return

    candidate_ids = (
        CandidateContact.objects
        .filter(
            type=CONTACT_TYPES.email,
            account_id=data['from'],
            is_active=True,
            candidate__is_duplicate=False,
        )
        .values_list('candidate_id', flat=True)
        .distinct()
    )
    if len(candidate_ids) == 0:
        logger.error('Candidate with email "%s" was not found', data['from'])
    elif len(candidate_ids) > 1:
        logger.error('Multiple candidates with email "%s" were found', data['from'])
    else:
        return 'cand%s' % candidate_ids[0]


def identify_candidate_from_separator(data):
    """
    {
      "type":      "IDENTIFY",
      "messageId": "уникальный_идентификатор",
      "appl_id":   "идентификатор_претендентства",
      "from":      "адрес_отправителя"
    }

    Если appl_id был передан и он есть у нас в базе, то его и передаем
    Иначе пробуем найти appl_id по email
    Условия: такой email должен быть только у одного кандидата
    и у него должно быть только одно активное претендентство
    """
    if 'appl_id' in data and data['appl_id'] is not None:
        if data['appl_id'].startswith('cand'):
            candidate_id = data['appl_id'][4:]
            if Candidate.unsafe.filter(id=candidate_id).exists():
                return data['appl_id']
        else:
            application = Application.unsafe.filter(pk=data['appl_id']).first()
            if application:
                return application.id
    return _identify_candidate_by_email(data)


def create_message_from_separator(data):
    """
    {
      "type":      "CREATE",
      "messageId": "уникальный_идентификатор",
      "applId":    "идентификатор_претендентства",
      "topic":     "код_темы(MAIN, OFFICIAL)",
      "subject":   "тема сообщения",
      "text":      "сообщение_plain_text_base64_utf-8",
      "html":      "сообщение_в_html_base64_utf-8",
      "from":      "от кого сообщение",
      "attachments": [
        {
          "name":        "название_аттача",
          "contentType": "content-type",
          "content":     "содержимое_в_base64"
        }, ... ]
    }
    """
    # if Message.objects.filter(separator_message_id=data['messageId']).count():
    #     raise MessageAlreadyProcessedError

    if data['applId'].startswith('cand'):
        candidate_id = data['applId'][4:]
        candidate = Candidate.unsafe.get(id=candidate_id)
    else:
        application = Application.unsafe.get(pk=data['applId'])
        candidate = application.candidate
    message = update_or_create_external_message(
        data={
            'candidate': candidate,
            'type': MESSAGE_TYPES.incoming,
            'subject': data['subject'],
            'text': b64decode(data.get('rawBody', '')).decode('utf-8'),
            'cleaned_text': b64decode(data['text']).decode('utf-8'),
            'html': b64decode(data['html']).decode('utf-8'),
            'separator_message_id': data['messageId'],
            'email': data.get('from', ''),
        },
    )
    for attachment_data in data.get('attachments', []):
        file = ContentFile(
            content=b64decode((attachment_data['content'])),
            name='file',
        )
        attachment = Attachment.objects.create(
            name=attachment_data['name'],
            content_type=attachment_data['contentType'],
            attached_file=file,
        )
        MessageAttachment.objects.create(
            attachment=attachment,
            message=message,
        )
    if data.get('from'):
        get_or_create_contact(
            candidate_id=candidate.id,
            type=CONTACT_TYPES.email,
            account_id=data['from'],
            source=CONTACT_SOURCES.separator,
        )
    return message
