from datetime import date, datetime, timedelta
from typing import Optional

from django.core.exceptions import PermissionDenied
from django.db.models import Count, Exists, OuterRef, Q, Subquery, Sum
from django.http import Http404, HttpRequest, HttpResponse, HttpResponseNotFound
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse, reverse_lazy
from django.utils.translation import ugettext as _

from calendarweek import CalendarWeek
from django_tables2 import SingleTableView
from reversion.views import RevisionMixin
from rules.contrib.views import PermissionRequiredMixin

from aleksis.apps.chronos.managers import TimetableType
from aleksis.apps.chronos.models import LessonPeriod
from aleksis.apps.chronos.util.chronos_helpers import get_el_by_pk
from aleksis.apps.chronos.util.date import get_weeks_for_year, week_weekday_to_date
from aleksis.core.mixins import AdvancedCreateView, AdvancedDeleteView, AdvancedEditView
from aleksis.core.models import Group, Person, SchoolTerm
from aleksis.core.util import messages
from aleksis.core.util.core_helpers import get_site_preferences, objectgetter_optional

from .forms import (
    ExcuseTypeForm,
    ExtraMarkForm,
    LessonDocumentationForm,
    PersonalNoteFormSet,
    RegisterAbsenceForm,
    SelectForm,
)
from .models import ExcuseType, ExtraMark, LessonDocumentation, PersonalNote
from .tables import ExcuseTypeTable, ExtraMarkTable


def lesson(
    request: HttpRequest,
    year: Optional[int] = None,
    week: Optional[int] = None,
    period_id: Optional[int] = None,
) -> HttpResponse:
    context = {}

    if year and week and period_id:
        # Get a specific lesson period if provided in URL
        wanted_week = CalendarWeek(year=year, week=week)
        lesson_period = LessonPeriod.objects.annotate_week(wanted_week).get(
            pk=period_id
        )

        date_of_lesson = week_weekday_to_date(wanted_week, lesson_period.period.weekday)

        if (
            date_of_lesson < lesson_period.lesson.validity.date_start
            or date_of_lesson > lesson_period.lesson.validity.date_end
        ):
            return HttpResponseNotFound()
    else:
        # Determine current lesson by current date and time
        lesson_period = (
            LessonPeriod.objects.at_time().filter_teacher(request.user.person).first()
        )
        wanted_week = CalendarWeek()

        if lesson_period:
            return redirect(
                "lesson_by_week_and_period",
                wanted_week.year,
                wanted_week.week,
                lesson_period.pk,
            )
        else:
            raise Http404(
                _(
                    "You either selected an invalid lesson or "
                    "there is currently no lesson in progress."
                )
            )

    if (
        datetime.combine(
            wanted_week[lesson_period.period.weekday], lesson_period.period.time_start,
        )
        > datetime.now()
        and not request.user.is_superuser
    ):
        raise PermissionDenied(
            _(
                "You are not allowed to create a lesson documentation for a lesson in the future."
            )
        )

    context["lesson_period"] = lesson_period
    context["week"] = wanted_week
    context["day"] = wanted_week[lesson_period.period.weekday]

    # Create or get lesson documentation object; can be empty when first opening lesson
    lesson_documentation = lesson_period.get_or_create_lesson_documentation(wanted_week)
    lesson_documentation_form = LessonDocumentationForm(
        request.POST or None,
        instance=lesson_documentation,
        prefix="lesson_documentation",
    )

    # Create a formset that holds all personal notes for all persons in this lesson
    persons_qs = lesson_period.get_personal_notes(wanted_week)
    personal_note_formset = PersonalNoteFormSet(
        request.POST or None, queryset=persons_qs, prefix="personal_notes"
    )

    if request.method == "POST":
        if lesson_documentation_form.is_valid():
            lesson_documentation_form.save()

            messages.success(request, _("The lesson documentation has been saved."))

        substitution = lesson_period.get_substitution()
        if (
            not getattr(substitution, "cancelled", False)
            or not get_site_preferences()["alsijil__block_personal_notes_for_cancelled"]
        ):
            if personal_note_formset.is_valid():
                instances = personal_note_formset.save()

                # Iterate over personal notes and carry changed absences to following lessons
                for instance in instances:
                    instance.person.mark_absent(
                        wanted_week[lesson_period.period.weekday],
                        lesson_period.period.period + 1,
                        instance.absent,
                        instance.excused,
                        instance.excuse_type,
                    )

            messages.success(request, _("The personal notes have been saved."))

            # Regenerate form here to ensure that programmatically changed data will be shown correctly
            personal_note_formset = PersonalNoteFormSet(
                None, queryset=persons_qs, prefix="personal_notes"
            )

    context["lesson_documentation"] = lesson_documentation
    context["lesson_documentation_form"] = lesson_documentation_form
    context["personal_note_formset"] = personal_note_formset

    return render(request, "alsijil/class_register/lesson.html", context)


