from collections import OrderedDict
from datetime import date, datetime, time
from typing import Union

from django.apps import apps

from calendarweek import CalendarWeek

from aleksis.apps.chronos.managers import TimetableType
from aleksis.apps.chronos.models import SupervisionEvent
from aleksis.core.models import Group, Person, Room

LessonPeriod = apps.get_model("chronos", "LessonPeriod")
LessonEvent = apps.get_model("chronos", "LessonEvent")
TimePeriod = apps.get_model("chronos", "TimePeriod")
Break = apps.get_model("chronos", "Break")
Supervision = apps.get_model("chronos", "Supervision")
LessonSubstitution = apps.get_model("chronos", "LessonSubstitution")
SupervisionSubstitution = apps.get_model("chronos", "SupervisionSubstitution")
Event = apps.get_model("chronos", "Event")
Holiday = apps.get_model("chronos", "Holiday")
ExtraLesson = apps.get_model("chronos", "ExtraLesson")


def build_timetable(
    type_: Union[TimetableType, str],
    obj: Union[Group, Room, Person],
    date_ref: Union[CalendarWeek, date],
    with_holidays: bool = True,
):
    needed_breaks = []

    is_person = False
    if type_ == "person":
        is_person = True
        type_ = obj.timetable_type

    is_week = False
    if isinstance(date_ref, CalendarWeek):
        is_week = True

    if type_ is None:
        return None

    # Get matching holidays
    if is_week:
        holidays_per_weekday = Holiday.in_week(date_ref) if with_holidays else {}
    else:
        holiday = Holiday.on_day(date_ref) if with_holidays else None

    # Get matching lesson periods
    lesson_periods = LessonPeriod.objects
    lesson_periods = (
        lesson_periods.select_related(None)
        .select_related("lesson", "lesson__subject", "period", "room")
        .only(
            "lesson",
            "period",
            "room",
            "lesson__subject",
            "period__weekday",
            "period__period",
            "lesson__subject__short_name",
            "lesson__subject__name",
            "lesson__subject__colour_fg",
            "lesson__subject__colour_bg",
            "room__short_name",
            "room__name",
        )
    )

    if is_week:
        lesson_periods = lesson_periods.in_week(date_ref)
    else:
        lesson_periods = lesson_periods.on_day(date_ref)

    if is_person:
        lesson_periods = lesson_periods.filter_from_person(obj)
    else:
        lesson_periods = lesson_periods.filter_from_type(type_, obj, is_smart=with_holidays)

    # Sort lesson periods in a dict
    lesson_periods_per_period = lesson_periods.group_by_periods(is_week=is_week)

    # Get events
    extra_lessons = ExtraLesson.objects
    if is_week:
        extra_lessons = extra_lessons.filter(week=date_ref.week, year=date_ref.year)
    else:
        extra_lessons = extra_lessons.on_day(date_ref)
    if is_person:
        extra_lessons = extra_lessons.filter_from_person(obj)
    else:
        extra_lessons = extra_lessons.filter_from_type(type_, obj)

    extra_lessons = extra_lessons.only(
        "week",
        "year",
        "period",
        "subject",
        "room",
        "comment",
        "period__weekday",
        "period__period",
        "subject__short_name",
        "subject__name",
        "subject__colour_fg",
        "subject__colour_bg",
        "room__short_name",
        "room__name",
    )

    # Sort lesson periods in a dict
    extra_lessons_per_period = extra_lessons.group_by_periods(is_week=is_week)

    # Get events
    events = Event.objects
    events = events.in_week(date_ref) if is_week else events.on_day(date_ref)

    events = events.only(
        "id",
        "title",
        "date_start",
        "date_end",
        "period_from",
        "period_to",
        "period_from__weekday",
        "period_from__period",
        "period_to__weekday",
        "period_to__period",
    )

    if is_person:
        events_to_display = events.filter_from_person(obj)
    else:
        events_to_display = events.filter_from_type(type_, obj)

    # Sort events in a dict
    events_per_period = {}
    events_for_replacement_per_period = {}
    for event in events:
        if is_week and event.date_start < date_ref[TimePeriod.weekday_min]:
            # If start date not in current week, set weekday and period to min
            weekday_from = TimePeriod.weekday_min
            period_from_first_weekday = TimePeriod.period_min
        else:
            weekday_from = event.date_start.weekday()
            period_from_first_weekday = event.period_from.period

        if is_week and event.date_end > date_ref[TimePeriod.weekday_max]:
            # If end date not in current week, set weekday and period to max
            weekday_to = TimePeriod.weekday_max
            period_to_last_weekday = TimePeriod.period_max
        else:
            weekday_to = event.date_end.weekday()
            period_to_last_weekday = event.period_to.period

        for weekday in range(weekday_from, weekday_to + 1):
            if not is_week and weekday != date_ref.weekday():
                # If daily timetable for person, skip other weekdays
                continue

            # If start day, use start period else use min period
            period_from = (
                period_from_first_weekday if weekday == weekday_from else TimePeriod.period_min
            )

            # If end day, use end period else use max period
            period_to = period_to_last_weekday if weekday == weekday_to else TimePeriod.periox_max

            for period in range(period_from, period_to + 1):
                # The following events are possibly replacing some lesson periods
                if period not in events_for_replacement_per_period:
                    events_for_replacement_per_period[period] = {} if is_week else []

                if is_week and weekday not in events_for_replacement_per_period[period]:
                    events_for_replacement_per_period[period][weekday] = []

                if not is_week:
                    events_for_replacement_per_period[period].append(event)
                else:
                    events_for_replacement_per_period[period][weekday].append(event)

                # and the following will be displayed in the timetable
                if event in events_to_display:
                    if period not in events_per_period:
                        events_per_period[period] = {} if is_week else []

                    if is_week and weekday not in events_per_period[period]:
                        events_per_period[period][weekday] = []

                    if not is_week:
                        events_per_period[period].append(event)
                    else:
                        events_per_period[period][weekday].append(event)

    if type_ == TimetableType.TEACHER:
        # Get matching supervisions
        week = CalendarWeek.from_date(date_ref) if not is_week else date_ref
        supervisions = (
            Supervision.objects.in_week(week)
            .all()
            .annotate_week(week)
            .filter_by_teacher(obj)
            .only(
                "area",
                "break_item",
                "teacher",
                "area",
                "area__short_name",
                "area__name",
                "area__colour_fg",
                "area__colour_bg",
                "break_item__short_name",
                "break_item__name",
                "break_item__after_period__period",
                "break_item__after_period__weekday",
                "break_item__before_period__period",
                "break_item__before_period__weekday",
                "teacher__short_name",
                "teacher__first_name",
                "teacher__last_name",
            )
        )

        if not is_week:
            supervisions = supervisions.filter_by_weekday(date_ref.weekday())

        supervisions_per_period_after = {}
        for supervision in supervisions:
            weekday = supervision.break_item.weekday
            period_after_break = supervision.break_item.before_period_number

            if period_after_break not in needed_breaks:
                needed_breaks.append(period_after_break)

            if is_week and period_after_break not in supervisions_per_period_after:
                supervisions_per_period_after[period_after_break] = {}

            if not is_week:
                supervisions_per_period_after[period_after_break] = supervision
            else:
                supervisions_per_period_after[period_after_break][weekday] = supervision

    # Get ordered breaks
    breaks = OrderedDict(sorted(Break.get_breaks_dict().items()))

    rows = []
    for period, break_ in breaks.items():  # period is period after break
        # Break
        if type_ == TimetableType.TEACHER and period in needed_breaks:
            row = {
                "type": "break",
                "after_period": break_.after_period_number,
                "before_period": break_.before_period_number,
                "time_start": break_.time_start,
                "time_end": break_.time_end,
            }

            if is_week:
                cols = []

                for weekday in range(TimePeriod.weekday_min, TimePeriod.weekday_max + 1):
                    col = None
                    if (
                        period in supervisions_per_period_after
                        and weekday not in holidays_per_weekday
                    ) and weekday in supervisions_per_period_after[period]:
                        col = supervisions_per_period_after[period][weekday]
                    cols.append(col)

                row["cols"] = cols
            else:
                col = None
                if period in supervisions_per_period_after and not holiday:
                    col = supervisions_per_period_after[period]
                row["col"] = col
            rows.append(row)

        # Period
        if period <= TimePeriod.period_max:
            row = {
                "type": "period",
                "period": period,
                "time_start": break_.before_period.time_start,
                "time_end": break_.before_period.time_end,
            }

            if is_week:
                cols = []
                for weekday in range(TimePeriod.weekday_min, TimePeriod.weekday_max + 1):
                    # Skip this period if there are holidays
                    if weekday in holidays_per_weekday:
                        cols.append([])
                        continue

                    col = []

                    events_for_this_period = (
                        events_per_period[period].get(weekday, [])
                        if period in events_per_period
                        else []
                    )
                    events_for_replacement_for_this_period = (
                        events_for_replacement_per_period[period].get(weekday, [])
                        if period in events_for_replacement_per_period
                        else []
                    )
                    lesson_periods_for_this_period = (
                        lesson_periods_per_period[period].get(weekday, [])
                        if period in lesson_periods_per_period
                        else []
                    )

                    # Add lesson periods
                    if lesson_periods_for_this_period:
                        if events_for_replacement_for_this_period:
                            # If there is a event in this period,
                            # we have to check whether the actual lesson is taking place.

                            for lesson_period in lesson_periods_for_this_period:
                                replaced_by_event = lesson_period.is_replaced_by_event(
                                    events_for_replacement_for_this_period,
                                    [obj] if type_ == TimetableType.GROUP else None,
                                )
                                lesson_period.replaced_by_event = replaced_by_event
                                if not replaced_by_event or (
                                    replaced_by_event and type_ != TimetableType.GROUP
                                ):
                                    col.append(lesson_period)

                        else:
                            col += lesson_periods_for_this_period

                    # Add extra lessons
                    if period in extra_lessons_per_period:
                        col += extra_lessons_per_period[period].get(weekday, [])

                    # Add events
                    col += events_for_this_period

                    cols.append(col)

                row["cols"] = cols
            else:
                col = []

                # Skip this period if there are holidays
                if holiday:
                    continue

                events_for_this_period = events_per_period.get(period, [])
                events_for_replacement_for_this_period = events_for_replacement_per_period.get(
                    period, []
                )
                lesson_periods_for_this_period = lesson_periods_per_period.get(period, [])

                # Add lesson periods
                if lesson_periods_for_this_period:
                    if events_for_replacement_for_this_period:
                        # If there is a event in this period,
                        # we have to check whether the actual lesson is taking place.

                        lesson_periods_to_keep = []
                        for lesson_period in lesson_periods_for_this_period:
                            if not lesson_period.is_replaced_by_event(
                                events_for_replacement_for_this_period
                            ):
                                lesson_periods_to_keep.append(lesson_period)
                        col += lesson_periods_to_keep
                    else:
                        col += lesson_periods_for_this_period

                # Add events and extra lessons
                col += extra_lessons_per_period.get(period, [])
                col += events_for_this_period

                row["col"] = col

            rows.append(row)

    return rows


