from datetime import datetime
from typing import Optional

from django.apps import apps
from django.db.models import FilteredRelation, Q
from django.http import HttpRequest, HttpResponse, HttpResponseNotFound
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils import timezone
from django.utils.translation import ugettext as _
from django.views.decorators.cache import never_cache

import reversion
from django_tables2 import RequestConfig
from rules.contrib.views import permission_required

from aleksis.core.models import Announcement
from aleksis.core.util import messages
from aleksis.core.util.core_helpers import has_person
from aleksis.core.util.pdf import render_pdf

from .filters import LessonPeriodFilter, SupervisionFilter
from .forms import LessonSubstitutionForm, SupervisionSubstitutionForm
from .managers import TimetableType
from .models import Holiday, LessonPeriod, Supervision, TimePeriod
from .tables import LessonsTable, SupervisionsTable
from .util.build import build_timetable, build_weekdays
from .util.change_tracker import TimetableDataChangeTracker
from .util.chronos_helpers import (
    get_classes,
    get_el_by_pk,
    get_rooms,
    get_substitution_by_id,
    get_substitutions_context_data,
    get_supervision_substitution_by_id,
    get_teachers,
)
from .util.date import CalendarWeek, get_weeks_for_year, week_weekday_to_date
from .util.js import date_unix


@permission_required("chronos.view_timetable_overview_rule")
def all_timetables(request: HttpRequest) -> HttpResponse:
    """View all timetables for persons, groups and rooms."""
    context = {}

    user = request.user
    teachers, classes, rooms = get_teachers(user), get_classes(user), get_rooms(user)

    context["teachers"] = teachers
    context["classes"] = classes
    context["rooms"] = rooms

    return render(request, "chronos/all.html", context)


@permission_required("chronos.view_my_timetable_rule")
def my_timetable(
    request: HttpRequest,
    year: Optional[int] = None,
    month: Optional[int] = None,
    day: Optional[int] = None,
) -> HttpResponse:
    """View personal timetable on a specified date."""
    context = {}

    if day:
        wanted_day = timezone.datetime(year=year, month=month, day=day).date()
        wanted_day = TimePeriod.get_next_relevant_day(wanted_day)
    else:
        wanted_day = TimePeriod.get_next_relevant_day(timezone.now().date(), datetime.now().time())

    wanted_week = CalendarWeek.from_date(wanted_day)

    if has_person(request.user):
        person = request.user.person
        type_ = person.timetable_type

        # Build timetable
        timetable = build_timetable("person", person, wanted_day)
        week_timetable = build_timetable("person", person, wanted_week)

        if type_ is None:
            # If no student or teacher, redirect to all timetables
            return redirect("all_timetables")

        super_el = person.timetable_object

        context["timetable"] = timetable
        context["week_timetable"] = week_timetable
        context["holiday"] = Holiday.on_day(wanted_day)
        context["super"] = {"type": type_, "el": super_el}
        context["type"] = type_
        context["day"] = wanted_day
        context["today"] = timezone.now().date()
        context["week"] = wanted_week
        context["periods"] = TimePeriod.get_times_dict()
        context["smart"] = True
        context["announcements"] = (
            Announcement.for_timetables().on_date(wanted_day).for_person(person)
        )
        context["week_announcements"] = (
            Announcement.for_timetables()
            .within_days(wanted_week[0], wanted_week[6])
            .for_person(person)
        )
        context["weekdays"] = build_weekdays(TimePeriod.WEEKDAY_CHOICES, wanted_week)
        context["weekdays_short"] = build_weekdays(TimePeriod.WEEKDAY_CHOICES_SHORT, wanted_week)
        context["url_prev"], context["url_next"] = TimePeriod.get_prev_next_by_day(
            wanted_day, "my_timetable_by_date"
        )

        return render(request, "chronos/my_timetable.html", context)
    else:
        return redirect("all_timetables")


