#
# 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 textwrap import dedent

from drf_spectacular.utils import OpenApiResponse, extend_schema, extend_schema_view
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,
    ErrorSerializer,
    FundingCategorySerializer,
    HostSerializer,
    LanguageSerializer,
    MusicFocusSerializer,
    NoteSerializer,
    ScheduleConflictResponseSerializer,
    ScheduleCreateUpdateRequestSerializer,
    ScheduleDryRunResponseSerializer,
    ScheduleResponseSerializer,
    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",
    )


@extend_schema_view(
    create=extend_schema(summary="Create a new user."),
    retrieve=extend_schema(
        summary="Retrieve a single user.",
        description="Non-admin users may only retrieve their own user record.",
    ),
    update=extend_schema(
        summary="Update an existing user.",
        description="Non-admin users may only update their own user record.",
    ),
    partial_update=extend_schema(
        summary="Partially update an existing user.",
        description="Non-admin users may only update their own user record.",
    ),
    list=extend_schema(
        summary="List all users.",
        description=(
            "The returned list of records will only contain a single record "
            "for non-admin users which is their own user account."
        ),
    ),
)
class APIUserViewSet(
    DisabledObjectPermissionCheckMixin,
    mixins.CreateModelMixin,
    mixins.RetrieveModelMixin,
    mixins.UpdateModelMixin,
    mixins.ListModelMixin,
    viewsets.GenericViewSet,
):
    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):
        """
        Only admins 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)


@extend_schema_view(
    create=extend_schema(summary="Create a new show."),
    retrieve=extend_schema(summary="Retrieve a single show."),
    update=extend_schema(summary="Update an existing show."),
    partial_update=extend_schema(summary="Partially update an existing show."),
    destroy=extend_schema(summary="Delete an existing show."),
    list=extend_schema(summary="List all shows."),
)
class APIShowViewSet(DisabledObjectPermissionCheckMixin, viewsets.ModelViewSet):
    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):
        """
        Only admins 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):
        """
        Non-admin 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):
        """
        Only admins 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)


