from django.apps import apps
from rest_framework import serializers

from AKModel.models import AK, AKSlot, Event, Room
from AKModel.serializers import IntListField, StringListField

class ExportRoomInfoSerializer(serializers.ModelSerializer):
    """Serializer of Room objects for the 'info' field.

    Used in `ExportRoomSerializer` to serialize Room objects
    for the export to a solver. Part of the implementation of the
    format of the KoMa solver:
    https://github.com/Die-KoMa/ak-plan-optimierung/wiki/Input-&-output-format#input--output-format
    """

    class Meta:
        model = Room
        fields = ["name"]
        read_only_fields = ["name"]


class ExportRoomSerializer(serializers.ModelSerializer):
    """Export serializer for Room objects.

    Used to serialize Room objects for the export to a solver.
    Part of the implementation of the format of the KoMa solver:
    https://github.com/Die-KoMa/ak-plan-optimierung/wiki/Input-&-output-format#input--output-format
    """

    time_constraints = StringListField(source="get_time_constraints", read_only=True)
    fulfilled_room_constraints = StringListField(
        source="get_fulfilled_room_constraints", read_only=True
    )
    info = ExportRoomInfoSerializer(source="*")

    class Meta:
        model = Room
        fields = [
            "id",
            "capacity",
            "time_constraints",
            "fulfilled_room_constraints",
            "info",
        ]
        read_only_fields = [
            "id",
            "capacity",
            "time_constraints",
            "fulfilled_room_constraints",
            "info",
        ]


class ExportAKSlotInfoSerializer(serializers.ModelSerializer):
    """Serializer of AKSlot objects for the 'info' field.

    Used in `ExportAKSlotSerializer` to serialize AKSlot objects
    for the export to a solver. Part of the implementation of the
    format of the KoMa solver:
    https://github.com/Die-KoMa/ak-plan-optimierung/wiki/Input-&-output-format#input--output-format
    """

    name = serializers.CharField(source="ak.name")
    head = serializers.SerializerMethodField()
    reso = serializers.BooleanField(source="ak.reso")
    description = serializers.CharField(source="ak.description")
    duration_in_hours = serializers.FloatField(source="duration")
    django_ak_id = serializers.IntegerField(source="ak.pk")
    types = StringListField(source="type_names")

    def get_head(self, slot: AKSlot) -> str:
        """Get string representation for 'head' field."""
        return ", ".join([str(owner) for owner in slot.ak.owners.all()])

    class Meta:
        model = AKSlot
        fields = [
            "name",
            "head",
            "description",
            "reso",
            "duration_in_hours",
            "django_ak_id",
            "types",
        ]
        read_only_fields = [
            "name",
            "head",
            "description",
            "reso",
            "duration_in_hours",
            "django_ak_id",
            "types",
        ]


class ExportAKSlotPropertiesSerializer(serializers.ModelSerializer):
    """Serializer of AKSlot objects for the 'properties' field.

    Used in `ExportAKSlotSerializer` to serialize AKSlot objects
    for the export to a solver. Part of the implementation of the
    format of the KoMa solver:
    https://github.com/Die-KoMa/ak-plan-optimierung/wiki/Input-&-output-format#input--output-format
    """

    conflicts = IntListField(source="conflict_pks")
    dependencies = IntListField(source="depencency_pks")

    class Meta:
        model = AKSlot
        fields = ["conflicts", "dependencies"]
        read_only_fields = ["conflicts", "dependencies"]


class ExportAKSlotSerializer(serializers.ModelSerializer):
    """Export serializer for AKSlot objects.

    Used to serialize AKSlot objects for the export to a solver.
    Part of the implementation of the format of the KoMa solver:
    https://github.com/Die-KoMa/ak-plan-optimierung/wiki/Input-&-output-format#input--output-format
    """

    duration = serializers.IntegerField(source="export_duration")
    room_constraints = StringListField(source="get_room_constraints")
    time_constraints = StringListField(source="get_time_constraints")
    info = ExportAKSlotInfoSerializer(source="*")
    properties = ExportAKSlotPropertiesSerializer(source="*")

    class Meta:
        model = AKSlot
        fields = [
            "id",
            "duration",
            "properties",
            "room_constraints",
            "time_constraints",
            "info",
        ]
        read_only_fields = [
            "id",
            "duration",
            "properties",
            "room_constraints",
            "time_constraints",
            "info",
        ]