@permission_required("chronos.view_timetable_rule", fn=get_el_by_pk)
def timetable(
    request: HttpRequest,
    type_: str,
    pk: int,
    year: Optional[int] = None,
    week: Optional[int] = None,
    regular: Optional[str] = None,
    is_print: bool = False,
) -> HttpResponse:
    """View a selected timetable for a person, group or room."""
    context = {}

    is_smart = regular != "regular"

    if is_print:
        is_smart = False

    el = get_el_by_pk(request, type_, pk, prefetch=True)

    if type(el) == HttpResponseNotFound:
        return HttpResponseNotFound()

    type_ = TimetableType.from_string(type_)

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

    # Build timetable
    timetable = build_timetable(type_, el, wanted_week, with_holidays=is_smart)
    context["timetable"] = timetable

    # Add time periods
    context["periods"] = TimePeriod.get_times_dict()

    # Build lists with weekdays and corresponding dates (long and short variant)
    context["weekdays"] = build_weekdays(
        TimePeriod.WEEKDAY_CHOICES, wanted_week, with_holidays=is_smart
    )
    context["weekdays_short"] = build_weekdays(
        TimePeriod.WEEKDAY_CHOICES_SHORT, wanted_week, with_holidays=is_smart
    )

    context["weeks"] = get_weeks_for_year(year=wanted_week.year)
    context["week"] = wanted_week
    context["type"] = type_
    context["pk"] = pk
    context["el"] = el
    context["smart"] = is_smart
    context["week_select"] = {
        "year": wanted_week.year,
        "dest": reverse(
            "timetable_by_week",
            args=[type_.value, pk, wanted_week.year, wanted_week.week],
        )[::-1]
        .replace(str(wanted_week.week)[::-1], "cw"[::-1], 1)
        .replace(str(wanted_week.year)[::-1], "year"[::-1], 1)[::-1],
    }

    if is_smart:
        start = wanted_week[TimePeriod.weekday_min]
        stop = wanted_week[TimePeriod.weekday_max]
        context["announcements"] = (
            Announcement.for_timetables().relevant_for(el).within_days(start, stop)
        )

    week_prev = wanted_week - 1
    week_next = wanted_week + 1

    context["url_prev"] = reverse(
        "timetable_by_week", args=[type_.value, pk, week_prev.year, week_prev.week]
    )
    context["url_next"] = reverse(
        "timetable_by_week", args=[type_.value, pk, week_next.year, week_next.week]
    )

    if apps.is_installed("aleksis.apps.alsijil"):
        context["is_alsijil_installed"] = True

    if is_print:
        context["back_url"] = reverse(
            "timetable_by_week",
            args=[type_.value, pk, wanted_week.year, wanted_week.week],
        )
        return render_pdf(request, "chronos/timetable_print.html", context)
    else:
        return render(request, "chronos/timetable.html", context)


@permission_required("chronos.view_lessons_day_rule")
def lessons_day(
    request: HttpRequest,
    year: Optional[int] = None,
    month: Optional[int] = None,
    day: Optional[int] = None,
) -> HttpResponse:
    """View all lessons taking place on a specified day."""
    context = {}

    if day:
        wanted_day = timezone.datetime(year=year, month=month, day=day).date()
        wanted_day = TimePeriod.get_next_relevant_day(wanted_day)
    else:
        wanted_day = TimePeriod.get_next_relevant_day(timezone.now().date(), datetime.now().time())

    # Get lessons
    lesson_periods = LessonPeriod.objects.all()

    # Get filter
    lesson_periods_filter = LessonPeriodFilter(
        request.GET,
        queryset=lesson_periods.annotate(
            current_substitution=FilteredRelation(
                "substitutions",
                condition=(
                    Q(substitutions__week=wanted_day.isocalendar()[1], substitutions__year=year)
                ),
            )
        ),
        weekday=wanted_day.weekday(),
    )
    context["lesson_periods_filter"] = lesson_periods_filter

    # Build table
    lessons_table = LessonsTable(lesson_periods_filter.qs)
    RequestConfig(request).configure(lessons_table)

    context["lessons_table"] = lessons_table
    context["day"] = wanted_day
    context["lesson_periods"] = lesson_periods

    context["datepicker"] = {
        "date": date_unix(wanted_day),
        "dest": reverse("lessons_day"),
    }

    context["url_prev"], context["url_next"] = TimePeriod.get_prev_next_by_day(
        wanted_day, "lessons_day_by_date"
    )

    return render(request, "chronos/lessons_day.html", context)


@never_cache
@permission_required("chronos.edit_substitution_rule", fn=get_substitution_by_id)
def edit_substitution(request: HttpRequest, id_: int, week: int) -> HttpResponse:
    """View a form to edit a substitution lessen."""
    context = {}

    lesson_period = get_object_or_404(LessonPeriod, pk=id_)
    wanted_week = lesson_period.lesson.get_calendar_week(week)
    context["lesson_period"] = lesson_period
    day = week_weekday_to_date(wanted_week, lesson_period.period.weekday)
    context["date"] = day

    lesson_substitution = get_substitution_by_id(request, id_, week)

    if lesson_substitution:
        edit_substitution_form = LessonSubstitutionForm(
            request.POST or None, instance=lesson_substitution
        )
    else:
        edit_substitution_form = LessonSubstitutionForm(
            request.POST or None,
        )

    context["substitution"] = lesson_substitution

    if request.method == "POST":
        if edit_substitution_form.is_valid():
            with reversion.create_revision(atomic=True):
                tracker = TimetableDataChangeTracker()

                lesson_substitution = edit_substitution_form.save(commit=False)
                if not lesson_substitution.pk:
                    lesson_substitution.lesson_period = lesson_period
                    lesson_substitution.week = wanted_week.week
                    lesson_substitution.year = wanted_week.year
                lesson_substitution.save()
                edit_substitution_form.save_m2m()

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

            return redirect("lessons_day_by_date", year=day.year, month=day.month, day=day.day)

    context["edit_substitution_form"] = edit_substitution_form

    return render(request, "chronos/edit_substitution.html", context)


