import base64
import datetime
import logging
import urllib.parse

import django.db.utils
import requests.exceptions
from django.conf import settings
from django.core.cache import cache
from django.db import transaction
from django.db.models import Count, F, Prefetch, Q, Sum
from django.db.models.functions import Collate
from django.utils import timezone
from drf_spectacular.utils import OpenApiParameter, extend_schema, extend_schema_view
from rest_framework import mixins, renderers
from rest_framework import settings as rest_settings
from rest_framework import views, viewsets
from rest_framework.decorators import action
from rest_framework.parsers import FormParser, MultiPartParser
from rest_framework.response import Response

from funkwhale_api.common import decorators as common_decorators
from funkwhale_api.common import permissions as common_permissions
from funkwhale_api.common import preferences
from funkwhale_api.common import utils as common_utils
from funkwhale_api.common import views as common_views
from funkwhale_api.federation import actors
from funkwhale_api.federation import api_serializers as federation_api_serializers
from funkwhale_api.federation import decorators as federation_decorators
from funkwhale_api.federation import models as federation_models
from funkwhale_api.federation import routes
from funkwhale_api.federation import tasks as federation_tasks
from funkwhale_api.federation.authentication import SignatureAuthentication
from funkwhale_api.tags.models import Tag, TaggedItem
from funkwhale_api.users.authentication import ScopedTokenAuthentication
from funkwhale_api.users.oauth import permissions as oauth_permissions

from . import filters, licenses, models, serializers, tasks, utils

logger = logging.getLogger(__name__)

TAG_PREFETCH = Prefetch(
    "tagged_items",
    queryset=TaggedItem.objects.all().select_related().order_by("tag__name"),
    to_attr="_prefetched_tagged_items",
)


def get_libraries(filter_uploads):
    def libraries(self, request, *args, **kwargs):
        obj = self.get_object()
        actor = utils.get_actor_from_request(request)
        uploads = models.Upload.objects.all()
        uploads = filter_uploads(obj, uploads)
        uploads = uploads.playable_by(actor)
        qs = models.Library.objects.filter(
            pk__in=uploads.values_list("library", flat=True),
            channel=None,
        ).annotate(_uploads_count=Count("uploads"))
        qs = qs.prefetch_related("actor")
        page = self.paginate_queryset(qs)
        if page is not None:
            serializer = federation_api_serializers.LibrarySerializer(page, many=True)
            return self.get_paginated_response(serializer.data)

        serializer = federation_api_serializers.LibrarySerializer(qs, many=True)
        return Response(serializer.data)

    return extend_schema(
        responses=federation_api_serializers.LibrarySerializer(many=True),
        parameters=[OpenApiParameter("id", location="query", exclude=True)],
    )(action(methods=["get"], detail=True)(libraries))


def refetch_obj(obj, queryset):
    """
    Given an Artist/Album/Track instance, if the instance is from a remote pod,
    will attempt to update local data with the latest ActivityPub representation.
    """
    if obj.is_local:
        return obj

    now = timezone.now()
    limit = now - datetime.timedelta(minutes=settings.FEDERATION_OBJECT_FETCH_DELAY)
    last_fetch = obj.fetches.order_by("-creation_date").first()
    if last_fetch is not None and last_fetch.creation_date > limit:
        # we fetched recently, no need to do it again
        return obj

    logger.info("Refetching %s:%s at %s…", obj._meta.label, obj.pk, obj.fid)
    actor = actors.get_service_actor()
    fetch = federation_models.Fetch.objects.create(actor=actor, url=obj.fid, object=obj)
    try:
        federation_tasks.fetch(fetch_id=fetch.pk)
    except Exception:
        logger.exception(
            "Error while refetching %s:%s at %s…", obj._meta.label, obj.pk, obj.fid
        )
    else:
        fetch.refresh_from_db()
        if fetch.status == "finished":
            obj = queryset.get(pk=obj.pk)
    return obj


class HandleInvalidSearch:
    def list(self, *args, **kwargs):
        try:
            return super().list(*args, **kwargs)
        except django.db.utils.ProgrammingError as e:
            if "in tsquery:" in str(e):
                return Response({"detail": "Invalid query"}, status=400)
            else:
                raise