@extend_schema_view(
    create=extend_schema(
        summary="Create a new schedule.",
        request=ScheduleCreateUpdateRequestSerializer,
        responses={
            status.HTTP_201_CREATED: OpenApiResponse(
                response=ScheduleResponseSerializer,
                description=(
                    "Signals the successful creation of the schedule and of the projected "
                    "timeslots."
                ),
            ),
            status.HTTP_202_ACCEPTED: OpenApiResponse(
                response=ScheduleDryRunResponseSerializer,
                description=(
                    "Returns the list of timeslots that would be created, updated and deleted if "
                    "the schedule request would not have been sent with the dryrun flag."
                ),
            ),
            status.HTTP_400_BAD_REQUEST: OpenApiResponse(
                response=ErrorSerializer(many=True),
                description=dedent(
                    """
                    Returned in case the request contained invalid data.

                    This may happen if:
                    * the last date is before the start date (`no-start-after-end`),
                      in which case you should correct either the start or until date.
                    * The start and last date are the same (`no-same-day-start-and-end`).
                      This is only allowed for single timeslots with the recurrence rule
                      set to `once`. You should fix either the start or until date.
                    * The number of conflicts and solutions aren’t the same
                      (`one-solution-per-conflict`). Only one solution is allowed per conflict,
                      so you either offered too many or not enough solutions for any reported
                      conflicts.
                      """
                ),
            ),
            status.HTTP_403_FORBIDDEN: OpenApiResponse(
                response=ErrorSerializer,
                description=(
                    "Returned in case the request contained no or invalid authenticated data "
                    "or the authenticated user does not have authorization to perform the "
                    "requested operation."
                ),
            ),
            status.HTTP_409_CONFLICT: OpenApiResponse(
                response=ScheduleConflictResponseSerializer,
                description=dedent(
                    """
                    Returns the list of projected timeslots and any collisions that may have
                    been found for existing timeslots.

                    Errors on projected timeslots may include:
                    * 'This change on the timeslot is not allowed.'
                      When adding: There was a change in the schedule's data during conflict
                      resolution.
                      When updating: Fields 'start', 'end', 'byweekday' or 'rrule' have changed,
                      which is not allowed.
                    * 'No solution given': No solution was provided for the conflict in
                      `solutions`. Provide a value of `solution_choices`.
                    * 'Given solution is not accepted for this conflict.':
                      The solution has a value which is not part of `solution_choices`.
                      Provide a value of `solution_choices` (at least `ours` or `theirs`).
                    """
                ),
            ),
        },
    ),
    retrieve=extend_schema(summary="Retrieve a single schedule."),
    update=extend_schema(
        summary="Update an existing schedule.",
        request=ScheduleCreateUpdateRequestSerializer,
    ),
    partial_update=extend_schema(
        summary="Partially update an existing schedule.",
        request=ScheduleCreateUpdateRequestSerializer,
    ),
    destroy=extend_schema(summary="Delete an existing schedule."),
    list=extend_schema(summary="List all schedules."),
)
class APIScheduleViewSet(
    DisabledObjectPermissionCheckMixin,
    NestedObjectFinderMixin,
    viewsets.ModelViewSet,
):
    ROUTE_FILTER_LOOKUPS = {
        "show_pk": "show",
    }

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

    def get_serializer_class(self):
        if self.action in ("create", "update", "partial_update"):
            return ScheduleCreateUpdateRequestSerializer
        return super().get_serializer_class()

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

        Note that creating or updating a schedule is the only way to create timeslots.

        Only admins may add schedules.

        The projected timeslots defined by the schedule are matched against existing
        timeslots. The API will return an object that contains

        * the schedule's data,
        * projected timeslots,
        * detected collisions,
        * and possible solutions.

        As long as no `solutions` object has been set or unresolved collisions exist,
        no data is written to the database. A schedule is only created if at least
        one timeslot was generated by it.

        In order to resolve any possible conflicts, the client must submit a new request with
        a solution for each conflict. Possible solutions are listed as part of the projected
        timeslot in the `solution_choices` array. In a best-case scenario with no detected
        conflicts an empty solutions object will suffice. For more details on the individual
        types of solutions see the SolutionChoicesEnum.

        **Please note**:
        If there's more than one collision for a projected timeslot, only `theirs` and `ours`
        are currently supported as solutions.
        """

        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 admins 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):
        """
        Only admins 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.