@permission_required("chronos.delete_substitution_rule", fn=get_substitution_by_id)
def delete_substitution(request: HttpRequest, id_: int, week: int) -> HttpResponse:
    """Delete a substitution lesson.

    Redirects back to substition list on success.
    """
    lesson_period = get_object_or_404(LessonPeriod, pk=id_)
    wanted_week = lesson_period.lesson.get_calendar_week(week)

    get_substitution_by_id(request, id_, week).delete()

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

    date = wanted_week[lesson_period.period.weekday]
    return redirect("lessons_day_by_date", year=date.year, month=date.month, day=date.day)


@permission_required("chronos.view_substitutions_rule")
def substitutions(
    request: HttpRequest,
    year: Optional[int] = None,
    month: Optional[int] = None,
    day: Optional[int] = None,
    is_print: bool = False,
) -> HttpResponse:
    """View all substitutions on a specified day."""
    context = get_substitutions_context_data(request, year, month, day, is_print)
    if not is_print:
        return render(request, "chronos/substitutions.html", context)
    else:
        return render_pdf(request, "chronos/substitutions_print.html", context)


@permission_required("chronos.view_supervisions_day_rule")
def supervisions_day(
    request: HttpRequest,
    year: Optional[int] = None,
    month: Optional[int] = None,
    day: Optional[int] = None,
) -> HttpResponse:
    """View all supervisions taking place on a specified day."""
    context = {}

    if day:
        wanted_day = timezone.datetime(year=year, month=month, day=day).date()
        wanted_day = TimePeriod.get_next_relevant_day(wanted_day)
    else:
        wanted_day = TimePeriod.get_next_relevant_day(timezone.now().date(), datetime.now().time())

    # Get supervisions
    supervisions = Supervision.objects.on_day(wanted_day).filter_by_weekday(wanted_day.weekday())

    # Get filter
    supervisions_filter = SupervisionFilter(
        request.GET,
        queryset=supervisions.annotate(
            current_substitution=FilteredRelation(
                "substitutions",
                condition=(Q(substitutions__date=wanted_day)),
            )
        ),
    )
    context["supervisions_filter"] = supervisions_filter

    # Build table
    supervisions_table = SupervisionsTable(
        supervisions_filter.qs.annotate_week(week=CalendarWeek.from_date(wanted_day))
    )
    RequestConfig(request).configure(supervisions_table)

    context["supervisions_table"] = supervisions_table
    context["day"] = wanted_day
    context["supervisions"] = supervisions

    context["datepicker"] = {
        "date": date_unix(wanted_day),
        "dest": reverse("supervisions_day"),
    }

    context["url_prev"], context["url_next"] = TimePeriod.get_prev_next_by_day(
        wanted_day, "supervisions_day_by_date"
    )

    return render(request, "chronos/supervisions_day.html", context)


@never_cache
@permission_required("chronos.edit_supervision_substitution_rule")
def edit_supervision_substitution(request: HttpRequest, id_: int, week: int) -> HttpResponse:
    """View a form to edit a supervision substitution."""
    context = {}

    supervision = get_object_or_404(Supervision, pk=id_)
    wanted_week = supervision.get_calendar_week(week)
    context["week"] = week
    context["supervision"] = supervision
    date = week_weekday_to_date(wanted_week, supervision.break_item.weekday)
    context["date"] = date

    supervision_substitution = get_supervision_substitution_by_id(request, id_, date)

    if supervision_substitution:
        edit_supervision_substitution_form = SupervisionSubstitutionForm(
            request.POST or None, instance=supervision_substitution
        )
    else:
        edit_supervision_substitution_form = SupervisionSubstitutionForm(
            request.POST or None,
        )

    context["substitution"] = supervision_substitution

    if request.method == "POST":
        if edit_supervision_substitution_form.is_valid():
            with reversion.create_revision(atomic=True):
                tracker = TimetableDataChangeTracker()

                supervision_substitution = edit_supervision_substitution_form.save(commit=False)
                if not supervision_substitution.pk:
                    supervision_substitution.supervision = supervision
                    supervision_substitution.date = date
                supervision_substitution.save()
                edit_supervision_substitution_form.save_m2m()

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

            return redirect(
                "supervisions_day_by_date", year=date.year, month=date.month, day=date.day
            )

    context["edit_supervision_substitution_form"] = edit_supervision_substitution_form

    return render(request, "chronos/edit_supervision_substitution.html", context)


@permission_required("chronos.delete_supervision_substitution_rule")
def delete_supervision_substitution(request: HttpRequest, id_: int, week: int) -> HttpResponse:
    """Delete a supervision substitution.

    Redirects back to supervision list on success.
    """
    supervision = get_object_or_404(Supervision, pk=id_)
    wanted_week = supervision.get_calendar_week(week)
    date = week_weekday_to_date(wanted_week, supervision.break_item.weekday)

    get_supervision_substitution_by_id(request, id_, date).delete()

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

    return redirect("supervisions_day_by_date", year=date.year, month=date.month, day=date.day)