class ArtistViewSet(
    HandleInvalidSearch,
    common_views.SkipFilterForGetObject,
    viewsets.ReadOnlyModelViewSet,
):
    queryset = (
        models.Artist.objects.all()
        .select_related("attributed_to", "attachment_cover")
        .order_by("-id")
        .prefetch_related(
            "channel__actor",
        )
        .annotate(_tracks_count=Count("artist_credit__tracks"))
    )
    serializer_class = serializers.ArtistWithAlbumsSerializer
    permission_classes = [oauth_permissions.ScopePermission]
    required_scope = "libraries"
    anonymous_policy = "setting"
    filterset_class = filters.ArtistFilter

    fetches = federation_decorators.fetches_route()
    mutations = common_decorators.mutations_route(types=["update"])

    def get_object(self):
        obj = super().get_object()

        if (
            self.action == "retrieve"
            and self.request.GET.get("refresh", "").lower() == "true"
        ):
            obj = refetch_obj(obj, self.get_queryset())
        return obj

    def get_serializer_context(self):
        context = super().get_serializer_context()
        context["description"] = self.action in ["retrieve", "create", "update"]
        return context

    def get_queryset(self):
        queryset = super().get_queryset()
        albums = (
            models.Album.objects.with_tracks_count()
            .select_related("attachment_cover")
            .prefetch_related("tracks")
        )
        albums = albums.annotate_playable_by_actor(
            utils.get_actor_from_request(self.request)
        )
        return queryset.prefetch_related(
            Prefetch("artist_credit__albums", queryset=albums), TAG_PREFETCH
        )

    libraries = get_libraries(
        lambda o, uploads: uploads.filter(
            Q(track__artist_credit__artist=o) | Q(track__album__artist_credit__artist=o)
        )
    )


class AlbumViewSet(
    HandleInvalidSearch,
    common_views.SkipFilterForGetObject,
    mixins.CreateModelMixin,
    mixins.DestroyModelMixin,
    viewsets.ReadOnlyModelViewSet,
):
    queryset = (
        models.Album.objects.all()
        .order_by("-creation_date")
        .select_related("attributed_to", "attachment_cover")
        .prefetch_related("artist_credit__artist__channel")
    ).distinct()
    serializer_class = serializers.AlbumSerializer
    permission_classes = [oauth_permissions.ScopePermission]
    required_scope = "libraries"
    anonymous_policy = "setting"
    filterset_class = filters.AlbumFilter

    fetches = federation_decorators.fetches_route()
    mutations = common_decorators.mutations_route(types=["update"])

    def get_object(self):
        obj = super().get_object()

        if (
            self.action == "retrieve"
            and self.request.GET.get("refresh", "").lower() == "true"
        ):
            obj = refetch_obj(obj, self.get_queryset())
        return obj

    def get_serializer_context(self):
        context = super().get_serializer_context()
        context["description"] = self.action in [
            "retrieve",
            "create",
        ]
        context["user"] = self.request.user
        return context

    def get_queryset(self):
        queryset = super().get_queryset()
        if self.action in ["destroy"]:
            queryset = queryset.exclude(artist_credit__artist__channel=None).filter(
                artist_credit__artist__attributed_to=self.request.user.actor
            )

        tracks = models.Track.objects.all().prefetch_related("album")
        tracks = tracks.annotate_playable_by_actor(
            utils.get_actor_from_request(self.request)
        )
        return queryset.prefetch_related(
            Prefetch("tracks", queryset=tracks), TAG_PREFETCH
        )

    libraries = get_libraries(lambda o, uploads: uploads.filter(track__album=o))

    def get_serializer_class(self):
        if self.action in ["create"]:
            return serializers.AlbumCreateSerializer
        return super().get_serializer_class()

    @transaction.atomic
    def perform_destroy(self, instance):
        routes.outbox.dispatch(
            {"type": "Delete", "object": {"type": "Album"}},
            context={"album": instance},
        )
        models.Album.objects.filter(pk=instance.pk).delete()

    @transaction.atomic
    def create(self, request, *args, **kwargs):
        request_data = request.data.copy()

        if mbid := request_data.get("musicbrainz_albumid", None) or request_data.get(
            "mbid", None
        ):
            artist_credit = tasks.get_or_create_artists_credits_from_musicbrainz(
                "release",
                mbid,
                attributed_to=request.user.actor,
                from_activity_id=None,
            )
        else:
            artist_data = request_data.pop("artist", False)
            artist_credit_data = request_data.pop("artist_credit", False)
            if not artist_data and not artist_credit_data:
                return Response({}, status=400)
            if artist_data:
                try:
                    artist = models.Artist.objects.get(pk=artist_data)
                except models.Artist.DoesNotExist:
                    return Response({"détail": "artist id not found"}, status=400)

                artist_credit, created = models.ArtistCredit.objects.get_or_create(
                    **{
                        "artist": artist,
                        "credit": artist.name,
                        "joinphrase": "",
                        "index": 0,
                    }
                )
            elif artist_credit_data:
                try:
                    artist_credit = models.ArtistCredit.objects.get(
                        pk=artist_credit_data
                    )
                except models.ArtistCredit.DoesNotExist:
                    return Response(
                        {"détail": "artist_credit id not found"}, status=400
                    )

        request_data["artist_credit"] = [artist_credit.pk]
        serializer = self.get_serializer(data=request_data)
        serializer.is_valid(raise_exception=True)
        self.perform_create(serializer)
        return Response(serializer.data, status=204)