def week_view(
    request: HttpRequest,
    year: Optional[int] = None,
    week: Optional[int] = None,
    type_: Optional[str] = None,
    id_: Optional[int] = None,
) -> HttpResponse:
    context = {}

    if year and week:
        wanted_week = CalendarWeek(year=year, week=week)
    else:
        wanted_week = CalendarWeek()

    lesson_periods = LessonPeriod.objects.annotate(
        has_documentation=Exists(
            LessonDocumentation.objects.filter(
                ~Q(topic__exact=""),
                lesson_period=OuterRef("pk"),
                week=wanted_week.week,
                year=wanted_week.year,
            )
        )
    ).in_week(wanted_week)

    group = None
    if type_ and id_:
        instance = get_el_by_pk(request, type_, id_)

        if isinstance(instance, HttpResponseNotFound):
            return HttpResponseNotFound()

        type_ = TimetableType.from_string(type_)

        if type_ == TimetableType.GROUP:
            group = instance

        lesson_periods = lesson_periods.filter_from_type(type_, instance)
    elif hasattr(request, "user") and hasattr(request.user, "person"):
        instance = request.user.person
        if request.user.person.lessons_as_teacher.exists():
            lesson_periods = lesson_periods.filter_teacher(request.user.person)
            type_ = TimetableType.TEACHER
        else:
            lesson_periods = lesson_periods.filter_participant(request.user.person)
    else:
        lesson_periods = None

    # Add a form to filter the view
    if type_:
        initial = {type_.value: instance}
    else:
        initial = {}
    select_form = SelectForm(request.POST or None, initial=initial)

    if request.method == "POST":
        if select_form.is_valid():
            if "type_" not in select_form.cleaned_data:
                return redirect("week_view_by_week", wanted_week.year, wanted_week.week)
            else:
                return redirect(
                    "week_view_by_week",
                    wanted_week.year,
                    wanted_week.week,
                    select_form.cleaned_data["type_"].value,
                    select_form.cleaned_data["instance"].pk,
                )

    if lesson_periods:
        # Aggregate all personal notes for this group and week
        lesson_periods_pk = list(lesson_periods.values_list("pk", flat=True))

        persons_qs = Person.objects.filter(is_active=True)

        if group:
            persons_qs = persons_qs.filter(member_of=group)
        else:
            persons_qs = persons_qs.filter(
                member_of__lessons__lesson_periods__in=lesson_periods_pk
            )

        persons_qs = (
            persons_qs.distinct()
            .prefetch_related("personal_notes")
            .annotate(
                absences_count=Count(
                    "personal_notes",
                    filter=Q(
                        personal_notes__lesson_period__in=lesson_periods_pk,
                        personal_notes__week=wanted_week.week,
                        personal_notes__year=wanted_week.year,
                        personal_notes__absent=True,
                    ),
                    distinct=True,
                ),
                unexcused_count=Count(
                    "personal_notes",
                    filter=Q(
                        personal_notes__lesson_period__in=lesson_periods_pk,
                        personal_notes__week=wanted_week.week,
                        personal_notes__year=wanted_week.year,
                        personal_notes__absent=True,
                        personal_notes__excused=False,
                    ),
                    distinct=True,
                ),
                tardiness_sum=Subquery(
                    Person.objects.filter(
                        pk=OuterRef("pk"),
                        personal_notes__lesson_period__in=lesson_periods_pk,
                        personal_notes__week=wanted_week.week,
                        personal_notes__year=wanted_week.year,
                    )
                    .distinct()
                    .annotate(tardiness_sum=Sum("personal_notes__late"))
                    .values("tardiness_sum")
                ),
            )
        )

        for extra_mark in ExtraMark.objects.all():
            persons_qs = persons_qs.annotate(
                **{
                    extra_mark.count_label: Count(
                        "personal_notes",
                        filter=Q(
                            personal_notes__lesson_period__in=lesson_periods_pk,
                            personal_notes__week=wanted_week.week,
                            personal_notes__year=wanted_week.year,
                            personal_notes__extra_marks=extra_mark,
                        ),
                        distinct=True,
                    )
                }
            )

        persons = []
        for person in persons_qs:
            persons.append(
                {
                    "person": person,
                    "personal_notes": person.personal_notes.filter(
                        week=wanted_week.week,
                        year=wanted_week.year,
                        lesson_period__in=lesson_periods_pk,
                    ),
                }
            )
    else:
        persons = None

    # Resort lesson periods manually because an union queryset doesn't support order_by
    lesson_periods = sorted(
        lesson_periods, key=lambda x: (x.period.weekday, x.period.period)
    )

    context["extra_marks"] = ExtraMark.objects.all()
    context["week"] = wanted_week
    context["weeks"] = get_weeks_for_year(year=wanted_week.year)
    context["lesson_periods"] = lesson_periods
    context["persons"] = persons
    context["group"] = group
    context["select_form"] = select_form
    context["instance"] = instance

    week_prev = wanted_week - 1
    week_next = wanted_week + 1
    args_prev = [week_prev.year, week_prev.week]
    args_next = [week_next.year, week_next.week]
    args_dest = []
    if type_ and id_:
        args_prev += [type_.value, id_]
        args_next += [type_.value, id_]
        args_dest += [type_.value, id_]

    context["week_select"] = {
        "year": wanted_week.year,
        "dest": reverse("week_view_placeholders", args=args_dest),
    }

    context["url_prev"] = reverse("week_view_by_week", args=args_prev)
    context["url_next"] = reverse("week_view_by_week", args=args_next)

    return render(request, "alsijil/class_register/week_view.html", context)