def build_substitutions_list(wanted_day: date) -> tuple[list[dict], set[Person], set[Group]]:
    rows = []
    affected_teachers = set()
    affected_groups = set()

    lesson_events = LessonEvent.get_single_events(
        datetime.combine(wanted_day, time.min),
        datetime.combine(wanted_day, time.max),
        params={"amending": True},
        with_reference_object=True,
    )

    for lesson_event in lesson_events:
        affected_teachers.update(lesson_event["REFERENCE_OBJECT"].teachers.all())
        affected_teachers.update(lesson_event["REFERENCE_OBJECT"].amends.teachers.all())
        affected_groups.update(lesson_event["REFERENCE_OBJECT"].groups.all())
        affected_groups.update(lesson_event["REFERENCE_OBJECT"].amends.groups.all())

        row = {
            "type": "substitution",
            "sort_a": lesson_event["REFERENCE_OBJECT"].group_names,
            "sort_b": str(lesson_event["DTSTART"]),
            "el": lesson_event,
        }

        rows.append(row)

    supervision_events = SupervisionEvent.get_single_events(
        datetime.combine(wanted_day, time.min),
        datetime.combine(wanted_day, time.max),
        params={"amending": True},
        with_reference_object=True,
    )
    print(supervision_events)

    for supervision_event in supervision_events:
        affected_teachers.update(supervision_event["REFERENCE_OBJECT"].teachers.all())
        affected_teachers.update(supervision_event["REFERENCE_OBJECT"].amends.teachers.all())

        row = {
            "type": "supervision_substitution",
            "sort_a": "Z",
            "sort_b": str(supervision_event["DTSTART"]),
            "el": supervision_event,
        }

        rows.append(row)

    rows.sort(key=lambda row: row["sort_a"] + row["sort_b"])

    return rows, affected_teachers, affected_groups


def build_weekdays(
    base: list[tuple[int, str]], wanted_week: CalendarWeek, with_holidays: bool = True
) -> list[dict]:
    if with_holidays:
        holidays_per_weekday = Holiday.in_week(wanted_week)

    weekdays = []
    for key, name in base[TimePeriod.weekday_min : TimePeriod.weekday_max + 1]:
        weekday = {
            "key": key,
            "name": name,
            "date": wanted_week[key],
        }
        if with_holidays:
            weekday["holiday"] = holidays_per_weekday[key] if key in holidays_per_weekday else None
        weekdays.append(weekday)

    return weekdays