class ExportParticipantAndDummiesSerializer(serializers.BaseSerializer):
    """Export serializer for EventParticipant objects that includes 'dummy' participants.

    This serializer is a work-around to make the solver compatible with the AKOwner model.

    Internally, `ExportParticipantSerializer` is used to serialize all EventParticipants of
    the event to serialize. To avoid scheduling conflicts, a 'dummy' participant is then added
    to the list for each AKOwner of the event. These dummy participants only have 'required'
    preference for all AKs of the owner, so the target of the optimization is not impacted.

    Part of the implementation of the format of the KoMa solver:
    https://github.com/Die-KoMa/ak-plan-optimierung/wiki/Input-&-output-format#input--output-format
    """

    def create(self, validated_data):
        raise ValueError("`ExportParticipantAndDummiesSerializer` is read-only.")

    def to_internal_value(self, data):
        raise ValueError("`ExportParticipantAndDummiesSerializer` is read-only.")

    def update(self, instance, validated_data):
        raise ValueError("`ExportParticipantAndDummiesSerializer` is read-only.")

    def to_representation(self, instance: Event):
        event = instance

        # default case
        real_participants = []
        next_participant_pk = 1

        # set variable values if AKPreference app is installed
        if apps.is_installed("AKPreference"):
            # local import to decouple
            # pylint: disable=import-outside-toplevel
            from AKPreference.models import EventParticipant
            from AKPreference.serializers import ExportParticipantSerializer

            real_participants = ExportParticipantSerializer(event.participants, many=True).data
            if EventParticipant.objects.exists():
                next_participant_pk = EventParticipant.objects.latest("pk").pk + 1

        dummies = []
        # add one dummy participant per owner
        # this ensures that the hard constraints from each owner are considered
        for new_pk, owner in enumerate(event.owners, next_participant_pk):
            owned_slots = event.slots.filter(ak__owners=owner).order_by().all()
            if not owned_slots:
                continue
            new_participant_data = {
                "id": new_pk,
                "info": {"name": f"{owner} [AKOwner]"},
                "room_constraints": [],
                "time_constraints": [],
                "preferences": [
                    {"ak_id": slot.pk, "required": True, "preference_score": -1}
                    for slot in owned_slots
                ]
            }
            dummies.append(new_participant_data)
        return real_participants + dummies


class ExportEventInfoSerializer(serializers.ModelSerializer):
    """Serializer of an Event object for the 'info' field.

    Used in `ExportEventSerializer` to serialize an Event object
    for the export to a solver. Part of the implementation of the
    format of the KoMa solver:
    https://github.com/Die-KoMa/ak-plan-optimierung/wiki/Input-&-output-format#input--output-format
    """

    title = serializers.CharField(source="name")
    contact_email = serializers.EmailField(required=False)
    place = serializers.CharField(required=False)

    class Meta:
        model = Event
        fields = ["title", "slug", "contact_email", "place"]


