from django import forms
from django.conf import settings
from django.core import paginator
from django.db.models import Prefetch
from django.http import HttpResponse
from django.urls import reverse
from rest_framework import exceptions, mixins, permissions, response, viewsets
from rest_framework.decorators import action

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.favorites import models as favorites_models
from funkwhale_api.federation import utils as federation_utils
from funkwhale_api.history import models as history_models
from funkwhale_api.moderation import models as moderation_models
from funkwhale_api.music import models as music_models
from funkwhale_api.music import utils as music_utils
from funkwhale_api.playlists import models as playlists_models

from . import (
    activity,
    actors,
    authentication,
    models,
    renderers,
    serializers,
    utils,
    webfinger,
)


def redirect_to_html(public_url):
    response = HttpResponse(status=302)
    response["Location"] = common_utils.join_url(settings.FUNKWHALE_URL, public_url)
    return response


def get_collection_response(
    conf, querystring, collection_serializer, page_access_check=None
):
    page = querystring.get("page")
    if page is None:
        data = collection_serializer.data
    else:
        if page_access_check and not page_access_check():
            raise exceptions.AuthenticationFailed(
                "You do not have access to this resource"
            )
        try:
            page_number = int(page)
        except Exception:
            return response.Response({"page": ["Invalid page number"]}, status=400)
        conf["page_size"] = preferences.get("federation__collection_page_size")
        p = paginator.Paginator(conf["items"], conf["page_size"])
        try:
            page = p.page(page_number)
            conf["page"] = page
            serializer = serializers.CollectionPageSerializer(conf)
            data = serializer.data
        except paginator.EmptyPage:
            return response.Response(status=404)

    return response.Response(data)


class AuthenticatedIfAllowListEnabled(permissions.BasePermission):
    def has_permission(self, request, view):
        allow_list_enabled = preferences.get("moderation__allow_list_enabled")
        if not allow_list_enabled:
            return True
        return bool(request.actor)


class FederationMixin:
    permission_classes = [AuthenticatedIfAllowListEnabled]

    def dispatch(self, request, *args, **kwargs):
        if not preferences.get("federation__enabled"):
            return HttpResponse(status=405)
        return super().dispatch(request, *args, **kwargs)


class SharedViewSet(FederationMixin, viewsets.GenericViewSet):
    authentication_classes = [authentication.SignatureAuthentication]
    renderer_classes = renderers.get_ap_renderers()

    @action(
        methods=["post"],
        detail=False,
        content_negotiation_class=renderers.IgnoreClientContentNegotiation,
    )
    def inbox(self, request, *args, **kwargs):
        if request.method.lower() == "post" and request.actor is None:
            raise exceptions.AuthenticationFailed(
                "You need a valid signature to send an activity"
            )
        if request.method.lower() == "post":
            activity.receive(activity=request.data, on_behalf_of=request.actor)
        return response.Response({}, status=200)


