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

from aleksis.apps.chronos.managers import TimetableType
from aleksis.apps.chronos.util.chronos_helpers import get_el_by_pk
from django.core.exceptions import PermissionDenied
from django.db.models import Count, Exists, OuterRef, Q, Sum
from django.http import Http404, HttpRequest, HttpResponse, HttpResponseNotFound
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils.translation import ugettext as _

from calendarweek import CalendarWeek
from django_tables2 import RequestConfig
from rules.contrib.views import permission_required

from aleksis.apps.chronos.models import LessonPeriod
from aleksis.core.models import Group, Person, SchoolYear
from aleksis.core.util import messages
from aleksis.core.util.core_helpers import objectgetter_optional
from .forms import (
    LessonDocumentationForm,
    PersonalNoteFilterForm,
    PersonalNoteFormSet,
    RegisterAbsenceForm,
    SelectForm,
)
from .models import LessonDocumentation, PersonalNoteFilter
from .tables import PersonalNoteFilterTable
from .util.alsijil_helpers import get_lesson_period_by_pk, get_lesson_periods_by_pk


@permission_required("alsijil.view_lesson", fn=get_lesson_period_by_pk)
def lesson(
    request: HttpRequest,
    year: Optional[int] = None,
    week: Optional[int] = None,
    period_id: Optional[int] = None,
) -> HttpResponse:
    context = {}

    lesson_period, wanted_week = get_lesson_period_by_pk(request, year, week, period_id)

    if not (year and week and period_id):
        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 - 1], 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 - 1]

    # Create or get lesson documentation object; can be empty when first opening lesson
    lesson_documentation, created = LessonDocumentation.objects.get_or_create(
        lesson_period=lesson_period, week=wanted_week.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()

        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 - 1],
                    lesson_period.period.period + 1,
                    instance.absent,
                    instance.excused,
                )

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

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


@permission_required("alsijil.view_week", fn=get_lesson_periods_by_pk)
def week_view(
    request: HttpRequest, year: Optional[int] = None, week: Optional[int] = None, type_: Optional[str] = None, id_: Optional[int] = None
) -> HttpResponse:
    context = {}

    lesson_periods, wanted_week, type_, instance = get_lesson_periods_by_pk(request, year, week, type_, id_)

    # 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 type_ == TimetableType.GROUP:
        group = instance
    else:
        group = None

    if lesson_periods:
        # Aggregate all personal notes for this group and week
        lesson_periods_pk = lesson_periods.values_list("pk", flat=True)
        persons = (
            Person.objects.filter(is_active=True)
            .filter(member_of__lessons__lesson_periods__in=lesson_periods_pk)
            .distinct()
            .prefetch_related("personal_notes")
            .annotate(
                absences_count=Count(
                    "personal_notes__absent",
                    filter=Q(
                        personal_notes__lesson_period__in=lesson_periods_pk,
                        personal_notes__week=wanted_week.week,
                        personal_notes__absent=True,
                    ),
                ),
                unexcused_count=Count(
                    "personal_notes__absent",
                    filter=Q(
                        personal_notes__lesson_period__in=lesson_periods_pk,
                        personal_notes__week=wanted_week.week,
                        personal_notes__absent=True,
                        personal_notes__excused=False,
                    ),
                ),
                tardiness_sum=Sum(
                    "personal_notes__late",
                    filter=Q(
                        personal_notes__lesson_period__in=lesson_periods_pk,
                        personal_notes__week=wanted_week.week,
                    ),
                ),
            )
        )
    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["week"] = wanted_week
    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
    context["url_prev"] = "%s?%s" % (
        reverse("week_view_by_week", args=[week_prev.year, week_prev.week]),
        request.GET.urlencode(),
    )
    context["url_next"] = "%s?%s" % (
        reverse("week_view_by_week", args=[week_next.year, week_next.week]),
        request.GET.urlencode(),
    )

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


@permission_required("alsijil.full_register_group", fn=objectgetter_optional(Group, None, False))
def full_register_group(request: HttpRequest, id_: int) -> HttpResponse:
    context = {}

    group = get_object_or_404(Group, pk=id_)

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

    weeks = CalendarWeek.weeks_within(
        SchoolYear.current.date_start,
        SchoolYear.current.date_end,
    )

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

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

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

    persons = group.members.annotate(
        absences_count=Count("personal_notes__absent", filter=Q(personal_notes__absent=True)),
        unexcused=Count(
            "personal_notes__absent",
            filter=Q(personal_notes__absent=True, personal_notes__excused=False),
        ),
        tardiness=Sum("personal_notes__late"),
    )

    # FIXME Move to manager
    personal_note_filters = PersonalNoteFilter.objects.all()
    for personal_note_filter in personal_note_filters:
        persons = persons.annotate(
            **{
                "_personal_notes_with_%s"
                % personal_note_filter.identifier: Count(
                    "personal_notes__remarks",
                    filter=Q(personal_notes__remarks__iregex=personal_note_filter.regex),
                )
            }
        )

    context["persons"] = persons
    context["personal_note_filters"] = personal_note_filters
    context["group"] = group
    context["weeks"] = weeks
    context["periods_by_day"] = periods_by_day
    context["today"] = date.today()

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


@permission_required("alsijil.register_absence")
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/register_absence.html", context)


@permission_required("alsijil.list_personal_note_filters")
def list_personal_note_filters(request: HttpRequest) -> HttpResponse:
    context = {}

    personal_note_filters = PersonalNoteFilter.objects.all()

    # Prepare table
    personal_note_filters_table = PersonalNoteFilterTable(personal_note_filters)
    RequestConfig(request).configure(personal_note_filters_table)

    context["personal_note_filters_table"] = personal_note_filters_table

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


@permission_required("alsijil.edit_personal_note_filter", fn=objectgetter_optional(PersonalNoteFilter, None, False))
def edit_personal_note_filter(request: HttpRequest, id: Optional["int"] = None) -> HttpResponse:
    context = {}

    if id:
        personal_note_filter = PersonalNoteFilter.objects.get(id=id)
        context["personal_note_filter"] = personal_note_filter
        personal_note_filter_form = PersonalNoteFilterForm(
            request.POST or None, instance=personal_note_filter
        )
    else:
        personal_note_filter_form = PersonalNoteFilterForm(request.POST or None)

    if request.method == "POST":
        if personal_note_filter_form.is_valid():
            personal_note_filter_form.save(commit=True)

            messages.success(request, _("The filter has been saved"))
            return redirect("list_personal_note_filters")

    context["personal_note_filter_form"] = personal_note_filter_form

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


@permission_required("alsijil.delete_personal_note_filter", fn=objectgetter_optional(PersonalNoteFilter, None, False))
def delete_personal_note_filter(request: HttpRequest, id_: int) -> HttpResponse:
    context = {}

    personal_note_filter = get_object_or_404(PersonalNoteFilter, pk=id_)

    PersonalNoteFilter.objects.filter(pk=id_).delete()

    messages.success(request, _("The filter has been deleted."))

    context["personal_note_filter"] = personal_note_filter
    return redirect("list_personal_note_filters")