def full_register_group(request: HttpRequest, id_: int) -> HttpResponse:
    context = {}

    group = get_object_or_404(Group, pk=id_)

    current_school_term = SchoolTerm.current

    if not current_school_term:
        return HttpResponseNotFound(_("There is no current school term."))

    # Get all lesson periods for the selected group
    lesson_periods = (
        LessonPeriod.objects.filter_group(group)
        .filter(lesson__validity__school_term=current_school_term)
        .distinct()
        .prefetch_related("documentations", "personal_notes")
    )

    weeks = CalendarWeek.weeks_within(
        current_school_term.date_start, current_school_term.date_end,
    )

    periods_by_day = {}
    for lesson_period in lesson_periods:
        for week in weeks:
            day = week[lesson_period.period.weekday]

            if (
                lesson_period.lesson.validity.date_start
                <= day
                <= lesson_period.lesson.validity.date_end
            ):
                documentations = list(
                    filter(
                        lambda d: d.week == week.week and d.year == week.year,
                        lesson_period.documentations.all(),
                    )
                )
                notes = list(
                    filter(
                        lambda d: d.week == week.week and d.year == week.year,
                        lesson_period.personal_notes.all(),
                    )
                )
                substitution = lesson_period.get_substitution(week)

                periods_by_day.setdefault(day, []).append(
                    (lesson_period, documentations, notes, substitution)
                )

    persons = Person.objects.filter(
        personal_notes__groups_of_person=group,
        personal_notes__lesson_period__lesson__validity__school_term=current_school_term,
    ).annotate(
        absences_count=Count(
            "personal_notes__absent",
            filter=Q(
                personal_notes__absent=True,
                personal_notes__lesson_period__lesson__validity__school_term=current_school_term,
            ),
        ),
        excused=Count(
            "personal_notes__absent",
            filter=Q(
                personal_notes__absent=True,
                personal_notes__excused=True,
                personal_notes__excuse_type__isnull=True,
                personal_notes__lesson_period__lesson__validity__school_term=current_school_term,
            ),
        ),
        unexcused=Count(
            "personal_notes__absent",
            filter=Q(
                personal_notes__absent=True,
                personal_notes__excused=False,
                personal_notes__lesson_period__lesson__validity__school_term=current_school_term,
            ),
        ),
        tardiness=Sum("personal_notes__late"),
    )

    for extra_mark in ExtraMark.objects.all():
        persons = persons.annotate(
            **{
                extra_mark.count_label: Count(
                    "personal_notes",
                    filter=Q(
                        personal_notes__extra_marks=extra_mark,
                        personal_notes__lesson_period__lesson__validity__school_term=current_school_term,
                    ),
                )
            }
        )

    for excuse_type in ExcuseType.objects.all():
        persons = persons.annotate(
            **{
                excuse_type.count_label: Count(
                    "personal_notes__absent",
                    filter=Q(
                        personal_notes__absent=True,
                        personal_notes__excuse_type=excuse_type,
                        personal_notes__lesson_period__lesson__validity__school_term=current_school_term,
                    ),
                )
            }
        )

    context["school_term"] = current_school_term
    context["persons"] = persons
    context["excuse_types"] = ExcuseType.objects.all()
    context["extra_marks"] = ExtraMark.objects.all()
    context["group"] = group
    context["weeks"] = weeks
    context["periods_by_day"] = periods_by_day
    context["lesson_periods"] = lesson_periods
    context["today"] = date.today()

    return render(request, "alsijil/print/full_register.html", context)