class LibraryViewSet(
    mixins.ListModelMixin,
    mixins.RetrieveModelMixin,
    viewsets.GenericViewSet,
):
    lookup_field = "uuid"
    queryset = (
        models.Library.objects.all()
        .filter(channel=None)
        .select_related("actor")
        .order_by("-creation_date")
        .annotate(_uploads_count=Count("uploads"))
        .annotate(_size=Sum("uploads__size"))
    )
    serializer_class = serializers.LibraryForOwnerSerializer
    permission_classes = [
        oauth_permissions.ScopePermission,
        common_permissions.OwnerPermission,
    ]
    filterset_class = filters.LibraryFilter
    required_scope = "libraries"
    anonymous_policy = "setting"
    owner_field = "actor.user"
    owner_checks = ["write"]

    def get_queryset(self):
        qs = super().get_queryset()
        # allow retrieving a single library by uuid if request.user isn't
        # the owner. Any other get should be from the owner only
        if self.action not in ["retrieve", "list"]:
            qs = qs.filter(actor=self.request.user.actor)
        if self.action == "list":
            actor = utils.get_actor_from_request(self.request)
            qs = qs.viewable_by(actor)

        return qs

    # TODO quickfix, basically specifying the response would be None
    @extend_schema(responses=None)
    @action(
        methods=["get", "post", "delete"],
        detail=False,
        url_name="fs-import",
        url_path="fs-import",
    )
    @transaction.non_atomic_requests
    def fs_import(self, request, *args, **kwargs):
        if not request.user.is_authenticated:
            return Response({}, status=403)
        if not request.user.all_permissions["library"]:
            return Response({}, status=403)
        if request.method == "GET":
            path = request.GET.get("path", "")
            data = {
                "root": settings.MUSIC_DIRECTORY_PATH,
                "path": path,
                "import": None,
            }
            status = cache.get("fs-import:status", default=None)
            if status:
                data["import"] = {
                    "status": status,
                    "reference": cache.get("fs-import:reference"),
                    "logs": cache.get("fs-import:logs", default=[]),
                }
            try:
                data["content"] = utils.browse_dir(data["root"], data["path"])
            except (NotADirectoryError, ValueError, FileNotFoundError) as e:
                return Response({"detail": str(e)}, status=400)

            return Response(data)
        if request.method == "POST":
            if cache.get("fs-import:status", default=None) in [
                "pending",
                "started",
            ]:
                return Response({"detail": "An import is already running"}, status=400)

            data = request.data
            serializer = serializers.FSImportSerializer(
                data=data, context={"user": request.user}
            )
            serializer.is_valid(raise_exception=True)
            cache.set("fs-import:status", "pending")
            cache.set(
                "fs-import:reference", serializer.validated_data["import_reference"]
            )
            tasks.fs_import.delay(
                library_id=serializer.validated_data["library"].pk,
                path=serializer.validated_data["path"],
                import_reference=serializer.validated_data["import_reference"],
                prune=serializer.validated_data["prune"],
                outbox=serializer.validated_data["outbox"],
                broadcast=serializer.validated_data["broadcast"],
                batch_size=serializer.validated_data["batch_size"],
                verbosity=serializer.validated_data["verbosity"],
            )
            return Response(status=201)
        if request.method == "DELETE":
            cache.set("fs-import:status", "canceled")
            return Response(status=204)


