from datetime import date
from typing import Dict, Optional, Union

from django.db.models import Exists, OuterRef, QuerySet

import reversion
from calendarweek import CalendarWeek

from aleksis.apps.chronos.models import LessonPeriod
from aleksis.core.models import Group, Person

from .models import ExcuseType, ExtraMark, LessonDocumentation, PersonalNote


@Person.method
def mark_absent(
    self,
    day: date,
    from_period: int = 0,
    absent: bool = True,
    excused: bool = False,
    excuse_type: Optional[ExcuseType] = None,
    remarks: str = "",
    to_period: Optional[int] = None,
):
    """Mark a person absent for all lessons in a day, optionally starting with a selected period number.

    This function creates `PersonalNote` objects for every `LessonPeriod` the person
    participates in on the selected day and marks them as absent/excused.

    ..note:: Only available when AlekSIS-App-Alsijil is installed.

    :Date: 2019-11-10
    :Authors:
        - Dominik George <dominik.george@teckids.org>
    """
    wanted_week = CalendarWeek.from_date(day)

    # Get all lessons of this person on the specified day
    lesson_periods = self.lesson_periods_as_participant.on_day(day).filter(
        period__period__gte=from_period
    )

    if to_period:
        lesson_periods = lesson_periods.filter(period__period__lte=to_period)

    # Create and update all personal notes for the discovered lesson periods
    for lesson_period in lesson_periods:
        with reversion.create_revision():
            personal_note, created = PersonalNote.objects.update_or_create(
                person=self,
                lesson_period=lesson_period,
                week=wanted_week.week,
                year=wanted_week.year,
                defaults={
                    "absent": absent,
                    "excused": excused,
                    "excuse_type": excuse_type,
                },
            )
            personal_note.groups_of_person.set(self.member_of.all())

            if remarks:
                if personal_note.remarks:
                    personal_note.remarks += "; %s" % remarks
                else:
                    personal_note.remarks = remarks
                personal_note.save()


@LessonPeriod.method
def get_personal_notes(self, wanted_week: CalendarWeek):
    """Get all personal notes for that lesson in a specified week.

    Returns all linked `PersonalNote` objects, filtered by the given weeek,
    creating those objects that haven't been created yet.

    ..note:: Only available when AlekSIS-App-Alsijil is installed.

    :Date: 2019-11-09
    :Authors:
        - Dominik George <dominik.george@teckids.org>
    """
    # Find all persons in the associated groups that do not yet have a personal note for this lesson
    missing_persons = Person.objects.annotate(
        no_personal_notes=~Exists(
            PersonalNote.objects.filter(
                week=wanted_week.week,
                year=wanted_week.year,
                lesson_period=self,
                person__pk=OuterRef("pk"),
            )
        )
    ).filter(
        member_of__in=Group.objects.filter(pk__in=self.lesson.groups.all()),
        is_active=True,
        no_personal_notes=True,
    )

    # Create all missing personal notes
    new_personal_notes = [
        PersonalNote(
            person=person,
            lesson_period=self,
            week=wanted_week.week,
            year=wanted_week.year,
        )
        for person in missing_persons
    ]
    PersonalNote.objects.bulk_create(new_personal_notes)

    for personal_note in new_personal_notes:
        personal_note.groups_of_person.set(personal_note.person.member_of.all())

    return PersonalNote.objects.select_related("person").filter(
        lesson_period=self, week=wanted_week.week, year=wanted_week.year
    )


@LessonPeriod.method
def get_lesson_documentation(
    self, week: Optional[CalendarWeek] = None
) -> Union[LessonDocumentation, None]:
    """Get lesson documentation object for this lesson."""
    if not week:
        week = self.week
    try:
        return LessonDocumentation.objects.get(
            lesson_period=self, week=week.week, year=week.year
        )
    except LessonDocumentation.DoesNotExist:
        return None


@LessonPeriod.method
def get_or_create_lesson_documentation(
    self, week: Optional[CalendarWeek] = None
) -> LessonDocumentation:
    """Get or create lesson documentation object for this lesson."""
    if not week:
        week = self.week
    lesson_documentation, created = LessonDocumentation.objects.get_or_create(
        lesson_period=self, week=week.week, year=week.year
    )
    return lesson_documentation


@LessonPeriod.method
def get_absences(self, week: Optional[CalendarWeek] = None) -> QuerySet:
    """Get all personal notes of absent persons for this lesson."""
    if not week:
        week = self.week
    return self.personal_notes.filter(week=week.week, year=week.year, absent=True)


@LessonPeriod.method
def get_excused_absences(self, week: Optional[CalendarWeek] = None) -> QuerySet:
    """Get all personal notes of excused absent persons for this lesson."""
    if not week:
        week = self.week
    return self.personal_notes.filter(
        week=week.week, year=week.year, absent=True, excused=True
    )


@LessonPeriod.method
def get_unexcused_absences(self, week: Optional[CalendarWeek] = None) -> QuerySet:
    """Get all personal notes of unexcused absent persons for this lesson."""
    if not week:
        week = self.week
    return self.personal_notes.filter(
        week=week.week, year=week.year, absent=True, excused=False
    )


@LessonPeriod.method
def get_tardinesses(self, week: Optional[CalendarWeek] = None) -> QuerySet:
    """Get all personal notes of late persons for this lesson."""
    if not week:
        week = self.week
    return self.personal_notes.filter(week=week.week, year=week.year, late__gt=0)


@LessonPeriod.method
def get_extra_marks(
    self, week: Optional[CalendarWeek] = None
) -> Dict[ExtraMark, QuerySet]:
    """Get all statistics on extra marks for this lesson."""
    if not week:
        week = self.week

    stats = {}
    for extra_mark in ExtraMark.objects.all():
        qs = self.personal_notes.filter(
            week=week.week, year=week.year, extra_marks=extra_mark
        )
        if qs:
            stats[extra_mark] = qs

    return stats