from django import http
from django.db import transaction
from django.db.models import Count, Prefetch, Q, Sum
from django.utils import timezone
from drf_spectacular.utils import extend_schema, extend_schema_view
from rest_framework import decorators, exceptions, mixins
from rest_framework import permissions as rest_permissions
from rest_framework import response, viewsets

from funkwhale_api.common import locales, permissions, preferences
from funkwhale_api.common import utils as common_utils
from funkwhale_api.common.mixins import MultipleLookupDetailMixin
from funkwhale_api.federation import actors
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 import utils as federation_utils
from funkwhale_api.music import models as music_models
from funkwhale_api.music import views as music_views
from funkwhale_api.users.oauth import permissions as oauth_permissions

from . import categories, filters, models, renderers, serializers

ARTIST_PREFETCH_QS = (
    music_models.Artist.objects.select_related(
        "description",
        "attachment_cover",
    )
    .prefetch_related(music_views.TAG_PREFETCH)
    .annotate(_tracks_count=Count("artist_credit__tracks"))
)


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


@extend_schema_view(
    metedata_choices=extend_schema(operation_id="get_channel_metadata_choices"),
    subscribe=extend_schema(operation_id="subscribe_channel"),
    unsubscribe=extend_schema(operation_id="unsubscribe_channel"),
    rss_subscribe=extend_schema(operation_id="subscribe_channel_rss"),
)
class ChannelViewSet(
    ChannelsMixin,
    MultipleLookupDetailMixin,
    mixins.CreateModelMixin,
    mixins.RetrieveModelMixin,
    mixins.UpdateModelMixin,
    mixins.ListModelMixin,
    mixins.DestroyModelMixin,
    viewsets.GenericViewSet,
):
    url_lookups = [
        {
            "lookup_field": "uuid",
            "validator": serializers.serializers.UUIDField().to_internal_value,
        },
        {
            "lookup_field": "username",
            "validator": federation_utils.get_actor_data_from_username,
            "get_query": lambda v: Q(
                actor__domain=v["domain"],
                actor__preferred_username__iexact=v["username"],
            ),
        },
    ]
    filterset_class = filters.ChannelFilter
    serializer_class = serializers.ChannelSerializer
    queryset = (
        models.Channel.objects.all()
        .prefetch_related(
            "library",
            "attributed_to",
            "actor",
            Prefetch("artist", queryset=ARTIST_PREFETCH_QS),
        )
        .order_by("-creation_date")
    )
    permission_classes = [
        oauth_permissions.ScopePermission,
        permissions.OwnerPermission,
    ]
    required_scope = "libraries"
    anonymous_policy = "setting"
    owner_checks = ["write"]
    owner_field = "attributed_to.user"
    owner_exception = exceptions.PermissionDenied

    def get_serializer_class(self):
        if self.request.method.lower() in ["head", "get", "options"]:
            return serializers.ChannelSerializer
        elif self.action in ["update", "partial_update"]:
            return serializers.ChannelUpdateSerializer
        elif self.action == "create":
            return serializers.ChannelCreateSerializer
        return serializers.ChannelSerializer

    def get_queryset(self):
        queryset = super().get_queryset()
        if self.action == "retrieve":
            queryset = queryset.annotate(
                _downloads_count=Sum("artist__artist_credit__tracks__downloads_count")
            )
        return queryset

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

    def list(self, request, *args, **kwargs):
        if self.request.GET.get("output") == "opml":
            queryset = self.filter_queryset(self.get_queryset())[:500]
            opml = serializers.get_opml(
                channels=queryset,
                date=timezone.now(),
                title="Funkwhale channels OPML export",
            )
            xml_body = renderers.render_xml(renderers.dict_to_xml_tree("opml", opml))
            return http.HttpResponse(xml_body, content_type="application/xml")

        else:
            return super().list(request, *args, **kwargs)

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

    @decorators.action(
        detail=True,
        methods=["post"],
        permission_classes=[rest_permissions.IsAuthenticated],
        serializer_class=serializers.SubscriptionSerializer,
    )
    def subscribe(self, request, *args, **kwargs):
        object = self.get_object()
        subscription = federation_models.Follow(actor=request.user.actor)
        subscription.fid = subscription.get_federation_id()
        subscription, created = SubscriptionsViewSet.queryset.get_or_create(
            target=object.actor,
            actor=request.user.actor,
            defaults={
                "approved": True,
                "fid": subscription.fid,
                "uuid": subscription.uuid,
            },
        )
        # prefetch stuff
        subscription = SubscriptionsViewSet.queryset.get(pk=subscription.pk)
        if not object.actor.is_local:
            routes.outbox.dispatch({"type": "Follow"}, context={"follow": subscription})

        data = serializers.SubscriptionSerializer(subscription).data
        return response.Response(data, status=201)

    @extend_schema(responses={204: None})
    @decorators.action(
        detail=True,
        methods=["post", "delete"],
        permission_classes=[rest_permissions.IsAuthenticated],
    )
    def unsubscribe(self, request, *args, **kwargs):
        object = self.get_object()
        follow_qs = request.user.actor.emitted_follows.filter(target=object.actor)
        follow = follow_qs.first()
        if follow:
            if not object.actor.is_local:
                routes.outbox.dispatch(
                    {"type": "Undo", "object": {"type": "Follow"}},
                    context={"follow": follow},
                )
            follow_qs.delete()
        return response.Response(status=204)

    @decorators.action(
        detail=True,
        methods=["get"],
        content_negotiation_class=renderers.PodcastRSSContentNegociation,
    )
    def rss(self, request, *args, **kwargs):
        object = self.get_object()
        if not object.attributed_to.is_local:
            return response.Response({"detail": "Not found"}, status=404)

        if object.attributed_to == actors.get_service_actor():
            # external feed, we redirect to the canonical one
            return http.HttpResponseRedirect(object.rss_url)
        uploads = (
            object.library.uploads.playable_by(None)
            .prefetch_related(
                Prefetch(
                    "track",
                    queryset=music_models.Track.objects.select_related(
                        "attachment_cover", "description"
                    ).prefetch_related(
                        music_views.TAG_PREFETCH,
                    ),
                ),
            )
            .select_related("track__attachment_cover", "track__description")
            .order_by("-creation_date")
        )[:50]
        data = serializers.rss_serialize_channel_full(channel=object, uploads=uploads)
        return response.Response(data, status=200)

    @decorators.action(
        methods=["get"],
        detail=False,
        url_path="metadata-choices",
        url_name="metadata_choices",
        permission_classes=[],
    )
    def metedata_choices(self, request, *args, **kwargs):
        data = {
            "language": [
                {"value": code, "label": name} for code, name in locales.ISO_639_CHOICES
            ],
            "itunes_category": [
                {"value": code, "label": code, "children": children}
                for code, children in categories.ITUNES_CATEGORIES.items()
            ],
        }
        return response.Response(data)

    @decorators.action(
        methods=["post"],
        detail=False,
        url_path="rss-subscribe",
        url_name="rss_subscribe",
    )
    @transaction.atomic
    def rss_subscribe(self, request, *args, **kwargs):
        serializer = serializers.RssSubscribeSerializer(data=request.data)
        if not serializer.is_valid():
            return response.Response(serializer.errors, status=400)
        channel = (
            models.Channel.objects.filter(
                rss_url=serializer.validated_data["url"],
            )
            .order_by("id")
            .first()
        )
        if not channel:
            # try to retrieve the channel via its URL and create it
            try:
                channel, uploads = serializers.get_channel_from_rss_url(
                    serializer.validated_data["url"]
                )
            except serializers.FeedFetchException as e:
                return response.Response(
                    {"detail": str(e)},
                    status=400,
                )

        subscription = federation_models.Follow(actor=request.user.actor)
        subscription.fid = subscription.get_federation_id()
        subscription, created = SubscriptionsViewSet.queryset.get_or_create(
            target=channel.actor,
            actor=request.user.actor,
            defaults={
                "approved": True,
                "fid": subscription.fid,
                "uuid": subscription.uuid,
            },
        )
        # prefetch stuff
        subscription = SubscriptionsViewSet.queryset.get(pk=subscription.pk)

        return response.Response(
            serializers.SubscriptionSerializer(subscription).data, status=201
        )

    def get_serializer_context(self):
        context = super().get_serializer_context()
        context["subscriptions_count"] = self.action in [
            "retrieve",
            "create",
            "update",
            "partial_update",
        ]
        if self.request.user.is_authenticated:
            context["actor"] = self.request.user.actor
        return context

    @transaction.atomic
    def perform_destroy(self, instance):
        instance.__class__.objects.filter(pk=instance.pk).delete()
        common_utils.on_commit(
            federation_tasks.remove_actor.delay, actor_id=instance.actor.pk
        )