@extend_schema_view(
    retrieve=extend_schema(summary="Retrieve a single timeslot."),
    update=extend_schema(summary="Update an existing timeslot."),
    partial_update=extend_schema(summary="Partially update an existing timeslot."),
    destroy=extend_schema(summary="Delete an existing timeslot."),
    list=extend_schema(
        summary="List all timeslots.",
        description=dedent(
            """
            By default, only timeslots ranging from now + 60 days will be displayed.
            You may override this default overriding start and/or end parameter.
            """
        ),
    ),
)
class APITimeSlotViewSet(
    DisabledObjectPermissionCheckMixin,
    NestedObjectFinderMixin,
    mixins.RetrieveModelMixin,
    mixins.UpdateModelMixin,
    mixins.DestroyModelMixin,
    mixins.ListModelMixin,
    viewsets.GenericViewSet,
):
    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):
        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):
        """
        Only admins 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)


@extend_schema_view(
    create=extend_schema(summary="Create a new note."),
    retrieve=extend_schema(summary="Retrieve a single note."),
    update=extend_schema(summary="Update an existing note."),
    partial_update=extend_schema(
        summary="Partially update an existing note.",
        description="Only admins can partially update existing notes.",
    ),
    destroy=extend_schema(summary="Delete an existing note."),
    list=extend_schema(summary="List all notes."),
)
class APINoteViewSet(
    DisabledObjectPermissionCheckMixin,
    NestedObjectFinderMixin,
    viewsets.ModelViewSet,
):
    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 admins 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 admins 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 admins 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


@extend_schema_view(
    create=extend_schema(summary="Create a new category."),
    retrieve=extend_schema(summary="Retrieve a single category."),
    update=extend_schema(summary="Update an existing category."),
    partial_update=extend_schema(summary="Partially update an existing category."),
    destroy=extend_schema(summary="Delete an existing category."),
    list=extend_schema(summary="List all categories."),
)
class APICategoryViewSet(ActiveFilterMixin, viewsets.ModelViewSet):
    queryset = Category.objects.all()
    serializer_class = CategorySerializer


@extend_schema_view(
    create=extend_schema(summary="Create a new type."),
    retrieve=extend_schema(summary="Retrieve a single type."),
    update=extend_schema(summary="Update an existing type."),
    partial_update=extend_schema(summary="Partially update an existing type."),
    destroy=extend_schema(summary="Delete an existing type."),
    list=extend_schema(summary="List all types."),
)
class APITypeViewSet(ActiveFilterMixin, viewsets.ModelViewSet):
    queryset = Type.objects.all()
    serializer_class = TypeSerializer


@extend_schema_view(
    create=extend_schema(summary="Create a new topic."),
    retrieve=extend_schema(summary="Retrieve a single topic."),
    update=extend_schema(summary="Update an existing topic."),
    partial_update=extend_schema(summary="Partially update an existing topic."),
    destroy=extend_schema(summary="Delete an existing topic."),
    list=extend_schema(summary="List all topics."),
)
class APITopicViewSet(ActiveFilterMixin, viewsets.ModelViewSet):
    queryset = Topic.objects.all()
    serializer_class = TopicSerializer


@extend_schema_view(
    create=extend_schema(summary="Create a new music focus."),
    retrieve=extend_schema(summary="Retrieve a single music focus."),
    update=extend_schema(summary="Update an existing music focus."),
    partial_update=extend_schema(summary="Partially update an existing music focus."),
    destroy=extend_schema(summary="Delete an existing music focus."),
    list=extend_schema(summary="List all music focuses."),
)
class APIMusicFocusViewSet(ActiveFilterMixin, viewsets.ModelViewSet):
    queryset = MusicFocus.objects.all()
    serializer_class = MusicFocusSerializer


@extend_schema_view(
    create=extend_schema(summary="Create a new funding category."),
    retrieve=extend_schema(summary="Retrieve a single funding category."),
    update=extend_schema(summary="Update an existing funding category."),
    partial_update=extend_schema(
        summary="Partially update an existing funding category."
    ),
    destroy=extend_schema(summary="Delete an existing funding category."),
    list=extend_schema(summary="List all funding categories."),
)
class APIFundingCategoryViewSet(ActiveFilterMixin, viewsets.ModelViewSet):
    queryset = FundingCategory.objects.all()
    serializer_class = FundingCategorySerializer


@extend_schema_view(
    create=extend_schema(summary="Create a new language."),
    retrieve=extend_schema(summary="Retrieve a single language."),
    update=extend_schema(summary="Update an existing language."),
    partial_update=extend_schema(summary="Partially update an existing language."),
    destroy=extend_schema(summary="Delete an existing language."),
    list=extend_schema(summary="List all languages."),
)
class APILanguageViewSet(ActiveFilterMixin, viewsets.ModelViewSet):
    queryset = Language.objects.all()
    serializer_class = LanguageSerializer


@extend_schema_view(
    create=extend_schema(summary="Create a new host."),
    retrieve=extend_schema(summary="Retrieve a single host."),
    update=extend_schema(summary="Update an existing host."),
    partial_update=extend_schema(summary="Partially update an existing host."),
    destroy=extend_schema(summary="Delete an existing host."),
    list=extend_schema(summary="List all hosts."),
)
class APIHostViewSet(ActiveFilterMixin, viewsets.ModelViewSet):
    queryset = Host.objects.all()
    serializer_class = HostSerializer
    pagination_class = LimitOffsetPagination