#
# 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/>.
#

import json
import logging
from datetime import date, datetime, time

from rest_framework import mixins, permissions, status, viewsets
from rest_framework.pagination import LimitOffsetPagination
from rest_framework.response import Response

from django.contrib.auth.models import User
from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from django.utils import timezone
from django.utils.translation import gettext as _
from program import filters
from program.models import (
    Category,
    FundingCategory,
    Host,
    Language,
    MusicFocus,
    Note,
    Schedule,
    Show,
    TimeSlot,
    Topic,
    Type,
)
from program.serializers import (
    CategorySerializer,
    FundingCategorySerializer,
    HostSerializer,
    LanguageSerializer,
    MusicFocusSerializer,
    NoteSerializer,
    ScheduleSerializer,
    ShowSerializer,
    TimeSlotSerializer,
    TopicSerializer,
    TypeSerializer,
    UserSerializer,
)
from program.utils import (
    DisabledObjectPermissionCheckMixin,
    NestedObjectFinderMixin,
    get_values,
    parse_date,
)

logger = logging.getLogger(__name__)


def json_day_schedule(request, year=None, month=None, day=None):
    if year is None and month is None and day is None:
        today = timezone.make_aware(datetime.combine(timezone.now(), time(0, 0)))
    else:
        today = timezone.make_aware(
            datetime.combine(date(year, month, day), time(0, 0))
        )

    timeslots = (
        TimeSlot.objects.get_24h_timeslots(today)
        .select_related("schedule")
        .select_related("show")
    )
    schedule = []
    for ts in timeslots:
        entry = {
            "start": ts.start.strftime("%Y-%m-%d_%H:%M:%S"),
            "end": ts.end.strftime("%Y-%m-%d_%H:%M:%S"),
            "title": ts.show.name,
            "id": ts.show.id,
        }

        schedule.append(entry)

    return HttpResponse(
        json.dumps(schedule, ensure_ascii=False).encode("utf8"),
        content_type="application/json; charset=utf-8",
    )


def json_playout(request):
    """
    Called by
       - engine (playout) to retrieve timeslots within a given timerange
         Expects GET variables 'start' (date) and 'end' (date).
         If start not given, it will be today

       - internal calendar to retrieve all timeslots for a week
         Expects GET variable 'start' (date), otherwise start will be today
         If end not given, it returns all timeslots of the next 7 days
    """

    if request.GET.get("start") is None:
        start = timezone.make_aware(datetime.combine(timezone.now(), time(0, 0)))
    else:
        start = timezone.make_aware(
            datetime.combine(parse_date(request.GET.get("start")), time(0, 0))
        )

    if request.GET.get("end") is None:
        # If no end was given, return the next week
        timeslots = (
            TimeSlot.objects.get_7d_timeslots(start)
            .select_related("schedule")
            .select_related("show")
        )
    else:
        # Otherwise return the given timerange
        end = timezone.make_aware(
            datetime.combine(parse_date(request.GET.get("end")), time(23, 59))
        )
        timeslots = (
            TimeSlot.objects.get_timerange_timeslots(start, end)
            .select_related("schedule")
            .select_related("show")
        )

    schedule = []
    for ts in timeslots:
        is_repetition = " " + _("REP") if ts.schedule.is_repetition is True else ""

        hosts = ", ".join(ts.show.hosts.values_list("name", flat=True))
        categories = ", ".join(ts.show.category.values_list("name", flat=True))
        topics = ", ".join(ts.show.topic.values_list("name", flat=True))
        music_focus = ", ".join(ts.show.music_focus.values_list("name", flat=True))
        languages = ", ".join(ts.show.language.values_list("name", flat=True))
        funding_category = (
            FundingCategory.objects.get(pk=ts.show.funding_category_id)
            if ts.show.funding_category_id
            else None
        )

        type_ = Type.objects.get(pk=ts.show.type_id)

        classname = "default"

        if ts.playlist_id is None or ts.playlist_id == 0:
            classname = "danger"

        entry = {
            "id": ts.id,
            "start": ts.start.strftime("%Y-%m-%dT%H:%M:%S"),
            "end": ts.end.strftime("%Y-%m-%dT%H:%M:%S"),
            "title": ts.show.name + is_repetition,  # For JS Calendar
            "schedule_id": ts.schedule.id,
            "is_repetition": ts.is_repetition,
            "playlist_id": ts.playlist_id,
            "schedule_default_playlist_id": ts.schedule.default_playlist_id,
            "show_default_playlist_id": ts.show.default_playlist_id,
            "show_id": ts.show.id,
            "show_name": ts.show.name + is_repetition,
            "show_hosts": hosts,
            "show_type": type_.name,
            "show_categories": categories,
            "show_topics": topics,
            # TODO: replace `show_musicfocus` with `show_music_focus` when engine is updated
            "show_musicfocus": music_focus,
            "show_languages": languages,
            # TODO: replace `show_fundingcategory` with `show_funding_category` when engine is
            #  updated
            "show_fundingcategory": funding_category.name,
            "memo": ts.memo,
            "className": classname,
        }

        schedule.append(entry)

    return HttpResponse(
        json.dumps(schedule, ensure_ascii=False).encode("utf8"),
        content_type="application/json; charset=utf-8",
    )