class SubscriptionsViewSet(
    ChannelsMixin,
    mixins.RetrieveModelMixin,
    mixins.ListModelMixin,
    viewsets.GenericViewSet,
):
    lookup_field = "uuid"
    serializer_class = serializers.SubscriptionSerializer
    queryset = (
        federation_models.Follow.objects.exclude(target__channel__isnull=True)
        .prefetch_related(
            "target__channel__library",
            "target__channel__attributed_to",
            "actor",
            Prefetch("target__channel__artist", queryset=ARTIST_PREFETCH_QS),
        )
        .order_by("-creation_date")
    )
    permission_classes = [
        oauth_permissions.ScopePermission,
        rest_permissions.IsAuthenticated,
    ]
    required_scope = "libraries"
    anonymous_policy = False

    def get_queryset(self):
        qs = super().get_queryset()
        return qs.filter(actor=self.request.user.actor)

    @extend_schema(
        responses=serializers.AllSubscriptionsSerializer(),
        operation_id="get_all_subscriptions",
    )
    @decorators.action(methods=["get"], detail=False)
    def all(self, request, *args, **kwargs):
        """
        Return all the subscriptions of the current user, with only limited data
        to have a performant endpoint and avoid lots of queries just to display
        subscription status in the UI
        """
        subscriptions = self.get_queryset().values("uuid", "target__channel__uuid")

        payload = serializers.AllSubscriptionsSerializer(subscriptions).data
        return response.Response(payload, status=200)