def overview_person(request: HttpRequest, id_: Optional[int] = None) -> HttpResponse:
    context = {}
    person = objectgetter_optional(Person, default="request.user.person", default_eval=True)(
        request, id_
    )
    context["person"] = person

    if request.method == "POST":
        if request.POST.get("excuse_type"):
            # Get excuse type
            excuse_type = request.POST["excuse_type"]
            found = False
            if excuse_type == "e":
                excuse_type = None
                found = True
            else:
                try:
                    excuse_type = ExcuseType.objects.get(pk=int(excuse_type))
                    found = True
                except (ExcuseType.DoesNotExist, ValueError):
                    pass

            if found:
                if request.POST.get("date"):
                    # Mark absences on date as excused
                    try:
                        date = datetime.strptime(
                            request.POST["date"], "%Y-%m-%d"
                        ).date()

                        notes = person.personal_notes.filter(
                            week=date.isocalendar()[1],
                            lesson_period__period__weekday=date.weekday(),
                            lesson_period__lesson__validity__date_start__lte=date,
                            lesson_period__lesson__validity__date_end__gte=date,
                            absent=True,
                            excused=False,
                        )
                        notes.update(excused=True, excuse_type=excuse_type)
                        messages.success(
                            request, _("The absences have been marked as excused.")
                        )
                    except ValueError:
                        pass
                elif request.POST.get("personal_note"):
                    # Mark specific absence as excused
                    try:
                        note = PersonalNote.objects.get(
                            pk=int(request.POST["personal_note"])
                        )
                        if note.absent:
                            note.excused = True
                            note.excuse_type = excuse_type
                            note.save()
                            messages.success(
                                request, _("The absence has been marked as excused.")
                            )
                    except (PersonalNote.DoesNotExist, ValueError):
                        pass

                person.refresh_from_db()

    unexcused_absences = person.personal_notes.filter(absent=True, excused=False)
    context["unexcused_absences"] = unexcused_absences

    personal_notes = person.personal_notes.filter(
        Q(absent=True) | Q(late__gt=0) | ~Q(remarks="") | Q(extra_marks__isnull=False)
    ).order_by(
        "-lesson_period__lesson__validity__date_start",
        "-week",
        "lesson_period__period__weekday",
        "lesson_period__period__period",
    )
    context["personal_notes"] = personal_notes
    context["excuse_types"] = ExcuseType.objects.all()
    return render(request, "alsijil/class_register/person.html", context)