class APIUserViewSet(
    DisabledObjectPermissionCheckMixin,
    mixins.CreateModelMixin,
    mixins.RetrieveModelMixin,
    mixins.UpdateModelMixin,
    mixins.ListModelMixin,
    viewsets.GenericViewSet,
):
    """
    Returns a list of users.

    Only returns the user that is currently authenticated unless the user is a superuser.
    """

    permission_classes = [permissions.DjangoModelPermissionsOrAnonReadOnly]
    serializer_class = UserSerializer
    queryset = User.objects.all()

    def get_queryset(self):
        queryset = super().get_queryset()

        # Constrain access to oneself except for superusers.
        if not self.request.user.is_superuser:
            queryset = queryset.filter(pk=self.request.user.id)

        return queryset

    def create(self, request, *args, **kwargs):
        """
        Create a User.

        Only superusers may create users.
        """

        if not request.user.is_superuser:
            return Response(status=status.HTTP_401_UNAUTHORIZED)

        serializer = UserSerializer(data=request.data)

        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)

        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)


class APIShowViewSet(DisabledObjectPermissionCheckMixin, viewsets.ModelViewSet):
    """
    Returns a list of available shows.

    Only superusers may add and delete shows.
    """

    queryset = Show.objects.all()
    serializer_class = ShowSerializer
    permission_classes = [permissions.DjangoModelPermissionsOrAnonReadOnly]
    pagination_class = LimitOffsetPagination
    filterset_class = filters.ShowFilterSet

    def get_object(self):
        queryset = self.filter_queryset(self.get_queryset())
        lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
        lookup_arg = self.kwargs[lookup_url_kwarg]
        # allow object retrieval through id or slug
        try:
            filter_kwargs = {self.lookup_field: int(lookup_arg)}
        except ValueError:
            filter_kwargs = {"slug": lookup_arg}
        obj = get_object_or_404(queryset, **filter_kwargs)
        self.check_object_permissions(self.request, obj)
        return obj

    def create(self, request, *args, **kwargs):
        """
        Create a show.

        Only superusers may create a show.
        """

        if not request.user.is_superuser:
            return Response(status=status.HTTP_401_UNAUTHORIZED)

        serializer = ShowSerializer(data=request.data)

        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)

        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

    def update(self, request, *args, **kwargs):
        """
        Update a show.

        Common users may only update shows they own.
        """

        pk = get_values(self.kwargs, "pk")

        if not request.user.is_superuser and pk not in request.user.shows.values_list(
            "id", flat=True
        ):
            return Response(status=status.HTTP_401_UNAUTHORIZED)

        show = self.get_object()
        serializer = ShowSerializer(
            show, data=request.data, context={"user": request.user}
        )

        if serializer.is_valid():
            # Common users mustn't edit the show's name
            if not request.user.is_superuser:
                serializer.validated_data["name"] = show.name
            serializer.save()
            return Response(serializer.data)

        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

    def destroy(self, request, *args, **kwargs):
        """
        Delete a show.

        Only superusers may delete shows.
        """

        if not request.user.is_superuser:
            return Response(status=status.HTTP_401_UNAUTHORIZED)

        self.get_object().delete()

        return Response(status=status.HTTP_204_NO_CONTENT)