class TrackViewSet(
    HandleInvalidSearch,
    common_views.SkipFilterForGetObject,
    mixins.DestroyModelMixin,
    viewsets.ReadOnlyModelViewSet,
):
    """
    A simple ViewSet for viewing and editing accounts.
    """

    queryset = (
        models.Track.objects.all()
        .for_nested_serialization()
        .prefetch_related("attributed_to", "attachment_cover")
        .order_by("-creation_date")
    ).distinct()
    serializer_class = serializers.TrackSerializer
    permission_classes = [oauth_permissions.ScopePermission]
    required_scope = "libraries"
    anonymous_policy = "setting"
    filterset_class = filters.TrackFilter
    fetches = federation_decorators.fetches_route()
    mutations = common_decorators.mutations_route(types=["update"])

    def get_object(self):
        obj = super().get_object()

        if (
            self.action == "retrieve"
            and self.request.GET.get("refresh", "").lower() == "true"
        ):
            obj = refetch_obj(obj, self.get_queryset())
        return obj

    def get_queryset(self):
        queryset = super().get_queryset()
        if self.action in ["destroy"]:
            queryset = queryset.exclude(artist_credit__artist__channel=None).filter(
                artist_credit__artist__attributed_to=self.request.user.actor
            )
        filter_favorites = self.request.GET.get("favorites", None)
        user = self.request.user
        if user.is_authenticated and filter_favorites == "true":
            queryset = queryset.filter(track_favorites__user=user)

        queryset = queryset.with_playable_uploads(
            utils.get_actor_from_request(self.request)
        )
        return queryset.prefetch_related(TAG_PREFETCH)

    libraries = get_libraries(lambda o, uploads: uploads.filter(track=o))

    def get_serializer_context(self):
        context = super().get_serializer_context()
        context["description"] = self.action in ["retrieve", "create", "update"]
        return context

    @transaction.atomic
    def perform_destroy(self, instance):
        uploads = instance.uploads.order_by("id")
        routes.outbox.dispatch(
            {"type": "Delete", "object": {"type": "Audio"}},
            context={"uploads": list(uploads)},
        )
        instance.delete()


def strip_absolute_media_url(path):
    if (
        settings.MEDIA_URL.startswith("http://")
        or settings.MEDIA_URL.startswith("https://")
        and path.startswith(settings.MEDIA_URL)
    ):
        path = path.replace(settings.MEDIA_URL, "/media/", 1)
    return path


def get_file_path(audio_file):
    serve_path = settings.MUSIC_DIRECTORY_SERVE_PATH
    prefix = settings.MUSIC_DIRECTORY_PATH
    t = settings.REVERSE_PROXY_TYPE
    if t == "nginx":
        # we have to use the internal locations
        try:
            path = audio_file.url
        except AttributeError:
            # a path was given
            if not serve_path or not prefix:
                raise ValueError(
                    "You need to specify MUSIC_DIRECTORY_SERVE_PATH and "
                    "MUSIC_DIRECTORY_PATH to serve in-place imported files"
                )
            path = "/music" + audio_file.replace(prefix, "", 1)
        path = strip_absolute_media_url(path)
        if path.startswith("http://") or path.startswith("https://"):
            protocol, remainder = path.split("://", 1)
            hostname, r_path = remainder.split("/", 1)
            r_path = urllib.parse.quote(r_path)
            path = protocol + "://" + hostname + "/" + r_path
            return (settings.PROTECT_FILES_PATH + "/media/" + path).encode("utf-8")
        # needed to serve files with % or ? chars
        path = urllib.parse.quote(path)
        return (settings.PROTECT_FILES_PATH + path).encode("utf-8")
    if t == "apache2":
        try:
            path = audio_file.path
        except AttributeError:
            # a path was given
            if not serve_path or not prefix:
                raise ValueError(
                    "You need to specify MUSIC_DIRECTORY_SERVE_PATH and "
                    "MUSIC_DIRECTORY_PATH to serve in-place imported files"
                )
            path = audio_file.replace(prefix, serve_path, 1)
        path = strip_absolute_media_url(path)
        return path.encode("utf-8")