def register_absence(request: HttpRequest) -> HttpResponse:
    context = {}

    register_absence_form = RegisterAbsenceForm(request.POST or None)

    if request.method == "POST":
        if register_absence_form.is_valid():
            # Get data from form
            person = register_absence_form.cleaned_data["person"]
            start_date = register_absence_form.cleaned_data["date_start"]
            end_date = register_absence_form.cleaned_data["date_end"]
            from_period = register_absence_form.cleaned_data["from_period"]
            absent = register_absence_form.cleaned_data["absent"]
            excused = register_absence_form.cleaned_data["excused"]
            remarks = register_absence_form.cleaned_data["remarks"]

            # Mark person as absent
            delta = end_date - start_date
            for i in range(delta.days + 1):
                from_period = from_period if i == 0 else 0
                day = start_date + timedelta(days=i)
                person.mark_absent(day, from_period, absent, excused, remarks)

            messages.success(request, _("The absence has been saved."))
            return redirect("index")

    context["register_absence_form"] = register_absence_form

    return render(request, "alsijil/absences/register.html", context)


class ExtraMarkListView(SingleTableView, PermissionRequiredMixin):
    """Table of all extra marks."""

    model = ExtraMark
    table_class = ExtraMarkTable
    permission_required = "core.view_extramark"
    template_name = "alsijil/extra_mark/list.html"


class ExtraMarkCreateView(AdvancedCreateView, PermissionRequiredMixin):
    """Create view for extra marks."""

    model = ExtraMark
    form_class = ExtraMarkForm
    permission_required = "core.create_extramark"
    template_name = "alsijil/extra_mark/create.html"
    success_url = reverse_lazy("extra_marks")
    success_message = _("The extra mark has been created.")


class ExtraMarkEditView(AdvancedEditView, PermissionRequiredMixin):
    """Edit view for extra marks."""

    model = ExtraMark
    form_class = ExtraMarkForm
    permission_required = "core.edit_extramark"
    template_name = "alsijil/extra_mark/edit.html"
    success_url = reverse_lazy("extra_marks")
    success_message = _("The extra mark has been saved.")


class ExtraMarkDeleteView(AdvancedDeleteView, PermissionRequiredMixin, RevisionMixin):
    """Delete view for extra marks"""

    model = ExtraMark
    permission_required = "core.delete_extramark"
    template_name = "core/pages/delete.html"
    success_url = reverse_lazy("extra_marks")
    success_message = _("The extra mark has been deleted.")


class ExcuseTypeListView(SingleTableView, PermissionRequiredMixin):
    """Table of all excuse types."""

    model = ExcuseType
    table_class = ExcuseTypeTable
    permission_required = "core.view_excusetype"
    template_name = "alsijil/excuse_type/list.html"


class ExcuseTypeCreateView(AdvancedCreateView, PermissionRequiredMixin):
    """Create view for excuse types."""

    model = ExcuseType
    form_class = ExcuseTypeForm
    permission_required = "core.create_excusetype"
    template_name = "alsijil/excuse_type/create.html"
    success_url = reverse_lazy("excuse_types")
    success_message = _("The excuse type has been created.")


class ExcuseTypeEditView(AdvancedEditView, PermissionRequiredMixin):
    """Edit view for excuse types."""

    model = ExcuseType
    form_class = ExcuseTypeForm
    permission_required = "core.edit_excusetype"
    template_name = "alsijil/excuse_type/edit.html"
    success_url = reverse_lazy("excuse_types")
    success_message = _("The excuse type has been saved.")


class ExcuseTypeDeleteView(AdvancedDeleteView, PermissionRequiredMixin, RevisionMixin):
    """Delete view for excuse types"""

    model = ExcuseType
    permission_required = "core.delete_excusetype"
    template_name = "core/pages/delete.html"
    success_url = reverse_lazy("excuse_types")
    success_message = _("The excuse type has been deleted.")