class ActorViewSet(FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet):
    lookup_field = "preferred_username"
    authentication_classes = [authentication.SignatureAuthentication]
    renderer_classes = renderers.get_ap_renderers()
    queryset = (
        models.Actor.objects.local()
        .select_related("user", "channel__artist", "channel__attributed_to")
        .prefetch_related("channel__artist__tagged_items__tag")
    )
    serializer_class = serializers.ActorSerializer

    def get_queryset(self):
        queryset = super().get_queryset()
        return queryset.exclude(channel__attributed_to=actors.get_service_actor())

    def get_permissions(self):
        # cf #1999 it must be possible to fetch actors without being authenticated
        # otherwise we end up in a loop
        if self.action == "retrieve":
            return []
        return super().get_permissions()

    def retrieve(self, request, *args, **kwargs):
        instance = self.get_object()
        if utils.should_redirect_ap_to_html(request.headers.get("accept")):
            if instance.get_channel():
                return redirect_to_html(instance.channel.get_absolute_url())
            return redirect_to_html(instance.get_absolute_url())

        serializer = self.get_serializer(instance)
        return response.Response(serializer.data)

    @action(
        methods=["get", "post"],
        detail=True,
        content_negotiation_class=renderers.IgnoreClientContentNegotiation,
    )
    def inbox(self, request, *args, **kwargs):
        inbox_actor = self.get_object()
        if request.method.lower() == "post" and request.actor is None:
            raise exceptions.AuthenticationFailed(
                "You need a valid signature to send an activity"
            )
        if request.method.lower() == "post":
            activity.receive(
                activity=request.data,
                on_behalf_of=request.actor,
                inbox_actor=inbox_actor,
            )
        return response.Response({}, status=200)

    @action(methods=["get", "post"], detail=True)
    def outbox(self, request, *args, **kwargs):
        actor = self.get_object()
        channel = actor.get_channel()
        if channel:
            return self.get_channel_outbox_response(request, channel)
        return response.Response({}, status=200)

    def get_channel_outbox_response(self, request, channel):
        conf = {
            "id": channel.actor.outbox_url,
            "actor": channel.actor,
            "items": channel.library.uploads.for_federation()
            .order_by("-creation_date")
            .prefetch_related(
                "library__channel__actor", "track__artist_credit__artist"
            ),
            "item_serializer": serializers.ChannelCreateUploadSerializer,
        }
        return get_collection_response(
            conf=conf,
            querystring=request.GET,
            collection_serializer=serializers.ChannelOutboxSerializer(channel),
        )

    @action(
        methods=["get"],
        detail=True,
        permission_classes=[common_permissions.PrivacyLevelPermission],
    )
    def followers(self, request, *args, **kwargs):
        actor = self.get_object()
        followers = list(actor.get_approved_followers())
        conf = {
            "id": federation_utils.full_url(
                reverse(
                    "federation:actors-followers",
                    kwargs={"preferred_username": actor.preferred_username},
                )
            ),
            "items": followers,
            "item_serializer": serializers.ActorSerializer,
            "page_size": 100,
            "actor": None,
        }
        response = get_collection_response(
            conf=conf,
            querystring=request.GET,
            collection_serializer=serializers.IndexSerializer(conf),
        )
        return response

    @action(
        methods=["get"],
        detail=True,
        permission_classes=[common_permissions.PrivacyLevelPermission],
    )
    def following(self, request, *args, **kwargs):
        actor = self.get_object()
        followings = list(
            actor.emitted_follows.filter(approved=True).values_list("target", flat=True)
        )
        conf = {
            "id": federation_utils.full_url(
                reverse(
                    "federation:actors-following",
                    kwargs={"preferred_username": actor.preferred_username},
                )
            ),
            "items": followings,
            "item_serializer": serializers.ActorSerializer,
            "page_size": 100,
            "actor": None,
        }
        response = get_collection_response(
            conf=conf,
            querystring=request.GET,
            collection_serializer=serializers.IndexSerializer(conf),
        )
        return response

    @action(
        methods=["get"],
        detail=True,
        permission_classes=[common_permissions.PrivacyLevelPermission],
    )
    def listens(self, request, *args, **kwargs):
        actor = self.get_object()
        listenings = history_models.Listening.objects.filter(actor=actor)
        conf = {
            "id": federation_utils.full_url(
                reverse(
                    "federation:actors-listens",
                    kwargs={"preferred_username": actor.preferred_username},
                )
            ),
            "items": listenings,
            "item_serializer": serializers.ListeningSerializer,
            "page_size": 100,
            "actor": None,
        }
        response = get_collection_response(
            conf=conf,
            querystring=request.GET,
            collection_serializer=serializers.IndexSerializer(conf),
        )
        return response

    @action(
        methods=["get"],
        detail=True,
        permission_classes=[common_permissions.PrivacyLevelPermission],
    )
    def likes(self, request, *args, **kwargs):
        actor = self.get_object()
        likes = favorites_models.TrackFavorite.objects.filter(actor=actor)
        conf = {
            "id": federation_utils.full_url(
                reverse(
                    "federation:actors-likes",
                    kwargs={"preferred_username": actor.preferred_username},
                )
            ),
            "items": likes,
            "item_serializer": serializers.TrackFavoriteSerializer,
            "page_size": 100,
            "actor": None,
        }
        response = get_collection_response(
            conf=conf,
            querystring=request.GET,
            collection_serializer=serializers.IndexSerializer(conf),
        )
        return response


class EditViewSet(FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet):
    lookup_field = "uuid"
    authentication_classes = [authentication.SignatureAuthentication]
    renderer_classes = renderers.get_ap_renderers()
    # queryset = common_models.Mutation.objects.local().select_related()
    # serializer_class = serializers.ActorSerializer


class ReportViewSet(
    FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
):
    lookup_field = "uuid"
    authentication_classes = [authentication.SignatureAuthentication]
    renderer_classes = renderers.get_ap_renderers()
    queryset = moderation_models.Report.objects.none()


