import json
import logging
import traceback
from datetime import timedelta
from typing import List, Optional

from simple_history.utils import bulk_update_with_history

from django.conf import settings
from django.db import transaction
from django.utils import timezone

from lms.contrib.sender.client import sender_client
from lms.courses.models import Course, CourseStudent
from lms.enrollments.models import EnrolledUser
from lms.staff.models import StaffProfile

from ..utils import chunks
from .models import CourseFollower, CourseMailing, Mailing, MailingEvent
from .settings import MAILING_RECIPIENTS_CHUNK_SIZE, MAILING_TASKS_CAMPAIGN_SLUGS_MAP

log = logging.getLogger(__name__)


def sync_course_mailings(course: Course):
    existed_mailings_ids_in_course = set(course.mailings.all().values_list('mailing_id', flat=True))
    if course.enable_followers:
        course_mailings_to_add = [
            CourseMailing(course=course, mailing_id=mailing_id, is_active=True)
            for mailing_id
            in Mailing.objects.active().values_list('id', flat=True)
            if mailing_id not in existed_mailings_ids_in_course
        ]
        if course_mailings_to_add:
            CourseMailing.objects.bulk_create(
                objs=course_mailings_to_add,
                batch_size=settings.BULK_BATCH_SIZE_DEFAULT,
            )

    else:
        if existed_mailings_ids_in_course:
            course.mailings.all().delete()


def process_mailing_event(mailing_event: MailingEvent, force: bool = False) -> None:
    if force or mailing_event.status == MailingEvent.Status.PENDING:
        try:
            result = sender_client.send_messages(
                receivers=[
                    {'email': recipient.get_staff_email()}
                    for recipient
                    in mailing_event.recipients.all()
                ],
                campaign_slug=MAILING_TASKS_CAMPAIGN_SLUGS_MAP[mailing_event.mailing.mailing_task],
                template_args=mailing_event.parameters,
            )
            status = result.get('result', {}).get('status', '').lower()
            if status == 'ok':
                mailing_event.status = MailingEvent.Status.PROCESSING
                mailing_event.result_message = json.dumps(result)
            elif status == 'cancelled':
                mailing_event.status = MailingEvent.Status.CANCELLED
                mailing_event.result_message = json.dumps(result)
            else:
                mailing_event.status = MailingEvent.Status.ERROR
                mailing_event.error_message = json.dumps(result)
        except Exception as exc:
            mailing_event.status = MailingEvent.Status.ERROR
            mailing_event.error_message = str(exc)
            log.error(traceback.print_exc())

        mailing_event.save()


def create_mailing_course_available_again(
    course: Course,
    course_mailing: CourseMailing,
    delta_participants: int,
):
    recipients = (
        CourseFollower.objects
        .filter(
            course=course,
            is_active=True,
        )
        .exclude(
            user__in=CourseStudent.objects.filter(
                course=course, status=CourseStudent.StatusChoices.ACTIVE,
            ).values_list('user', flat=True),
        )
        .exclude(
            user__in=EnrolledUser.objects.filter(
                course=course, status__in=EnrolledUser.USER_STATUS_KEYS,
            ).values_list('user', flat=True),
        )
        .values_list('user', flat=True)
    )

    parameters = {
        'coursename': course.name,
        'courselink': course.frontend_url,
        'slotscount': delta_participants,
    }

    create_mailing_events(course_mailing.mailing, recipients, [course], parameters)


def create_mailing_course_enroll_begin(
    course: Course,
    course_mailing: CourseMailing,
):
    recipients = (
        CourseFollower.objects
        .filter(
            course=course,
            is_active=True,
        )
        .exclude(
            user__in=CourseStudent.objects.filter(
                course=course, status=CourseStudent.StatusChoices.ACTIVE,
            ).values_list('user', flat=True),
        )
        .exclude(
            user__in=EnrolledUser.objects.filter(
                course=course, status__in=EnrolledUser.USER_STATUS_KEYS,
            ).values_list('user', flat=True),
        )
        .values_list('user', flat=True)
    )

    parameters = {
        'coursename': course.name,
        'courselink': course.frontend_url,
    }

    create_mailing_events(course_mailing.mailing, recipients, [course], parameters)