def should_transcode(upload, format, max_bitrate=None):
    if not preferences.get("music__transcoding_enabled"):
        return False
    format_need_transcoding = True
    bitrate_need_transcoding = True
    if format is None:
        format_need_transcoding = False
    elif format not in utils.EXTENSION_TO_MIMETYPE:
        # format should match supported formats
        format_need_transcoding = False
    elif upload.mimetype is None:
        # upload should have a mimetype, otherwise we cannot transcode
        format_need_transcoding = False
    elif upload.mimetype == utils.EXTENSION_TO_MIMETYPE[format]:
        # requested format should be different than upload mimetype, otherwise
        # there is no need to transcode
        format_need_transcoding = False

    if max_bitrate is None:
        bitrate_need_transcoding = False
    elif not upload.bitrate:
        bitrate_need_transcoding = False
    elif upload.bitrate <= max_bitrate:
        bitrate_need_transcoding = False

    return format_need_transcoding or bitrate_need_transcoding


def get_content_disposition(filename):
    filename = f"filename*=UTF-8''{urllib.parse.quote(filename)}"
    return f"attachment; {filename}"


def record_downloads(f):
    def inner(*args, **kwargs):
        user = kwargs.get("user")
        wsgi_request = kwargs.pop("wsgi_request")
        upload = kwargs.get("upload")
        response = f(*args, **kwargs)
        if response.status_code >= 200 and response.status_code < 400:
            utils.increment_downloads_count(
                upload=upload, user=user, wsgi_request=wsgi_request
            )

        return response

    return inner


@record_downloads
def handle_serve(
    upload, user, format=None, max_bitrate=None, proxy_media=True, download=True
):
    f = upload
    # we update the accessed_date
    now = timezone.now()
    upload.accessed_date = now
    upload.save(update_fields=["accessed_date"])
    f = upload
    if f.audio_file:
        file_path = get_file_path(f.audio_file)

    elif f.source and (
        f.source.startswith("http://") or f.source.startswith("https://")
    ):
        # we need to populate from cache
        with transaction.atomic():
            # why the transaction/select_for_update?
            # this is because browsers may send multiple requests
            # in a short time range, for partial content,
            # thus resulting in multiple downloads from the remote
            qs = f.__class__.objects.select_for_update()
            f = qs.get(pk=f.pk)
            if user.is_authenticated:
                actor = user.actor
            else:
                actor = actors.get_service_actor()
            try:
                f.download_audio_from_remote(actor=actor)
            except requests.exceptions.RequestException:
                return Response({"detail": "Remote track is unavailable"}, status=503)
        data = f.get_audio_data()
        if data:
            f.duration = data["duration"]
            f.size = data["size"]
            f.bitrate = data["bitrate"]
            f.save(update_fields=["bitrate", "duration", "size"])
        file_path = get_file_path(f.audio_file)
    elif f.source and f.source.startswith("file://"):
        file_path = get_file_path(f.source.replace("file://", "", 1))
    mt = f.mimetype

    if should_transcode(f, format, max_bitrate=max_bitrate):
        transcoded_version = f.get_transcoded_version(format, max_bitrate=max_bitrate)
        transcoded_version.accessed_date = now
        transcoded_version.save(update_fields=["accessed_date"])
        f = transcoded_version
        file_path = get_file_path(f.audio_file)
        mt = f.mimetype
    if not proxy_media and f.audio_file:
        # we simply issue a 302 redirect to the real URL
        response = Response(status=302)
        response["Location"] = f.audio_file.url
        return response
    if mt:
        response = Response(content_type=mt)
    else:
        response = Response()
    filename = f.filename
    mapping = {"nginx": "X-Accel-Redirect", "apache2": "X-Sendfile"}
    file_header = mapping[settings.REVERSE_PROXY_TYPE]
    response[file_header] = file_path
    if download:
        response["Content-Disposition"] = get_content_disposition(filename)
    if mt:
        response["Content-Type"] = mt

    return response