class APIScheduleViewSet(
    DisabledObjectPermissionCheckMixin,
    NestedObjectFinderMixin,
    viewsets.ModelViewSet,
):
    """
    Returns a list of schedules.

    Only superusers may create and update schedules.
    """

    ROUTE_FILTER_LOOKUPS = {
        "show_pk": "show",
    }

    queryset = Schedule.objects.all()
    serializer_class = ScheduleSerializer
    permission_classes = [permissions.DjangoModelPermissionsOrAnonReadOnly]

    def create(self, request, *args, **kwargs):
        """
        Create a schedule, generate timeslots, test for collisions and resolve them including notes

        Only superusers may add schedules.
        """

        if not request.user.is_superuser:
            return Response(status=status.HTTP_401_UNAUTHORIZED)

        pk, show_pk = get_values(self.kwargs, "pk", "show_pk")

        # Only allow creating when calling /shows/{show_pk}/schedules/ and with ehe `schedule` JSON
        # object
        if show_pk is None or "schedule" not in request.data:
            return Response(status=status.HTTP_400_BAD_REQUEST)

        # First create submit -> return projected timeslots and collisions
        # TODO: Perhaps directly insert into database if no conflicts found
        if "solutions" not in request.data:
            # TODO: respond with status.HTTP_409_CONFLICT when the dashboard can handle it
            return Response(
                Schedule.make_conflicts(request.data["schedule"], pk, show_pk),
            )

        # Otherwise try to resolve
        resolution = Schedule.resolve_conflicts(request.data, pk, show_pk)

        if all(key in resolution for key in ["create", "update", "delete"]):
            # this is a dry-run
            return Response(resolution, status=status.HTTP_202_ACCEPTED)

        # If resolution went well
        if "projected" not in resolution:
            return Response(resolution, status=status.HTTP_201_CREATED)

        # Otherwise return conflicts
        # TODO: respond with status.HTTP_409_CONFLICT when the dashboard can handle it
        return Response(resolution)

    def update(self, request, *args, **kwargs):
        """
        Update a schedule, generate timeslots, test for collisions and resolve
        them including notes.

        Only superusers may update schedules.
        """

        if not request.user.is_superuser:
            return Response(status=status.HTTP_401_UNAUTHORIZED)

        # Only allow updating when with the `schedule` JSON object
        if "schedule" not in request.data:
            return Response(status=status.HTTP_400_BAD_REQUEST)

        schedule = self.get_object()

        # If default playlist id or repetition are given, just update
        if default_playlist_id := request.data.get("schedule").get(
            "default_playlist_id"
        ):
            schedule.default_playlist_id = int(default_playlist_id)
            schedule.save()

            serializer = ScheduleSerializer(schedule)
            return Response(serializer.data)

        if is_repetition := request.data.get("schedule").get("is_repetition"):
            schedule.is_repetition = bool(is_repetition)
            schedule.save()

            serializer = ScheduleSerializer(schedule)
            return Response(serializer.data)

        # First update submit -> return projected timeslots and collisions
        if "solutions" not in request.data:
            # TODO: respond with status.HTTP_409_CONFLICT when the dashboard can handle it
            return Response(
                Schedule.make_conflicts(
                    request.data["schedule"], schedule.pk, schedule.show.pk
                )
            )

        # Otherwise try to resolve
        resolution = Schedule.resolve_conflicts(
            request.data, schedule.pk, schedule.show.pk
        )

        # If resolution went well
        if "projected" not in resolution:
            return Response(resolution)

        # Otherwise return conflicts
        # TODO: respond with status.HTTP_409_CONFLICT when the dashboard can handle it
        return Response(resolution)

    def destroy(self, request, *args, **kwargs):
        """
        Delete a schedule.

        Only superusers may delete schedules.
        """

        if not request.user.is_superuser:
            return Response(status=status.HTTP_401_UNAUTHORIZED)

        self.get_object().delete()

        return Response(status=status.HTTP_204_NO_CONTENT)


# TODO: Create is currently not implemented because timeslots are supposed to be inserted
#       by creating or updating a schedule.
#       There might be a use case for adding a single timeslot without any conflicts though.
class APITimeSlotViewSet(
    DisabledObjectPermissionCheckMixin,
    NestedObjectFinderMixin,
    mixins.RetrieveModelMixin,
    mixins.UpdateModelMixin,
    mixins.DestroyModelMixin,
    mixins.ListModelMixin,
    viewsets.GenericViewSet,
):
    """
    Returns a list of timeslots.

    By default, only timeslots ranging from now + 60 days will be displayed.
    You may override this default overriding start and/or end parameter.

    Timeslots may only be added by creating/updating a schedule.
    """

    ROUTE_FILTER_LOOKUPS = {
        "show_pk": "show",
        "schedule_pk": "schedule",
    }

    permission_classes = [permissions.DjangoModelPermissionsOrAnonReadOnly]
    serializer_class = TimeSlotSerializer
    pagination_class = LimitOffsetPagination
    queryset = TimeSlot.objects.all().order_by("-start")
    filterset_class = filters.TimeSlotFilterSet

    def update(self, request, *args, **kwargs):
        """Link a playlist_id to a timeslot"""

        show_pk = get_values(self.kwargs, "show_pk")

        if (
            not request.user.is_superuser
            and show_pk not in request.user.shows.values_lis("id", flat=True)
        ):
            return Response(status=status.HTTP_401_UNAUTHORIZED)

        timeslot = self.get_object()
        serializer = TimeSlotSerializer(timeslot, data=request.data)
        if serializer.is_valid():
            serializer.save()

            # Return the next repetition
            # We do this because the Dashboard needs to update the repetition timeslot as well
            # but with another playlist containing the recording instead of the original playlist
            ts = TimeSlot.objects.filter(show=show_pk, start__gt=timeslot.start)[0]
            if ts.is_repetition:
                serializer = TimeSlotSerializer(ts)
                return Response(serializer.data)

            # ...or nothing if there isn't one
            return Response(status=status.HTTP_200_OK)

        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

    def destroy(self, request, *args, **kwargs):
        """
        Deletes a timeslot.

        Only superusers may delete timeslots.
        """

        if not request.user.is_superuser:
            return Response(status=status.HTTP_401_UNAUTHORIZED)

        self.get_object().delete()

        return Response(status=status.HTTP_204_NO_CONTENT)


