#
# steering, Programme/schedule management for AURA
#
# Copyright (C) 2011-2017, 2020, Ernesto Rico Schmidt
# Copyright (C) 2017-2019, Ingo Leindecker
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#

from typing import List

from rest_framework import serializers

from django.contrib.auth.models import User
from django.core.exceptions import ObjectDoesNotExist
from django.utils import timezone
from program.models import (
    Category,
    FundingCategory,
    Host,
    HostLink,
    Image,
    Language,
    LicenseType,
    LinkType,
    MusicFocus,
    Note,
    NoteLink,
    RRule,
    Schedule,
    Show,
    ShowLink,
    TimeSlot,
    Topic,
    Type,
    UserProfile,
)
from program.utils import delete_links, get_audio_url
from steering.settings import THUMBNAIL_SIZES

SOLUTION_CHOICES = {
    "theirs": "Discard projected timeslot. Keep existing timeslot(s).",
    "ours": "Create projected timeslot. Delete existing timeslot(s).",
    "theirs-start": (
        "Keep existing timeslot. Create projected timeslot with start time of existing end."
    ),
    "ours-start": (
        "Create projected timeslot. Change end of existing timeslot to projected start time."
    ),
    "theirs-end": (
        "Keep existing timeslot. Create projected timeslot with end of existing start time."
    ),
    "ours-end": (
        "Create projected timeslot. Change start of existing timeslot to projected end time."
    ),
    "theirs-both": (
        "Keep existing timeslot. "
        "Create two projected timeslots with end of existing start and start of existing end."
    ),
    "ours-both": (
        "Create projected timeslot. Split existing timeslot into two: \n\n"
        "* set existing end time to projected start,\n"
        "* create another timeslot with start = projected end and end = existing end."
    ),
}


class ErrorSerializer(serializers.Serializer):
    message = serializers.CharField()
    code = serializers.CharField(allow_null=True)


class ProfileSerializer(serializers.ModelSerializer):
    class Meta:
        model = UserProfile
        fields = (
            "user",
            "cba_username",
            "cba_user_token",
            "created_at",
            "created_by",
            "updated_at",
            "updated_by",
        )
        read_only_fields = (
            "created_at",
            "created_by",
            "updated_at",
            "updated_by",
        )


class UserSerializer(serializers.ModelSerializer):
    # Add profile fields to JSON
    profile = ProfileSerializer(required=False)

    class Meta:
        model = User
        fields = (
            "email",
            "first_name",
            "id",
            "is_active",
            "is_staff",
            "is_superuser",
            "last_name",
            "password",
            "profile",
            "username",
        )

    def create(self, validated_data):
        """
        Create and return a new User instance, given the validated data.
        """

        profile_data = validated_data.pop("profile") if "profile" in validated_data else None

        user = super(UserSerializer, self).create(validated_data)
        user.date_joined = timezone.now()
        user.set_password(validated_data["password"])
        user.save()

        if profile_data:
            profile = UserProfile(
                user=user,
                cba_username=profile_data.get("cba_username").strip(),
                cba_user_token=profile_data.get("cba_user_token").strip(),
                created_by=self.context["user"],
            )
            profile.save()

        return user

    def update(self, instance, validated_data):
        """
        Update and return an existing User instance, given the validated data.
        """

        instance.first_name = validated_data.get("first_name", instance.first_name)
        instance.last_name = validated_data.get("last_name", instance.last_name)
        instance.email = validated_data.get("email", instance.email)
        instance.is_active = validated_data.get("is_active", instance.is_active)
        instance.is_staff = validated_data.get("is_staff", instance.is_staff)
        instance.is_superuser = validated_data.get("is_superuser", instance.is_superuser)

        profile_data = validated_data.pop("profile") if "profile" in validated_data else None

        if profile_data:
            # TODO: How to hook into this from ProfileSerializer without having to call it here?
            try:
                profile = UserProfile.objects.get(user=instance.id)
            except ObjectDoesNotExist:
                profile = UserProfile.objects.create(user=instance, **profile_data)

            profile.cba_username = profile_data.get("cba_username")
            profile.cba_user_token = profile_data.get("cba_user_token")
            profile.updated_by = self.context["user"]
            profile.save()

        instance.save()
        return instance


class CategorySerializer(serializers.ModelSerializer):
    class Meta:
        model = Category
        fields = ("description", "id", "is_active", "name", "slug", "subtitle")


