import json
from typing import List

from django.contrib import messages
from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _
from django.views.generic import ListView, DetailView

from AKModel.availability.models import Availability
from AKModel.metaviews.admin import AdminViewMixin, FilterByEventSlugMixin, EventSlugMixin, IntermediateAdminView, \
    IntermediateAdminActionView
from AKModel.models import AKRequirement, AKSlot, DefaultSlot, Event, AKOrgaMessage, AK, Room, AKOwner, merge_blocks


class AKRequirementOverview(AdminViewMixin, FilterByEventSlugMixin, ListView):
    """
    View: Display requirements for the given event
    """
    model = AKRequirement
    context_object_name = "requirements"
    title = _("Requirements for Event")
    template_name = "admin/AKModel/requirements_overview.html"

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context["event"] = self.event
        context["site_url"] = reverse_lazy("dashboard:dashboard_event", kwargs={'slug': context["event"].slug})
        return context


class AKCSVExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
    """
    View: Export all AK slots of this event in CSV format ordered by tracks
    """
    template_name = "admin/AKModel/ak_csv_export.html"
    model = AKSlot
    context_object_name = "slots"
    title = _("AK CSV Export")

    def get_queryset(self):
        return super().get_queryset().order_by("ak__track")


class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
    """
    View: Export all AK slots of this event in JSON format ordered by tracks
    """
    template_name = "admin/AKModel/ak_json_export.html"
    model = AKSlot
    context_object_name = "slots"
    title = _("AK JSON Export")


    def _test_slot_contained(self, slot: Availability, availabilities: List[Availability]) -> bool:
        """Test if slot is contained in any member of availabilities."""
        return any(availability.contains(slot) for availability in availabilities)

    def _test_event_not_covered(self, availabilities: List[Availability]) -> bool:
        """Test if event is not covered by availabilities."""
        return not Availability.is_event_covered(self.event, availabilities)

    def _test_ak_fixed_in_slot(self, ak_id, slot: Availability, ak_fixed: dict) -> bool:
        """Test if AK defined by `ak_id` is fixed to happen during slot."""
        if not ak_id in ak_fixed:
            return False

        fixed_slot = Availability(self.event, start=ak_fixed[ak_id].start, end=ak_fixed[ak_id].end)
        return fixed_slot.overlaps(slot, strict=True)

    def _test_add_constraint(self, slot: Availability, availabilities: List[Availability]) -> bool:
        """Test if object is not available for whole event and may happen during slot."""
        return (
            self._test_event_not_covered(availabilities)
            and self._test_slot_contained(slot, availabilities)
        )

    def get_queryset(self):
        return super().get_queryset().order_by("ak__track")

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context["participants"] = json.dumps([])

        rooms = Room.objects.filter(event=self.event)
        context["rooms"] = rooms

        # TODO: Configure magic number in event
        SLOTS_IN_AN_HOUR = 1

        timeslots = {
            "info": {"duration": (1.0 / SLOTS_IN_AN_HOUR), },
            "blocks": [],
            }

        for slot in context["slots"]:
            slot.slots_in_an_hour = SLOTS_IN_AN_HOUR

        ak_availabilities = {
            slot.ak.pk: Availability.union(slot.ak.availabilities.all())
            for slot in context["slots"]
        }
        room_availabilities = {
            room.pk: Availability.union(room.availabilities.all())
            for room in rooms
        }
        person_availabilities = {
            person.pk: Availability.union(person.availabilities.all())
            for person in AKOwner.objects.filter(event=self.event)
        }

        ak_fixed = {
            ak_id: values.get()
            for ak_id in ak_availabilities.keys()
            if (values := AKSlot.objects.select_related().filter(ak__pk=ak_id, fixed=True)).exists()
        }

        if DefaultSlot.objects.filter(event=self.event).exists():
            # discretize default slots if they exists
            blocks = merge_blocks(self.event.default_time_slots(slots_in_an_hour=SLOTS_IN_AN_HOUR))
        else:
            blocks = self.event.uniform_time_slots(slos_in_an_hour=SLOTS_IN_AN_HOUR)

        for block in blocks:
            current_block = []

            for timeslot in block:
                time_constraints = []
                # if reso_deadline is set and timeslot ends before it,
                #   add fulfilled time constraint 'resolution'
                if self.event.reso_deadline is None or timeslot.avail.end < self.event.reso_deadline:
                    time_constraints.append("resolution")

                # add fulfilled time constraints for all AKs that cannot happen during full event
                time_constraints.extend([
                    f"availability-ak-{ak_id}"
                    for ak_id, availabilities in ak_availabilities.items()
                    if (
                        self._test_add_constraint(timeslot.avail, availabilities)
                        or self._test_ak_fixed_in_slot(ak_id, timeslot.avail, ak_fixed)
                    )
                ])
                # add fulfilled time constraints for all persons that are not available for full event
                time_constraints.extend([
                    f"availability-person-{person_id}"
                    for person_id, availabilities in person_availabilities.items()
                    if self._test_add_constraint(timeslot.avail, availabilities)
                ])
                # add fulfilled time constraints for all rooms that are not available for full event
                time_constraints.extend([
                    f"availability-room-{room_id}"
                    for room_id, availabilities in room_availabilities.items()
                    if self._test_add_constraint(timeslot.avail, availabilities)
                ])
                time_constraints.extend(timeslot.constraints)

                current_block.append({
                    "id": str(timeslot.idx),
                    "info": {
                        "start": timeslot.avail.simplified,
                    },
                    "fulfilled_time_constraints": time_constraints,
                    })

            timeslots["blocks"].append(current_block)

        context["timeslots"] = json.dumps(timeslots)

        info_dict = {
            "title": self.event.name,
            "slug": self.event.slug
        }
        for attr in ["contact_email", "place"]:
            if hasattr(self.event, attr) and getattr(self.event, attr):
                info_dict[attr] = getattr(self.event, attr)

        context["info_dict"] = json.dumps(info_dict)

        return context