class ListenMixin(mixins.RetrieveModelMixin, viewsets.GenericViewSet):
    queryset = models.Track.objects.all()
    serializer_class = serializers.TrackSerializer
    authentication_classes = (
        rest_settings.api_settings.DEFAULT_AUTHENTICATION_CLASSES
        + [SignatureAuthentication, ScopedTokenAuthentication]
    )
    permission_classes = [oauth_permissions.ScopePermission]
    required_scope = "libraries"
    anonymous_policy = "setting"
    lookup_field = "uuid"

    @extend_schema(responses=bytes)
    def retrieve(self, request, *args, **kwargs):
        config = {
            "explicit_file": request.GET.get("upload"),
            "download": request.GET.get("download", "true").lower() == "true",
            "format": request.GET.get("to"),
            "max_bitrate": request.GET.get("max_bitrate"),
        }
        track = self.get_object()
        return handle_stream(track, request, **config)


def handle_stream(track, request, download, explicit_file, format, max_bitrate):
    actor = utils.get_actor_from_request(request)
    queryset = track.uploads.prefetch_related(
        "track__album__artist_credit__artist", "track__artist_credit"
    )
    if explicit_file:
        queryset = queryset.filter(uuid=explicit_file)
    queryset = queryset.playable_by(actor)
    queryset = queryset.order_by(F("audio_file").desc(nulls_last=True))
    upload = queryset.first()
    if not upload:
        return Response(status=404)

    try:
        max_bitrate = min(max(int(max_bitrate), 0), 320) or None
    except (TypeError, ValueError):
        max_bitrate = None

    if max_bitrate:
        max_bitrate = max_bitrate * 1000
    return handle_serve(
        upload=upload,
        user=request.user,
        format=format,
        max_bitrate=max_bitrate,
        proxy_media=settings.PROXY_MEDIA,
        download=download,
        wsgi_request=request._request,
    )


class AudioRenderer(renderers.JSONRenderer):
    media_type = "audio/*"


@extend_schema_view(get=extend_schema(operation_id="get_track_file"))
class ListenViewSet(ListenMixin):
    renderer_classes = [AudioRenderer]


class MP3Renderer(renderers.JSONRenderer):
    format = "mp3"
    media_type = "audio/mpeg"


class StreamViewSet(ListenMixin):
    renderer_classes = [MP3Renderer]

    @extend_schema(operation_id="get_track_stream", responses=bytes)
    def retrieve(self, request, *args, **kwargs):
        config = {
            "explicit_file": None,
            "download": False,
            "format": "mp3",
            "max_bitrate": None,
        }
        track = self.get_object()
        return handle_stream(track, request, **config)