class LinkTypeSerializer(serializers.ModelSerializer):
    class Meta:
        model = LinkType
        fields = ("name", "type")


class LicenseTypeSerializer(serializers.ModelSerializer):
    class Meta:
        model = LicenseType
        fields = ("name", "type")


class HostLinkSerializer(serializers.ModelSerializer):
    class Meta:
        model = HostLink
        fields = ("type", "url")


class ImageSerializer(serializers.ModelSerializer):
    thumbnails = serializers.SerializerMethodField()

    @staticmethod
    def get_thumbnails(instance) -> List[str]:
        """Returns thumbnails"""
        thumbnails = []

        if instance.image.name and THUMBNAIL_SIZES:
            for size in THUMBNAIL_SIZES:
                thumbnails.append(instance.image.crop[size].name)

        return thumbnails

    class Meta:
        model = Image
        read_only_fields = (
            "height",
            "id",
            "ppoi",
            "thumbnails",
            "width",
        )
        fields = (
            "alt_text",
            "credits",
            "image",
        ) + read_only_fields

    def create(self, validated_data):
        """Create and return a new Image instance, given the validated data."""

        image = Image.objects.create(**validated_data | self.context)
        image.save()

        return image

    def update(self, instance, validated_data):
        """Update and return an existing Image instance, given the validated data."""

        # Only alt_text and credits can be updated.
        instance.alt_text = validated_data.get("alt_text", instance.alt_text)
        instance.credits = validated_data.get("credits", instance.credits)

        instance.save()

        return instance


class HostSerializer(serializers.ModelSerializer):
    image = serializers.PrimaryKeyRelatedField(queryset=Image.objects.all(), required=False)
    links = HostLinkSerializer(many=True, required=False)

    class Meta:
        model = Host
        read_only_fields = (
            "created_at",
            "created_by",
            "updated_at",
            "updated_by",
        )
        fields = (
            "biography",
            "email",
            "id",
            "image",
            "is_active",
            "links",
            "name",
        ) + read_only_fields

    def create(self, validated_data):
        """
        Create and return a new Host instance, given the validated data.
        """

        links_data = validated_data.pop("links", [])

        host = Host.objects.create(**validated_data | self.context)  # created_by

        for link_data in links_data:
            HostLink.objects.create(host=host, **link_data)

        host.save()

        return host

    def update(self, instance, validated_data):
        """
        Update and return an existing Host instance, given the validated data.
        """

        instance.biography = validated_data.get("biography", instance.biography)
        instance.email = validated_data.get("email", instance.email)
        instance.image = validated_data.get("image", instance.image)
        instance.is_active = validated_data.get("is_active", instance.is_active)
        instance.name = validated_data.get("name", instance.name)

        if links_data := validated_data.get("links"):
            instance = delete_links(instance)

            for link_data in links_data:
                HostLink.objects.create(host=instance, **link_data)

        instance.updated_by = self.context.get("updated_by")

        instance.save()

        return instance


class LanguageSerializer(serializers.ModelSerializer):
    class Meta:
        model = Language
        fields = ("id", "is_active", "name")


class TopicSerializer(serializers.ModelSerializer):
    class Meta:
        model = Topic
        fields = ("id", "is_active", "name", "slug")


class MusicFocusSerializer(serializers.ModelSerializer):
    class Meta:
        model = MusicFocus
        fields = ("id", "is_active", "name", "slug")


class TypeSerializer(serializers.ModelSerializer):
    class Meta:
        model = Type
        fields = ("id", "is_active", "name", "slug")


class FundingCategorySerializer(serializers.ModelSerializer):
    class Meta:
        model = FundingCategory
        fields = ("id", "is_active", "name", "slug")


class ShowLinkSerializer(serializers.ModelSerializer):
    class Meta:
        model = ShowLink
        fields = ("type", "url")