class WellKnownViewSet(viewsets.GenericViewSet):
    authentication_classes = []
    permission_classes = []
    renderer_classes = [renderers.JSONRenderer, renderers.WebfingerRenderer]

    @action(methods=["get"], detail=False)
    def nodeinfo(self, request, *args, **kwargs):
        data = {
            "links": [
                {
                    "rel": "http://nodeinfo.diaspora.software/ns/schema/2.0",
                    "href": utils.full_url(reverse("api:v1:instance:nodeinfo-2.0")),
                }
            ]
        }
        return response.Response(data)

    @action(methods=["get"], detail=False)
    def webfinger(self, request, *args, **kwargs):
        if not preferences.get("federation__enabled"):
            return HttpResponse(status=405)
        try:
            resource_type, resource = webfinger.clean_resource(request.GET["resource"])
            cleaner = getattr(webfinger, f"clean_{resource_type}")
            result = cleaner(resource)
            handler = getattr(self, f"handler_{resource_type}")
            data = handler(result)
        except forms.ValidationError as e:
            return response.Response({"errors": {"resource": e.message}}, status=400)
        except KeyError:
            return response.Response(
                {"errors": {"resource": "This field is required"}}, status=400
            )

        return response.Response(data)

    def handler_acct(self, clean_result):
        username, hostname = clean_result

        try:
            actor = models.Actor.objects.local().get(preferred_username=username)
        except models.Actor.DoesNotExist:
            raise forms.ValidationError("Invalid username")

        return serializers.ActorWebfingerSerializer(actor).data


def has_library_access(request, library):
    if library.privacy_level == "everyone":
        return True
    if request.user.is_authenticated and request.user.is_superuser:
        return True

    try:
        actor = request.actor
    except AttributeError:
        return False

    return library.received_follows.filter(actor=actor, approved=True).exists()


class MusicLibraryViewSet(
    FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
):
    authentication_classes = [authentication.SignatureAuthentication]
    renderer_classes = renderers.get_ap_renderers()
    serializer_class = serializers.LibrarySerializer
    queryset = (
        music_models.Library.objects.all()
        .local()
        .select_related("actor")
        .filter(channel=None)
    )
    lookup_field = "uuid"

    def retrieve(self, request, *args, **kwargs):
        lb = self.get_object()
        if utils.should_redirect_ap_to_html(request.headers.get("accept")):
            return redirect_to_html(lb.get_absolute_url())
        conf = {
            "id": lb.get_federation_id(),
            "actor": lb.actor,
            "name": lb.name,
            "items": lb.uploads.for_federation()
            .order_by("-creation_date")
            .prefetch_related(
                Prefetch(
                    "track",
                    queryset=music_models.Track.objects.select_related(
                        "attachment_cover",
                        "album__attributed_to",
                        "attributed_to",
                        "album__attachment_cover",
                        "description",
                    ).prefetch_related(
                        "album__artist_credit__artist__attributed_to",
                        "artist_credit__artist__attributed_to",
                        "artist_credit__artist__attachment_cover",
                        "tagged_items__tag",
                        "album__tagged_items__tag",
                        "album__artist_credit__artist__tagged_items__tag",
                        "album__artist_credit__artist__attachment_cover",
                        "artist_credit__artist__tagged_items__tag",
                        "artist_credit__artist__description",
                        "album__description",
                    ),
                )
            ),
            "item_serializer": serializers.UploadSerializer,
        }

        return get_collection_response(
            conf=conf,
            querystring=request.GET,
            collection_serializer=serializers.LibrarySerializer(lb),
            page_access_check=lambda: has_library_access(request, lb),
        )

    @action(methods=["get"], detail=True)
    def followers(self, request, *args, **kwargs):
        self.get_object()
        # XXX Implement this
        return response.Response({})