class UploadViewSet(
    mixins.ListModelMixin,
    mixins.CreateModelMixin,
    mixins.RetrieveModelMixin,
    mixins.UpdateModelMixin,
    mixins.DestroyModelMixin,
    viewsets.GenericViewSet,
):
    lookup_field = "uuid"
    queryset = (
        models.Upload.objects.all()
        .order_by("-creation_date")
        .prefetch_related(
            "library__actor",
            "track__artist_credit",
            "track__album__artist_credit",
            "track__attachment_cover",
        )
    )
    serializer_class = serializers.UploadForOwnerSerializer
    permission_classes = [
        oauth_permissions.ScopePermission,
        common_permissions.OwnerPermission,
    ]
    required_scope = "libraries"
    anonymous_policy = "setting"
    owner_field = "library.actor.user"
    owner_checks = ["write"]
    filterset_class = filters.UploadFilter
    ordering_fields = (
        "creation_date",
        "import_date",
        "bitrate",
        "size",
        "artist_credit__artist__name",
    )

    def get_queryset(self):
        qs = super().get_queryset()
        if self.action in ["update", "partial_update"]:
            # prevent updating an upload that is already processed
            qs = qs.filter(import_status="draft")
        if self.action != "retrieve":
            qs = qs.filter(library__actor=self.request.user.actor)
        else:
            actor = utils.get_actor_from_request(self.request)
            qs = qs.playable_by(actor)
        return qs

    @extend_schema(
        responses=tasks.metadata.TrackMetadataSerializer(),
        operation_id="get_upload_metadata",
    )
    @action(methods=["get"], detail=True, url_path="audio-file-metadata")
    def audio_file_metadata(self, request, *args, **kwargs):
        upload = self.get_object()
        try:
            m = tasks.metadata.Metadata(upload.get_audio_file())
        except FileNotFoundError:
            return Response({"detail": "File not found"}, status=500)
        serializer = tasks.metadata.TrackMetadataSerializer(
            data=m, context={"strict": False}
        )
        if not serializer.is_valid():
            return Response(serializer.errors, status=500)
        payload = serializer.validated_data
        cover_data = payload.get(
            "cover_data", payload.get("album", {}).get("cover_data", {})
        )
        if cover_data and "content" in cover_data:
            cover_data["content"] = base64.b64encode(cover_data["content"])
        return Response(payload, status=200)

    @action(methods=["post"], detail=False)
    def perform_upload_action(self, request, *args, **kwargs):
        queryset = self.get_queryset()
        serializer = serializers.UploadActionSerializer(request.data, queryset=queryset)
        serializer.is_valid(raise_exception=True)
        result = serializer.save()
        return Response(result, status=200)

    def get_serializer_context(self):
        context = super().get_serializer_context()
        context["user"] = self.request.user
        return context

    def perform_create(self, serializer):
        group_name = serializer.validated_data.get("import_reference") or str(
            datetime.datetime.date(datetime.datetime.now())
        )
        upload_group, _ = models.UploadGroup.objects.get_or_create(
            name=group_name, owner=self.request.user.actor
        )
        upload = serializer.save(upload_group=upload_group)
        if upload.import_status == "pending":
            common_utils.on_commit(tasks.process_upload.delay, upload_id=upload.pk)

    def perform_update(self, serializer):
        upload = serializer.save()
        if upload.import_status == "pending":
            common_utils.on_commit(tasks.process_upload.delay, upload_id=upload.pk)

    @transaction.atomic
    def perform_destroy(self, instance):
        routes.outbox.dispatch(
            {"type": "Delete", "object": {"type": "Audio"}},
            context={"uploads": [instance]},
        )
        instance.delete()

    @action(detail=False, methods=["patch"])
    def bulk_update(self, request, *args, **kwargs):
        """
        Used to move an upload from one library to another. Receive a upload uuid and a privacy_level
        """
        serializer = serializers.UploadBulkUpdateSerializer(
            data=request.data, many=True
        )
        serializer.is_valid(raise_exception=True)

        models.Upload.objects.bulk_update(serializer.validated_data, ["library"])

        return Response(
            serializers.UploadForOwnerSerializer(serializer.validated_data).data,
            status=200,
        )