class ShowSerializer(serializers.HyperlinkedModelSerializer):
    category = serializers.PrimaryKeyRelatedField(queryset=Category.objects.all(), many=True)
    funding_category = serializers.PrimaryKeyRelatedField(queryset=FundingCategory.objects.all())
    hosts = serializers.PrimaryKeyRelatedField(queryset=Host.objects.all(), many=True)
    image = serializers.PrimaryKeyRelatedField(queryset=Image.objects.all(), required=False)
    language = serializers.PrimaryKeyRelatedField(queryset=Language.objects.all(), many=True)
    links = HostLinkSerializer(many=True, required=False)
    music_focus = serializers.PrimaryKeyRelatedField(queryset=MusicFocus.objects.all(), many=True)
    owners = serializers.PrimaryKeyRelatedField(queryset=User.objects.all(), many=True)
    predecessor = serializers.PrimaryKeyRelatedField(
        queryset=Show.objects.all(), required=False, allow_null=True
    )
    topic = serializers.PrimaryKeyRelatedField(queryset=Topic.objects.all(), many=True)
    type = serializers.PrimaryKeyRelatedField(queryset=Type.objects.all())

    class Meta:
        model = Show
        read_only_fields = (
            "created_at",
            "created_by",
            "updated_at",
            "updated_by",
        )
        fields = (
            "category",
            "cba_series_id",
            "default_playlist_id",
            "description",
            "email",
            "funding_category",
            "hosts",
            "id",
            "image",
            "internal_note",
            "is_active",
            "is_public",
            "language",
            "links",
            "logo",
            "music_focus",
            "name",
            "owners",
            "predecessor",
            "short_description",
            "slug",
            "topic",
            "type",
        ) + read_only_fields

    def create(self, validated_data):
        """
        Create and return a new Show instance, given the validated data.
        """

        owners = validated_data.pop("owners")
        category = validated_data.pop("category")
        hosts = validated_data.pop("hosts")
        language = validated_data.pop("language")
        topic = validated_data.pop("topic")
        music_focus = validated_data.pop("music_focus")
        links_data = validated_data.pop("links", [])

        show = Show.objects.create(**validated_data | self.context)  # created_by

        # Save many-to-many relationships
        show.owners.set(owners)
        show.category.set(category)
        show.hosts.set(hosts)
        show.language.set(language)
        show.topic.set(topic)
        show.music_focus.set(music_focus)

        for link_data in links_data:
            ShowLink.objects.create(show=show, **link_data)

        show.save()

        return show

    def update(self, instance, validated_data):
        """
        Update and return an existing Show instance, given the validated data.
        """

        instance.cba_series_id = validated_data.get("cba_series_id", instance.cba_series_id)
        instance.default_playlist_id = validated_data.get(
            "default_playlist_id", instance.default_playlist_id
        )
        instance.description = validated_data.get("description", instance.description)
        instance.email = validated_data.get("email", instance.email)
        instance.funding_category = validated_data.get(
            "funding_category", instance.funding_category
        )
        instance.image = validated_data.get("image", instance.image)
        instance.internal_note = validated_data.get("internal_note", instance.internal_note)
        instance.is_active = validated_data.get("is_active", instance.is_active)
        instance.is_public = validated_data.get("is_public", instance.is_public)
        instance.logo = validated_data.get("logo", instance.logo)
        instance.name = validated_data.get("name", instance.name)
        instance.predecessor = validated_data.get("predecessor", instance.predecessor)
        instance.short_description = validated_data.get(
            "short_description", instance.short_description
        )
        instance.slug = validated_data.get("slug", instance.slug)
        instance.type = validated_data.get("type", instance.type)

        instance.category.set(validated_data.get("category", instance.category))
        instance.hosts.set(validated_data.get("hosts", instance.hosts))
        instance.language.set(validated_data.get("language", instance.language))
        instance.music_focus.set(validated_data.get("music_focus", instance.music_focus))
        instance.owners.set(validated_data.get("owners", instance.owners))
        instance.topic.set(validated_data.get("topic", instance.topic))

        if links_data := validated_data.get("links"):
            instance = delete_links(instance)

            for link_data in links_data:
                ShowLink.objects.create(show=instance, **link_data)

        instance.updated_by = self.context.get("updated_by")

        instance.save()

        return instance


class RRuleSerializer(serializers.ModelSerializer):
    class Meta:
        model = RRule
        fields = (
            "id",
            "name",
        )
        read_only_fields = fields


class ScheduleSerializer(serializers.ModelSerializer):
    class Meta:
        model = Schedule
        fields = (
            "add_business_days_only",
            "add_days_no",
            "by_weekday",
            "default_playlist_id",
            "end_time",
            "first_date",
            "id",
            "is_repetition",
            "last_date",
            "rrule",
            "show",
            "start_time",
        )


class UnsavedScheduleSerializer(ScheduleSerializer):
    id = serializers.IntegerField(allow_null=True)