class AKWikiExportView(AdminViewMixin, DetailView):
    """
    View: Export AKs of this event in wiki syntax
    This will show one text field per category, with a separate category/field for wishes
    """
    template_name = "admin/AKModel/wiki_export.html"
    model = Event
    context_object_name = "event"
    title = _("AK Wiki Export")

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)

        categories_with_aks, ak_wishes = context["event"].get_categories_with_aks(
            wishes_seperately=True,
            filter_func=lambda ak: ak.include_in_export
        )

        context["categories_with_aks"] = [(category.name, ak_list) for category, ak_list in categories_with_aks]
        context["categories_with_aks"].append((_("Wishes"), ak_wishes))

        return context


class AKMessageDeleteView(EventSlugMixin, IntermediateAdminView):
    """
    View: Confirmation page to delete confidential AK-related messages to orga

    Confirmation functionality provided by :class:`AKModel.metaviews.admin.IntermediateAdminView`
    """
    template_name = "admin/AKModel/message_delete.html"
    title = _("Delete AK Orga Messages")

    def get_orga_messages_for_event(self, event):
        """
        Get all orga messages for the given event
        """
        return AKOrgaMessage.objects.filter(ak__event=event)

    def get_success_url(self):
        return reverse_lazy('admin:event_status', kwargs={'slug': self.event.slug})

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context["ak_messages"] = self.get_orga_messages_for_event(self.event)
        return context

    def form_valid(self, form):
        self.get_orga_messages_for_event(self.event).delete()
        messages.add_message(self.request, messages.SUCCESS, _("AK Orga Messages successfully deleted"))
        return super().form_valid(form)


class AKResetInterestView(IntermediateAdminActionView):
    """
    View: Confirmation page to reset all manually specified interest values

    Confirmation functionality provided by :class:`AKModel.metaviews.admin.IntermediateAdminView`
    """
    title = _("Reset interest in AKs")
    model = AK
    confirmation_message = _("Interest of the following AKs will be set to not filled (-1):")
    success_message = _("Reset of interest in AKs successful.")

    def action(self, form):
        self.entities.update(interest=-1)


class AKResetInterestCounterView(IntermediateAdminActionView):
    """
    View: Confirmation page to reset all interest counters (online interest indication)

    Confirmation functionality provided by :class:`AKModel.metaviews.admin.IntermediateAdminView`
    """
    title = _("Reset AKs' interest counters")
    model = AK
    confirmation_message = _("Interest counter of the following AKs will be set to 0:")
    success_message = _("AKs' interest counters set back to 0.")

    def action(self, form):
        self.entities.update(interest_counter=0)