def create_mailing_new_follower(course_follower: CourseFollower, course_mailing: CourseMailing):
    user = course_follower.user
    staff_profile = getattr(user, 'staffprofile', None)
    course = course_follower.course
    parameters = {
        'username': user.username,
        'first_name': staff_profile.first_name() if staff_profile else user.first_name,
        'last_name': staff_profile.last_name() if staff_profile else user.last_name,
        'coursename': course.name,
        'courselink': course.frontend_url,
    }

    create_mailing_events(course_mailing.mailing, [user], [course], parameters)


@transaction.atomic
def create_mailing_events(mailing, recipients, courses, parameters):
    for chunk_recipients in chunks(recipients, MAILING_RECIPIENTS_CHUNK_SIZE):
        mailing_event = MailingEvent(
            mailing=mailing,
            parameters=parameters,
        )
        mailing_event.save()

        mailing_event.recipients.set(chunk_recipients)
        mailing_event.courses.set(courses)


def check_mailing_events(
    batch_limit: Optional[int] = None,
    time_limit: Optional[int] = None,
    mailing_event_ids: List[int] = None,
    force: bool = False,
) -> None:
    end_time = timezone.now() + timedelta(seconds=time_limit) if time_limit else None

    filter_kwargs = {}
    if not force:
        filter_kwargs['status'] = MailingEvent.Status.PROCESSING
    if mailing_event_ids is not None:
        filter_kwargs['id__in'] = mailing_event_ids
    mailing_events_to_check = MailingEvent.objects.filter(**filter_kwargs)
    if batch_limit is not None:
        mailing_events_to_check = mailing_events_to_check[:batch_limit]

    for mailing_event_to_check in mailing_events_to_check:
        if end_time and timezone.now() >= end_time:
            break
        with transaction.atomic():
            locked_mailing_event = (
                MailingEvent.objects
                .filter(id=mailing_event_to_check.id)
                .select_for_update(skip_locked=True)
                .select_related('mailing')
                .first()
            )
            if locked_mailing_event is not None:
                try:
                    campaign_slug = MAILING_TASKS_CAMPAIGN_SLUGS_MAP[locked_mailing_event.mailing.mailing_task]
                    parsed_result_message = json.loads(locked_mailing_event.result_message)
                    message_id = (
                        parsed_result_message.get('result', {}).get('message_id') or
                        parsed_result_message.get('message_id')
                    )
                    result = sender_client.status(
                        campaign_slug=campaign_slug,
                        message_id=message_id,
                    )
                    if 200 <= result['code'] <= 299:
                        locked_mailing_event.status = MailingEvent.Status.COMPLETED
                        locked_mailing_event.result_message = json.dumps(result)
                    elif result['retry']:
                        locked_mailing_event.error_message = json.dumps(result)
                    else:
                        locked_mailing_event.status = MailingEvent.Status.ERROR
                        locked_mailing_event.error_message = json.dumps(result)
                except Exception as exc:
                    locked_mailing_event.error_message = str(exc)
                locked_mailing_event.save()
            else:
                log.error(f"Mailing event {locked_mailing_event} locked or does not exist")


def unsubscribe_on_create_enrolled_user(enrolled_user: EnrolledUser):
    follower = CourseFollower.objects.filter(
        user=enrolled_user.user, course=enrolled_user.course, is_active=True,
    ).first()

    if follower is not None:
        follower.is_active = False
        follower.unsubscribed_date = timezone.now()
        follower.unsubscription_reason = CourseFollower.UnsubscriptionReasonChoice.ENROLLED

        bulk_update_with_history(
            objs=[follower],
            model=CourseFollower,
            fields=['is_active', 'unsubscribed_date', 'unsubscription_reason'],
            batch_size=settings.BULK_BATCH_SIZE_DEFAULT,
        )


def unsubscribe_on_dismiss(staff_profile: StaffProfile):
    followers = list(CourseFollower.objects.filter(user=staff_profile.user, is_active=True))

    for follower in followers:
        follower.is_active = False
        follower.unsubscribed_date = timezone.now()
        follower.unsubscription_reason = CourseFollower.UnsubscriptionReasonChoice.DISMISSED

    bulk_update_with_history(
        objs=followers,
        model=CourseFollower,
        fields=['is_active', 'unsubscribed_date', 'unsubscription_reason'],
        batch_size=settings.BULK_BATCH_SIZE_DEFAULT,
    )