class ScheduleInRequestSerializer(ScheduleSerializer):
    dryrun = serializers.BooleanField(
        write_only=True,
        required=False,
        help_text=(
            "Whether to simulate the database changes. If true, no database changes will occur. "
            "Instead a list of objects that would be created, updated and deleted if dryrun was "
            "false will be returned."
        ),
    )

    class Meta:
        model = Schedule
        fields = (
            "add_business_days_only",
            "add_days_no",
            "by_weekday",
            "default_playlist_id",
            "dryrun",
            "end_time",
            "first_date",
            "is_repetition",
            "last_date",
            "rrule",
            "show",
            "start_time",
        )

    def create(self, validated_data):
        """Create and return a new Schedule instance, given the validated data."""

        rrule = validated_data.pop("rrule")
        show = validated_data.pop("show")

        schedule = Schedule.objects.create(**validated_data)
        schedule.rrule = rrule
        schedule.show = show

        schedule.save()
        return schedule

    def update(self, instance, validated_data):
        """Update and return an existing Schedule instance, given the validated data."""

        instance.by_weekday = validated_data.get("by_weekday", instance.by_weekday)
        instance.first_date = validated_data.get("first_date", instance.first_date)
        instance.start_time = validated_data.get("start_time", instance.start_time)
        instance.end_time = validated_data.get("end_time", instance.end_time)
        instance.last_date = validated_data.get("last_date", instance.last_date)
        instance.is_repetition = validated_data.get("is_repetition", instance.is_repetition)
        instance.default_playlist_id = validated_data.get(
            "default_playlist_id", instance.default_playlist_id
        )
        instance.rrule = validated_data.get("rrule", instance.rrule)
        instance.show = validated_data.get("show", instance.show)
        instance.add_days_no = validated_data.get("add_days_no", instance.add_days_no)
        instance.add_business_days_only = validated_data.get(
            "add_business_days_only", instance.add_business_days_only
        )

        instance.save()
        return instance


class CollisionSerializer(serializers.Serializer):
    id = serializers.IntegerField()
    start = serializers.DateTimeField()
    end = serializers.DateTimeField()
    playlist_id = serializers.IntegerField(allow_null=True)
    show = serializers.IntegerField()
    show_name = serializers.CharField()
    repetition_of = serializers.IntegerField(allow_null=True)
    schedule = serializers.IntegerField()
    memo = serializers.CharField()
    note_id = serializers.IntegerField(allow_null=True)


class ProjectedTimeSlotSerializer(serializers.Serializer):
    hash = serializers.CharField()
    start = serializers.DateTimeField()
    end = serializers.DateTimeField()
    collisions = CollisionSerializer(many=True)
    error = serializers.CharField(allow_null=True)
    solution_choices = serializers.ListField(child=serializers.ChoiceField(SOLUTION_CHOICES))


class DryRunTimeSlotSerializer(serializers.Serializer):
    id = serializers.PrimaryKeyRelatedField(queryset=TimeSlot.objects.all(), allow_null=True)
    schedule = serializers.PrimaryKeyRelatedField(queryset=Schedule.objects.all(), allow_null=True)
    playlist_id = serializers.IntegerField(allow_null=True)
    start = serializers.DateField()
    end = serializers.DateField()
    repetition_of = serializers.IntegerField(allow_null=True)
    memo = serializers.CharField()


class ScheduleCreateUpdateRequestSerializer(serializers.Serializer):
    schedule = ScheduleInRequestSerializer()
    solutions = serializers.DictField(
        child=serializers.ChoiceField(SOLUTION_CHOICES), required=False
    )
    notes = serializers.DictField(child=serializers.IntegerField(), required=False)
    playlists = serializers.DictField(child=serializers.IntegerField(), required=False)


class ScheduleResponseSerializer(serializers.Serializer):
    projected = ProjectedTimeSlotSerializer(many=True)
    solutions = serializers.DictField(child=serializers.ChoiceField(SOLUTION_CHOICES))
    notes = serializers.DictField(child=serializers.IntegerField())
    playlists = serializers.DictField(child=serializers.IntegerField())
    schedule = ScheduleSerializer()


class ScheduleConflictResponseSerializer(ScheduleResponseSerializer):
    schedule = UnsavedScheduleSerializer()


class ScheduleDryRunResponseSerializer(serializers.Serializer):
    created = DryRunTimeSlotSerializer(many=True)
    updated = DryRunTimeSlotSerializer(many=True)
    deleted = DryRunTimeSlotSerializer(many=True)