class Search(views.APIView):
    max_results = 3
    permission_classes = [oauth_permissions.ScopePermission]
    required_scope = "libraries"
    anonymous_policy = "setting"

    @extend_schema(
        operation_id="get_search_results", responses=serializers.SearchResultSerializer
    )
    def get(self, request, *args, **kwargs):
        query = request.GET.get("query", request.GET.get("q", "")) or ""
        query = query.strip()
        if not query:
            return Response({"detail": "empty query"}, status=400)
        try:
            results = {
                "artists": self.get_artists(query),
                "tracks": self.get_tracks(query),
                "albums": self.get_albums(query),
                "tags": self.get_tags(query),
            }
        except django.db.utils.ProgrammingError as e:
            if "in tsquery:" in str(e):
                return Response({"detail": "Invalid query"}, status=400)
            else:
                raise

        return Response(serializers.SearchResultSerializer(results).data, status=200)

    def get_tracks(self, query):
        query_obj = utils.get_fts_query(
            query,
            fts_fields=[
                "body_text",
                "album__body_text",
                "artist_credit__artist__body_text",
            ],
            model=models.Track,
        )
        qs = (
            models.Track.objects.all()
            .filter(query_obj)
            .prefetch_related(
                "artist_credit",
                "attributed_to",
                Prefetch(
                    "album",
                    queryset=models.Album.objects.select_related(
                        "attachment_cover", "attributed_to"
                    ).prefetch_related("tracks", "artist_credit"),
                ),
            )
        )
        return common_utils.order_for_search(qs, "title")[: self.max_results]

    def get_albums(self, query):
        query_obj = utils.get_fts_query(
            query,
            fts_fields=["body_text", "artist_credit__artist__body_text"],
            model=models.Album,
        )
        qs = (
            models.Album.objects.all()
            .filter(query_obj)
            .select_related("attachment_cover", "attributed_to")
            .prefetch_related("tracks__artist_credit", "artist_credit")
        )
        return common_utils.order_for_search(qs, "title")[: self.max_results]

    def get_artists(self, query):
        query_obj = utils.get_fts_query(query, model=models.Artist)
        qs = (
            models.Artist.objects.all()
            .filter(query_obj)
            .with_albums()
            .prefetch_related("channel__actor")
            .select_related("attributed_to")
        )
        return common_utils.order_for_search(qs, "name")[: self.max_results]

    def get_tags(self, query):
        search_fields = ["tag_deterministic"]
        query_obj = utils.get_query(query, search_fields)
        qs = (
            Tag.objects.all()
            .annotate(tag_deterministic=Collate("name", "und-x-icu"))
            .filter(query_obj)
        )
        return common_utils.order_for_search(qs, "name")[: self.max_results]


class LicenseViewSet(viewsets.ReadOnlyModelViewSet):
    permission_classes = [oauth_permissions.ScopePermission]
    required_scope = "libraries"
    anonymous_policy = "setting"
    serializer_class = serializers.LicenseSerializer
    queryset = models.License.objects.all().order_by("code")
    lookup_value_regex = ".*"
    max_page_size = 1000

    def get_queryset(self):
        # ensure our licenses are up to date in DB
        licenses.load(licenses.LICENSES)
        return super().get_queryset()

    def get_serializer(self, *args, **kwargs):
        if len(args) == 0:
            return super().get_serializer(*args, **kwargs)

        # our serializer works with license dict, not License instances
        # so we pass those instead
        instance_or_qs = args[0]
        try:
            first_arg = instance_or_qs.conf
        except AttributeError:
            first_arg = [i.conf for i in instance_or_qs if i.conf]
        return super().get_serializer(*((first_arg,) + args[1:]), **kwargs)


class OembedView(views.APIView):
    permission_classes = [oauth_permissions.ScopePermission]
    required_scope = "libraries"
    anonymous_policy = "setting"
    serializer_class = serializers.OembedSerializer

    def get(self, request, *args, **kwargs):
        serializer = serializers.OembedSerializer(data=request.GET)
        serializer.is_valid(raise_exception=True)
        embed_data = serializer.save()
        return Response(embed_data)


class UploadGroupViewSet(viewsets.ModelViewSet):
    permission_classes = [oauth_permissions.ScopePermission]
    required_scope = "libraries"
    serializer_class = serializers.UploadGroupSerializer

    def get_queryset(self):
        return models.UploadGroup.objects.filter(owner__user__id=self.request.user.id)

    def perform_create(self, serializer):
        serializer.save(owner=self.request.user.actor)

    @action(
        detail=True,
        methods=["post"],
        parser_classes=(MultiPartParser, FormParser),
        serializer_class=serializers.UploadGroupUploadSerializer,
    )
    def uploads(self, request, pk=None):
        print(request.data)
        serializer = self.get_serializer(data=request.data)
        if serializer.is_valid():
            upload_group = models.UploadGroup.objects.get(guid=pk)
            if upload_group.owner == request.user.actor:
                upload = serializer.save(upload_group=upload_group)
                common_utils.on_commit(tasks.process_upload.delay, upload_id=upload.pk)
                response = serializers.BaseUploadSerializer(upload).data
                return Response(response, status=200)
            else:
                return Response("You don't own this Upload Group", status=403)
        else:
            print(serializer.errors)

        return Response("Fehler", status=202)