class ExportTimeslotBlockSerializer(serializers.BaseSerializer):
    """Read-only serializer for timeslots.

    Used to serialize timeslots for the export to a solver.
    Part of the implementation of the format of the KoMa solver:
    https://github.com/Die-KoMa/ak-plan-optimierung/wiki/Input-&-output-format#input--output-format
    """

    def create(self, validated_data):
        raise ValueError("`ExportTimeslotBlockSerializer` is read-only.")

    def to_internal_value(self, data):
        raise ValueError("`ExportTimeslotBlockSerializer` is read-only.")

    def update(self, instance, validated_data):
        raise ValueError("`ExportTimeslotBlockSerializer` is read-only.")

    def to_representation(self, instance: Event):
        """Construct serialized representation of the timeslots of an event."""
        # pylint: disable=import-outside-toplevel
        from AKModel.availability.models import Availability

        event = instance
        blocks = list(event.discretize_timeslots())

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

        def _check_akslot_fixed_in_timeslot(
            ak_slot: AKSlot, timeslot: Availability
        ) -> bool:
            """Test if an AKSlot is fixed to overlap a timeslot slot."""
            if not ak_slot.fixed or ak_slot.start is None:
                return False

            fixed_avail = Availability(
                event=event, start=ak_slot.start, end=ak_slot.end
            )
            return fixed_avail.overlaps(timeslot, strict=True)

        def _check_add_constraint(
            slot: Availability, availabilities: list[Availability]
        ) -> bool:
            """Test if object is not available for whole event and may happen during slot."""
            return _check_event_not_covered(availabilities) and slot.is_covered(
                availabilities
            )

        def _generate_time_constraints(
            avail_label: str,
            avail_dict: dict,
            timeslot_avail: Availability,
            prefix: str = "availability",
        ) -> list[str]:
            return [
                f"{prefix}-{avail_label}-{pk}"
                for pk, availabilities in avail_dict.items()
                if _check_add_constraint(timeslot_avail, availabilities)
            ]

        timeslots = {
            "info": {"duration": float(event.export_slot)},
            "blocks": [],
        }

        ak_availabilities = {
            ak.pk: Availability.union(ak.availabilities.all())
            for ak in AK.objects.filter(event=event).all()
        }
        room_availabilities = {
            room.pk: Availability.union(room.availabilities.all()) for room in event.rooms
        }
        person_availabilities = {
            person.pk: Availability.union(person.availabilities.all())
            for person in event.owners
        }
        participant_availabilities = {
            participant.pk: Availability.union(participant.availabilities.all())
            for participant in event.participants
        }

        block_names = []
        for block_idx, block in enumerate(blocks):
            current_block = []

            if not block:
                continue

            block_start = block[0].avail.start.astimezone(event.timezone)
            block_end = block[-1].avail.end.astimezone(event.timezone)

            start_day = block_start.strftime("%A, %d. %b")
            if block_start.date() == block_end.date():
                # same day
                time_str = (
                    block_start.strftime("%H:%M") + " - " + block_end.strftime("%H:%M")
                )
            else:
                # different days
                time_str = (
                    block_start.strftime("%a %H:%M")
                    + " - "
                    + block_end.strftime("%a %H:%M")
                )
            block_names.append([start_day, time_str])

            block_timeconstraints = [
                f"notblock{idx}" for idx in range(len(blocks)) if idx != block_idx
            ]

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

                # add fulfilled time constraints for all AKs that cannot happen during full event
                time_constraints.extend(
                    _generate_time_constraints("ak", ak_availabilities, timeslot.avail)
                )

                # add fulfilled time constraints for all persons that are not available for full event
                time_constraints.extend(
                    _generate_time_constraints(
                        "person", person_availabilities, timeslot.avail
                    )
                )

                # add fulfilled time constraints for all rooms that are not available for full event
                time_constraints.extend(
                    _generate_time_constraints(
                        "room", room_availabilities, timeslot.avail
                    )
                )

                # add fulfilled time constraints for all participants that are not available for full event
                time_constraints.extend(
                    _generate_time_constraints(
                        "participant", participant_availabilities, timeslot.avail
                    )
                )

                # add fulfilled time constraints for all AKSlots fixed to happen during timeslot
                time_constraints.extend(
                    [
                        f"fixed-akslot-{slot.id}"
                        for slot in AKSlot.objects.filter(
                            event=event, fixed=True
                        ).exclude(start__isnull=True)
                        if _check_akslot_fixed_in_timeslot(slot, timeslot.avail)
                    ]
                )

                time_constraints.extend(timeslot.constraints)
                time_constraints.extend(block_timeconstraints)
                time_constraints.sort()

                current_block.append(
                    {
                        "id": timeslot.idx,
                        "info": {
                            "start": timeslot.avail.start.astimezone(
                                event.timezone
                            ).strftime("%Y-%m-%d %H:%M"),
                            "end": timeslot.avail.end.astimezone(
                                event.timezone
                            ).strftime("%Y-%m-%d %H:%M"),
                        },
                        "fulfilled_time_constraints": time_constraints,
                    }
                )

            timeslots["blocks"].append(current_block)

        timeslots["info"]["blocknames"] = block_names

        return timeslots


class ExportEventSerializer(serializers.ModelSerializer):
    """Export serializer for an Event object.

    Used to serialize an Event for the export to a solver.
    Part of the implementation of the format of the KoMa solver:
    https://github.com/Die-KoMa/ak-plan-optimierung/wiki/Input-&-output-format#input--output-format
    """

    info = ExportEventInfoSerializer(source="*")
    rooms = ExportRoomSerializer(many=True)
    aks = ExportAKSlotSerializer(source="slots", many=True)
    participants = ExportParticipantAndDummiesSerializer(source="*")
    timeslots = ExportTimeslotBlockSerializer(source="*")

    class Meta:
        model = Event
        fields = ["participants", "rooms", "timeslots", "info", "aks"]
        read_only_fields = ["participants", "rooms", "timeslots", "info", "aks"]