class TimeSlotSerializer(serializers.ModelSerializer):
    show = serializers.PrimaryKeyRelatedField(queryset=Show.objects.all())
    schedule = serializers.PrimaryKeyRelatedField(queryset=Schedule.objects.all())
    repetition_of = serializers.PrimaryKeyRelatedField(queryset=TimeSlot.objects.all())

    class Meta:
        model = TimeSlot
        fields = (
            "end",
            "memo",
            "note_id",
            "playlist_id",
            "repetition_of",
            "schedule",
            "show",
            "start",
        )

    def create(self, validated_data):
        """Create and return a new TimeSlot instance, given the validated data."""
        return TimeSlot.objects.create(**validated_data)

    def update(self, instance, validated_data):
        """Update and return an existing Show instance, given the validated data."""

        # Only save certain fields
        instance.memo = validated_data.get("memo", instance.memo)
        instance.repetition_of = validated_data.get("repetition_of", instance.repetition_of)
        instance.playlist_id = validated_data.get("playlist_id", instance.playlist_id)
        instance.save()
        return instance


class NoteLinkSerializer(serializers.ModelSerializer):
    class Meta:
        model = NoteLink
        fields = ("type", "url")


class NoteSerializer(serializers.ModelSerializer):
    contributors = serializers.PrimaryKeyRelatedField(queryset=Host.objects.all(), many=True)
    image = serializers.PrimaryKeyRelatedField(queryset=Image.objects.all(), required=False)
    links = NoteLinkSerializer(many=True, required=False)
    timeslot = serializers.PrimaryKeyRelatedField(queryset=TimeSlot.objects.all())

    class Meta:
        model = Note
        read_only_fields = (
            "created_at",
            "created_by",
            "updated_at",
            "updated_by",
        )
        fields = (
            "cba_id",
            "content",
            "contributors",
            "id",
            "image",
            "links",
            "owner",
            "playlist",
            "slug",
            "summary",
            "tags",
            "timeslot",
            "title",
        ) + read_only_fields

    def create(self, validated_data):
        """Create and return a new Note instance, given the validated data."""

        links_data = validated_data.pop("links", [])
        contributors = validated_data.pop("contributors", [])

        # the creator of the note is the owner
        validated_data["owner"] = self.context["user_id"]

        note = Note.objects.create(**validated_data | self.context)  # created_by

        note.contributors.set(contributors)

        if cba_id := validated_data.get("cba_id"):
            if audio_url := get_audio_url(cba_id):
                NoteLink.objects.create(note=note, type="CBA", url=audio_url)

        for link_data in links_data:
            NoteLink.objects.create(note=note, **link_data)

        note.save()

        # Assign note to timeslot
        if note.timeslot_id is not None:
            try:
                timeslot = TimeSlot.objects.get(pk=note.timeslot_id)
                timeslot.note_id = note.id
                timeslot.save(update_fields=["note_id"])
            except ObjectDoesNotExist:
                pass

        return note

    def update(self, instance, validated_data):
        """Update and return an existing Note instance, given the validated data."""

        instance.cba_id = validated_data.get("cba_id", instance.cba_id)
        instance.content = validated_data.get("content", instance.content)
        instance.image = validated_data.get("image", instance.image)
        instance.show = validated_data.get("show", instance.show)
        instance.slug = validated_data.get("slug", instance.slug)
        instance.summary = validated_data.get("summary", instance.summary)
        instance.timeslot = validated_data.get("timeslot", instance.timeslot)
        instance.title = validated_data.get("title", instance.title)

        instance.contributors.set(validated_data.get("contributors", instance.contributors))

        if cba_id := validated_data.get("cba_id"):
            if audio_url := get_audio_url(cba_id):
                NoteLink.objects.create(note=instance, type="CBA", url=audio_url)

        if links_data := validated_data.get("links"):
            instance = delete_links(instance)

            for link_data in links_data:
                NoteLink.objects.create(note=instance, **link_data)

        instance.updated_by = self.context.get("updated_by")

        instance.save()

        # Remove existing note connections from timeslots
        timeslots = TimeSlot.objects.filter(note_id=instance.id)
        for ts in timeslots:
            ts.note_id = None
            ts.save(update_fields=["note_id"])

        # Assign note to timeslot
        if instance.timeslot.id is not None:
            try:
                timeslot = TimeSlot.objects.get(pk=instance.timeslot.id)
                timeslot.note_id = instance.id
                timeslot.save(update_fields=["note_id"])
            except ObjectDoesNotExist:
                pass

        return instance