class APINoteViewSet(
    DisabledObjectPermissionCheckMixin,
    NestedObjectFinderMixin,
    viewsets.ModelViewSet,
):
    """
    Returns a list of notes.

    Superusers may access and update all notes.
    """

    ROUTE_FILTER_LOOKUPS = {
        "show_pk": "show",
        "timeslot_pk": "timeslot",
    }

    queryset = Note.objects.all()
    serializer_class = NoteSerializer
    permission_classes = [permissions.DjangoModelPermissionsOrAnonReadOnly]
    pagination_class = LimitOffsetPagination
    filter_class = filters.NoteFilterSet

    def create(self, request, *args, **kwargs):
        """
        Only superusers can create new notes.
        """
        show_pk, timeslot_pk = get_values(self.kwargs, "show_pk", "timeslot_pk")

        if (
            not request.user.is_superuser
            and show_pk not in request.user.shows.values_list("id", flat=True)
        ):
            return Response(status=status.HTTP_401_UNAUTHORIZED)

        serializer = NoteSerializer(
            data={"show": show_pk, "timeslot": timeslot_pk} | request.data,
            context={"user_id": request.user.id},
        )

        if serializer.is_valid():
            hosts = Host.objects.filter(
                shows__in=request.user.shows.values_list("id", flat=True)
            )
            if not request.user.is_superuser and request.data["host"] not in hosts:
                serializer.validated_data["host"] = None

            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)

        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

    def update(self, request, *args, **kwargs):
        """
        Only superusers can update existing notes.
        """
        show_pk = get_values(self.kwargs, "show_pk")

        if (
            not request.user.is_superuser
            and show_pk not in request.user.shows.values_list("id", flat=True)
        ):
            return Response(status=status.HTTP_401_UNAUTHORIZED)

        note = self.get_object()
        serializer = NoteSerializer(note, data=request.data)

        if serializer.is_valid():
            hosts = Host.objects.filter(
                shows__in=request.user.shows.values_list("id", flat=True)
            )
            # Don't assign a host the user mustn't edit. Reassign the original value instead
            if not request.user.is_superuser and int(request.data["host"]) not in hosts:
                serializer.validated_data["host"] = Host.objects.filter(
                    pk=note.host_id
                )[0]

            serializer.save()
            return Response(serializer.data)

        return Response(status=status.HTTP_400_BAD_REQUEST)

    def destroy(self, request, *args, **kwargs):
        """
        Only superusers can delete existing notes.
        """
        show_pk = get_values(self.kwargs, "show_pk")

        if (
            not request.user.is_superuser
            and show_pk not in request.user.shows.values_list("id", flat=True)
        ):
            return Response(status=status.HTTP_401_UNAUTHORIZED)

        self.get_object().delete()

        return Response(status=status.HTTP_204_NO_CONTENT)


class ActiveFilterMixin:
    filter_class = filters.ActiveFilterSet


class APICategoryViewSet(ActiveFilterMixin, viewsets.ModelViewSet):
    """
    Returns a list of categories.
    """

    queryset = Category.objects.all()
    serializer_class = CategorySerializer


class APITypeViewSet(ActiveFilterMixin, viewsets.ModelViewSet):
    """
    Returns a list of types.
    """

    queryset = Type.objects.all()
    serializer_class = TypeSerializer


class APITopicViewSet(ActiveFilterMixin, viewsets.ModelViewSet):
    """
    Returns a list of topics.
    """

    queryset = Topic.objects.all()
    serializer_class = TopicSerializer


class APIMusicFocusViewSet(ActiveFilterMixin, viewsets.ModelViewSet):
    """
    Returns a list of music focuses.
    """

    queryset = MusicFocus.objects.all()
    serializer_class = MusicFocusSerializer


class APIFundingCategoryViewSet(ActiveFilterMixin, viewsets.ModelViewSet):
    """
    Returns a list of funding categories.
    """

    queryset = FundingCategory.objects.all()
    serializer_class = FundingCategorySerializer


class APILanguageViewSet(ActiveFilterMixin, viewsets.ModelViewSet):
    """
    Returns a list of languages.
    """

    queryset = Language.objects.all()
    serializer_class = LanguageSerializer


class APIHostViewSet(ActiveFilterMixin, viewsets.ModelViewSet):
    """
    Returns a list of hosts.
    """

    queryset = Host.objects.all()
    serializer_class = HostSerializer
    pagination_class = LimitOffsetPagination