class MusicUploadViewSet(
    FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
):
    authentication_classes = [authentication.SignatureAuthentication]
    renderer_classes = renderers.get_ap_renderers()
    queryset = (
        music_models.Upload.objects.local()
        .select_related(
            "library__actor",
            "track__description",
            "track__album__attachment_cover",
            "track__attachment_cover",
        )
        .prefetch_related(
            "track__artist_credit__artist",
            "track__album__artist_credit__artist",
            "track__album__artist_credit__artist__attachment_cover",
            "track__artist_credit__artist__attachment_cover",
        )
    )
    serializer_class = serializers.UploadSerializer
    lookup_field = "uuid"

    def retrieve(self, request, *args, **kwargs):
        instance = self.get_object()
        if utils.should_redirect_ap_to_html(request.headers.get("accept")):
            return redirect_to_html(instance.track.get_absolute_url())

        serializer = self.get_serializer(instance)
        return response.Response(serializer.data)

    def get_queryset(self):
        queryset = super().get_queryset()
        actor = music_utils.get_actor_from_request(self.request)
        return queryset.playable_by(actor)

    def get_serializer(self, obj):
        if obj.library.get_channel():
            return serializers.ChannelUploadSerializer(obj)
        return super().get_serializer(obj)

    @action(
        methods=["get"],
        detail=True,
        content_negotiation_class=renderers.IgnoreClientContentNegotiation,
    )
    def activity(self, request, *args, **kwargs):
        object = self.get_object()
        serializer = serializers.ChannelCreateUploadSerializer(object)
        return response.Response(serializer.data)


class MusicArtistViewSet(
    FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
):
    authentication_classes = [authentication.SignatureAuthentication]
    renderer_classes = renderers.get_ap_renderers()
    queryset = music_models.Artist.objects.local().select_related(
        "description", "attachment_cover"
    )
    serializer_class = serializers.ArtistSerializer
    lookup_field = "uuid"

    def retrieve(self, request, *args, **kwargs):
        instance = self.get_object()
        if utils.should_redirect_ap_to_html(request.headers.get("accept")):
            return redirect_to_html(instance.get_absolute_url())

        serializer = self.get_serializer(instance)
        return response.Response(serializer.data)


class MusicArtistCreditViewSet(
    FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
):
    authentication_classes = [authentication.SignatureAuthentication]
    renderer_classes = renderers.get_ap_renderers()
    queryset = music_models.ArtistCredit.objects.local().prefetch_related("artist")
    serializer_class = serializers.ArtistCreditSerializer
    lookup_field = "uuid"

    def retrieve(self, request, *args, **kwargs):
        instance = self.get_object()
        serializer = self.get_serializer(instance)
        return response.Response(serializer.data)


class MusicAlbumViewSet(
    FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
):
    authentication_classes = [authentication.SignatureAuthentication]
    renderer_classes = renderers.get_ap_renderers()
    queryset = (
        music_models.Album.objects.local()
        .prefetch_related(
            "artist_credit__artist__description",
            "artist_credit__artist__attachment_cover",
        )
        .select_related(
            "description",
        )
    )
    serializer_class = serializers.AlbumSerializer
    lookup_field = "uuid"

    def retrieve(self, request, *args, **kwargs):
        instance = self.get_object()
        if utils.should_redirect_ap_to_html(request.headers.get("accept")):
            return redirect_to_html(instance.get_absolute_url())

        serializer = self.get_serializer(instance)
        return response.Response(serializer.data)


class MusicTrackViewSet(
    FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
):
    authentication_classes = [authentication.SignatureAuthentication]
    renderer_classes = renderers.get_ap_renderers()
    queryset = (
        music_models.Track.objects.local()
        .select_related(
            "album__description",
            "description",
            "attachment_cover",
            "album__attachment_cover",
        )
        .prefetch_related(
            "album__artist_credit__artist",
            "artist_credit__artist__description",
            "artist_credit__artist__attachment_cover",
            "album__artist_credit__artist__attachment_cover",
        )
    )

    serializer_class = serializers.TrackSerializer
    lookup_field = "uuid"

    def retrieve(self, request, *args, **kwargs):
        instance = self.get_object()
        if utils.should_redirect_ap_to_html(request.headers.get("accept")):
            return redirect_to_html(instance.get_absolute_url())

        serializer = self.get_serializer(instance)
        return response.Response(serializer.data)


class ChannelViewSet(
    FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
):
    authentication_classes = [authentication.SignatureAuthentication]
    renderer_classes = renderers.get_ap_renderers()
    queryset = music_models.Artist.objects.local().select_related(
        "description", "attachment_cover"
    )
    serializer_class = serializers.ArtistSerializer
    lookup_field = "uuid"

    def retrieve(self, request, *args, **kwargs):
        instance = self.get_object()
        if utils.should_redirect_ap_to_html(request.headers.get("accept")):
            return redirect_to_html(instance.get_absolute_url())

        serializer = self.get_serializer(instance)
        return response.Response(serializer.data)


class IndexViewSet(FederationMixin, viewsets.GenericViewSet):
    authentication_classes = [authentication.SignatureAuthentication]
    renderer_classes = renderers.get_ap_renderers()

    def dispatch(self, request, *args, **kwargs):
        if not preferences.get("federation__public_index"):
            return HttpResponse(status=405)
        return super().dispatch(request, *args, **kwargs)

    @action(
        methods=["get"],
        detail=False,
    )
    def libraries(self, request, *args, **kwargs):
        libraries = (
            music_models.Library.objects.local()
            .filter(channel=None, privacy_level="everyone")
            .prefetch_related("actor")
            .order_by("creation_date")
        )
        conf = {
            "id": federation_utils.full_url(
                reverse("federation:index:index-libraries")
            ),
            "items": libraries,
            "item_serializer": serializers.LibrarySerializer,
            "page_size": 100,
            "actor": None,
        }
        return get_collection_response(
            conf=conf,
            querystring=request.GET,
            collection_serializer=serializers.IndexSerializer(conf),
        )

        return response.Response({}, status=200)

    @action(
        methods=["get"],
        detail=False,
    )
    def channels(self, request, *args, **kwargs):
        actors = (
            models.Actor.objects.local()
            .exclude(channel=None)
            .order_by("channel__creation_date")
            .prefetch_related(
                "channel__attributed_to",
                "channel__artist",
                "channel__artist__description",
                "channel__artist__attachment_cover",
            )
        )
        conf = {
            "id": federation_utils.full_url(reverse("federation:index:index-channels")),
            "items": actors,
            "item_serializer": serializers.ActorSerializer,
            "page_size": 100,
            "actor": None,
        }
        return get_collection_response(
            conf=conf,
            querystring=request.GET,
            collection_serializer=serializers.IndexSerializer(conf),
        )

        return response.Response({}, status=200)


class TrackFavoriteViewSet(
    FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
):
    authentication_classes = [authentication.SignatureAuthentication]
    permission_classes = [common_permissions.PrivacyLevelPermission]
    renderer_classes = renderers.get_ap_renderers()
    queryset = favorites_models.TrackFavorite.objects.local().select_related(
        "track", "actor"
    )
    serializer_class = serializers.TrackFavoriteSerializer
    lookup_field = "uuid"

    def retrieve(self, request, *args, **kwargs):
        instance = self.get_object()
        if utils.should_redirect_ap_to_html(request.headers.get("accept")):
            return redirect_to_html(instance.get_absolute_url())

        serializer = self.get_serializer(instance)
        return response.Response(serializer.data)


class ListeningsViewSet(
    FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
):
    authentication_classes = [authentication.SignatureAuthentication]
    permission_classes = [common_permissions.PrivacyLevelPermission]
    renderer_classes = renderers.get_ap_renderers()
    queryset = history_models.Listening.objects.local().select_related("track", "actor")
    serializer_class = serializers.ListeningSerializer
    lookup_field = "uuid"

    def retrieve(self, request, *args, **kwargs):
        instance = self.get_object()
        if utils.should_redirect_ap_to_html(request.headers.get("accept")):
            return redirect_to_html(instance.get_absolute_url())

        serializer = self.get_serializer(instance)
        return response.Response(serializer.data)


class PlaylistViewSet(
    FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
):
    authentication_classes = [authentication.SignatureAuthentication]
    permission_classes = [common_permissions.PrivacyLevelPermission]
    renderer_classes = renderers.get_ap_renderers()
    queryset = playlists_models.Playlist.objects.local().select_related("actor")
    serializer_class = serializers.PlaylistCollectionSerializer
    lookup_field = "uuid"

    def retrieve(self, request, *args, **kwargs):
        playlist = self.get_object()
        if utils.should_redirect_ap_to_html(request.headers.get("accept")):
            return redirect_to_html(playlist.get_absolute_url())

        conf = {
            "id": playlist.fid,
            "actor": playlist.actor,
            "name": playlist.name,
            "items": playlist.playlist_tracks.order_by("index").prefetch_related(
                "track",
            ),
            "item_serializer": serializers.PlaylistTrackSerializer,
        }
        return get_collection_response(
            conf=conf,
            querystring=request.GET,
            collection_serializer=serializers.PlaylistCollectionSerializer(playlist),
        )
