from django.db import transaction
from django.db.models import Count, OuterRef, Prefetch, Q, Subquery, Sum
from django.db.models.functions import Coalesce, Collate, Length
from django.shortcuts import get_object_or_404
from drf_spectacular.utils import extend_schema
from rest_framework import decorators as rest_decorators
from rest_framework import mixins, response, viewsets

from funkwhale_api.audio import models as audio_models
from funkwhale_api.common import decorators
from funkwhale_api.common import models as common_models
from funkwhale_api.common import preferences
from funkwhale_api.common import utils as common_utils
from funkwhale_api.common.mixins import MultipleLookupDetailMixin
from funkwhale_api.favorites import models as favorites_models
from funkwhale_api.federation import models as federation_models
from funkwhale_api.federation import tasks as federation_tasks
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.moderation import tasks as moderation_tasks
from funkwhale_api.music import models as music_models
from funkwhale_api.music import views as music_views
from funkwhale_api.playlists import models as playlists_models
from funkwhale_api.tags import models as tags_models
from funkwhale_api.users import models as users_models

from . import filters, serializers


def get_stats(tracks, target, ignore_fields=[]):
    tracks = list(tracks.values_list("pk", flat=True))
    uploads = music_models.Upload.objects.filter(track__in=tracks)
    fields = {
        "listenings": history_models.Listening.objects.filter(track__in=tracks),
        "mutations": common_models.Mutation.objects.get_for_target(target),
        "playlists": (
            playlists_models.PlaylistTrack.objects.filter(track__in=tracks)
            .values_list("playlist", flat=True)
            .distinct()
        ),
        "track_favorites": (
            favorites_models.TrackFavorite.objects.filter(track__in=tracks)
        ),
        "libraries": (
            uploads.filter(library__channel=None)
            .values_list("library", flat=True)
            .distinct()
        ),
        "channels": (
            uploads.exclude(library__channel=None)
            .values_list("library", flat=True)
            .distinct()
        ),
        "uploads": uploads,
        "reports": moderation_models.Report.objects.get_for_target(target),
    }
    data = {}
    for key, qs in fields.items():
        if key in ignore_fields:
            continue
        data[key] = qs.count()

    data.update(get_media_stats(uploads))
    return data


def get_media_stats(uploads):
    data = {}
    data["media_total_size"] = uploads.aggregate(v=Sum("size"))["v"] or 0
    data["media_downloaded_size"] = (
        uploads.with_file().aggregate(v=Sum("size"))["v"] or 0
    )
    return data


class ManageArtistViewSet(
    mixins.ListModelMixin,
    mixins.RetrieveModelMixin,
    mixins.DestroyModelMixin,
    viewsets.GenericViewSet,
):
    queryset = (
        music_models.Artist.objects.all()
        .order_by("-id")
        .select_related("attributed_to", "attachment_cover", "channel")
        .annotate(_tracks_count=Count("artist_credit__tracks", distinct=True))
        .annotate(_albums_count=Count("artist_credit__albums", distinct=True))
        .prefetch_related(music_views.TAG_PREFETCH)
    )
    serializer_class = serializers.ManageArtistSerializer
    filterset_class = filters.ManageArtistFilterSet
    required_scope = "instance:libraries"
    ordering_fields = ["creation_date", "name"]

    @extend_schema(operation_id="admin_get_library_artist_stats")
    @rest_decorators.action(methods=["get"], detail=True)
    def stats(self, request, *args, **kwargs):
        artist = self.get_object()
        tracks = music_models.Track.objects.filter(
            Q(artist_credit__artist=artist) | Q(album__artist_credit__artist=artist)
        )
        data = get_stats(tracks, artist)
        return response.Response(data, status=200)

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

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


class ManageAlbumViewSet(
    mixins.ListModelMixin,
    mixins.RetrieveModelMixin,
    mixins.DestroyModelMixin,
    viewsets.GenericViewSet,
):
    queryset = (
        music_models.Album.objects.all()
        .order_by("-id")
        .select_related("attributed_to", "attachment_cover")
        .prefetch_related("tracks", "artist_credit__artist")
    )
    serializer_class = serializers.ManageAlbumSerializer
    filterset_class = filters.ManageAlbumFilterSet
    required_scope = "instance:libraries"
    ordering_fields = ["creation_date", "title", "release_date"]

    @extend_schema(operation_id="admin_get_library_album_stats")
    @rest_decorators.action(methods=["get"], detail=True)
    def stats(self, request, *args, **kwargs):
        album = self.get_object()
        data = get_stats(album.tracks.all(), album)
        return response.Response(data, status=200)

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

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


uploads_subquery = (
    music_models.Upload.objects.filter(track_id=OuterRef("pk"))
    .order_by()
    .values("track_id")
    .annotate(track_count=Count("track_id"))
    .values("track_count")
)


class ManageTrackViewSet(
    mixins.ListModelMixin,
    mixins.RetrieveModelMixin,
    mixins.DestroyModelMixin,
    viewsets.GenericViewSet,
):
    queryset = (
        music_models.Track.objects.all()
        .order_by("-id")
        .prefetch_related(
            "attributed_to",
            "artist_credit",
            "album__artist_credit",
            "album__attachment_cover",
            "attachment_cover",
        )
        .annotate(uploads_count=Coalesce(Subquery(uploads_subquery), 0))
        .prefetch_related(music_views.TAG_PREFETCH)
    )
    serializer_class = serializers.ManageTrackSerializer
    filterset_class = filters.ManageTrackFilterSet
    required_scope = "instance:libraries"
    ordering_fields = [
        "creation_date",
        "title",
        "album__release_date",
        "position",
        "disc_number",
    ]

    @extend_schema(operation_id="admin_get_track_stats")
    @rest_decorators.action(methods=["get"], detail=True)
    def stats(self, request, *args, **kwargs):
        track = self.get_object()
        data = get_stats(track.__class__.objects.filter(pk=track.pk), track)
        return response.Response(data, status=200)

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

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


uploads_subquery = (
    music_models.Upload.objects.filter(library_id=OuterRef("pk"))
    .order_by()
    .values("library_id")
    .annotate(library_count=Count("library_id"))
    .values("library_count")
)

follows_subquery = (
    federation_models.LibraryFollow.objects.filter(target_id=OuterRef("pk"))
    .order_by()
    .values("target_id")
    .annotate(library_count=Count("target_id"))
    .values("library_count")
)


class ManageLibraryViewSet(
    mixins.ListModelMixin,
    mixins.RetrieveModelMixin,
    mixins.UpdateModelMixin,
    mixins.DestroyModelMixin,
    viewsets.GenericViewSet,
):
    lookup_field = "uuid"
    queryset = (
        music_models.Library.objects.all()
        .filter(channel=None)
        .order_by("-id")
        .select_related("actor")
        .annotate(
            followers_count=Coalesce(Subquery(follows_subquery), 0),
            _uploads_count=Coalesce(Subquery(uploads_subquery), 0),
        )
    )
    serializer_class = serializers.ManageLibrarySerializer
    filterset_class = filters.ManageLibraryFilterSet
    required_scope = "instance:libraries"

    @extend_schema(operation_id="admin_get_library_stats")
    @rest_decorators.action(methods=["get"], detail=True)
    def stats(self, request, *args, **kwargs):
        library = self.get_object()
        uploads = library.uploads.all()
        tracks = uploads.values_list("track", flat=True).distinct()
        albums = (
            music_models.Track.objects.filter(pk__in=tracks)
            .values_list("album", flat=True)
            .distinct()
        )
        artists = set(
            music_models.Album.objects.filter(pk__in=albums).values_list(
                "artist_credit__artist", flat=True
            )
        ) | set(
            music_models.Track.objects.filter(pk__in=tracks).values_list(
                "artist_credit__artist", flat=True
            )
        )

        data = {
            "uploads": uploads.count(),
            "followers": library.received_follows.count(),
            "tracks": tracks.count(),
            "albums": albums.count(),
            "artists": len(artists),
            "reports": moderation_models.Report.objects.get_for_target(library).count(),
        }
        data.update(get_media_stats(uploads.all()))
        return response.Response(data, status=200)

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


class ManageUploadViewSet(
    mixins.ListModelMixin,
    mixins.RetrieveModelMixin,
    mixins.DestroyModelMixin,
    viewsets.GenericViewSet,
):
    lookup_field = "uuid"
    queryset = (
        music_models.Upload.objects.all()
        .order_by("-id")
        .prefetch_related(
            "library__actor",
            "track__artist_credit__artist",
            "track__album__artist_credit__artist",
        )
    )
    serializer_class = serializers.ManageUploadSerializer
    filterset_class = filters.ManageUploadFilterSet
    required_scope = "instance:libraries"

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


class ManageUserViewSet(
    mixins.ListModelMixin,
    mixins.RetrieveModelMixin,
    mixins.UpdateModelMixin,
    viewsets.GenericViewSet,
):
    queryset = users_models.User.objects.all().select_related("actor").order_by("-id")
    serializer_class = serializers.ManageUserSerializer
    filterset_class = filters.ManageUserFilterSet
    required_scope = "instance:users"
    ordering_fields = ["date_joined", "last_activity", "username"]

    def get_serializer_context(self):
        context = super().get_serializer_context()
        context["default_permissions"] = preferences.get("users__default_permissions")
        return context


class ManageInvitationViewSet(
    mixins.CreateModelMixin,
    mixins.ListModelMixin,
    mixins.RetrieveModelMixin,
    mixins.UpdateModelMixin,
    viewsets.GenericViewSet,
):
    queryset = (
        users_models.Invitation.objects.all()
        .order_by("-id")
        .prefetch_related("users")
        .select_related("owner")
    )
    serializer_class = serializers.ManageInvitationSerializer
    filterset_class = filters.ManageInvitationFilterSet
    required_scope = "instance:invitations"
    ordering_fields = ["creation_date", "expiration_date"]

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

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


class ManageDomainViewSet(
    mixins.CreateModelMixin,
    mixins.ListModelMixin,
    mixins.RetrieveModelMixin,
    mixins.UpdateModelMixin,
    viewsets.GenericViewSet,
):
    lookup_value_regex = r"[a-zA-Z0-9\-\.]+"
    queryset = (
        federation_models.Domain.objects.with_actors_count()
        .with_outbox_activities_count()
        .prefetch_related("instance_policy")
        .order_by("name")
    )
    serializer_class = serializers.ManageDomainSerializer
    filterset_class = filters.ManageDomainFilterSet
    required_scope = "instance:domains"
    ordering_fields = [
        "name",
        "creation_date",
        "nodeinfo_fetch_date",
        "actors_count",
        "outbox_activities_count",
        "instance_policy",
    ]

    def get_queryset(self, **kwargs):
        queryset = super().get_queryset(**kwargs)
        return queryset.external()

    def get_serializer_class(self):
        if self.action in ["update", "partial_update"]:
            # A dedicated serializer for update
            # to ensure domain name can't be changed
            return serializers.ManageDomainUpdateSerializer
        return super().get_serializer_class()

    def perform_create(self, serializer):
        domain = serializer.save()
        federation_tasks.update_domain_nodeinfo(domain_name=domain.name)

    @rest_decorators.action(methods=["get"], detail=True)
    def nodeinfo(self, request, *args, **kwargs):
        domain = self.get_object()
        federation_tasks.update_domain_nodeinfo(domain_name=domain.name)
        domain.refresh_from_db()
        return response.Response(domain.nodeinfo, status=200)

    @extend_schema(operation_id="admin_get_federation_domain_stats")
    @rest_decorators.action(methods=["get"], detail=True)
    def stats(self, request, *args, **kwargs):
        domain = self.get_object()
        return response.Response(domain.get_stats(), status=200)

    action = decorators.action_route(serializers.ManageDomainActionSerializer)


class ManageActorViewSet(
    mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
):
    lookup_value_regex = r"([a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+)"
    queryset = (
        federation_models.Actor.objects.all()
        .with_uploads_count()
        .order_by("-creation_date")
        .select_related("user")
        .prefetch_related("instance_policy")
    )
    serializer_class = serializers.ManageActorSerializer
    filterset_class = filters.ManageActorFilterSet
    required_scope = "instance:accounts"
    required_permissions = ["moderation"]
    ordering_fields = [
        "name",
        "preferred_username",
        "domain",
        "fid",
        "creation_date",
        "last_fetch_date",
        "uploads_count",
        "outbox_activities_count",
        "instance_policy",
    ]

    def get_object(self):
        queryset = self.filter_queryset(self.get_queryset())
        username, domain = self.kwargs["pk"].split("@")
        filter_kwargs = {"domain_id": domain, "preferred_username": username}
        obj = get_object_or_404(queryset, **filter_kwargs)
        self.check_object_permissions(self.request, obj)

        return obj

    @extend_schema(operation_id="admin_get_account_stats")
    @rest_decorators.action(methods=["get"], detail=True)
    def stats(self, request, *args, **kwargs):
        obj = self.get_object()
        return response.Response(obj.get_stats(), status=200)

    action = decorators.action_route(serializers.ManageActorActionSerializer)


class ManageInstancePolicyViewSet(
    mixins.ListModelMixin,
    mixins.RetrieveModelMixin,
    mixins.DestroyModelMixin,
    mixins.CreateModelMixin,
    mixins.UpdateModelMixin,
    viewsets.GenericViewSet,
):
    queryset = (
        moderation_models.InstancePolicy.objects.all()
        .order_by("-creation_date")
        .select_related()
    )
    serializer_class = serializers.ManageInstancePolicySerializer
    filterset_class = filters.ManageInstancePolicyFilterSet
    required_scope = "instance:policies"
    ordering_fields = ["id", "creation_date"]

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


class ManageReportViewSet(
    mixins.ListModelMixin,
    mixins.RetrieveModelMixin,
    mixins.UpdateModelMixin,
    viewsets.GenericViewSet,
):
    lookup_field = "uuid"
    queryset = (
        moderation_models.Report.objects.all()
        .order_by("-creation_date")
        .select_related(
            "submitter", "target_owner", "assigned_to", "target_content_type"
        )
        .prefetch_related("target")
        .prefetch_related(
            Prefetch(
                "notes",
                queryset=moderation_models.Note.objects.order_by(
                    "creation_date"
                ).select_related("author"),
                to_attr="_prefetched_notes",
            )
        )
    )
    serializer_class = serializers.ManageReportSerializer
    filterset_class = filters.ManageReportFilterSet
    required_scope = "instance:reports"
    ordering_fields = ["id", "creation_date", "handled_date"]

    def perform_update(self, serializer):
        is_handled = serializer.instance.is_handled
        if not is_handled and serializer.validated_data.get("is_handled"):
            # report was resolved, we assign to the mod making the request
            serializer.save(assigned_to=self.request.user.actor)
        else:
            serializer.save()


class ManageNoteViewSet(
    mixins.ListModelMixin,
    mixins.RetrieveModelMixin,
    mixins.DestroyModelMixin,
    mixins.CreateModelMixin,
    viewsets.GenericViewSet,
):
    lookup_field = "uuid"
    queryset = (
        moderation_models.Note.objects.all()
        .order_by("-creation_date")
        .select_related("author", "target_content_type")
        .prefetch_related("target")
    )
    serializer_class = serializers.ManageNoteSerializer
    filterset_class = filters.ManageNoteFilterSet
    required_scope = "instance:notes"
    ordering_fields = ["id", "creation_date"]

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


class ManageTagViewSet(
    mixins.ListModelMixin,
    mixins.RetrieveModelMixin,
    mixins.DestroyModelMixin,
    mixins.CreateModelMixin,
    viewsets.GenericViewSet,
):
    lookup_field = "name"
    queryset = (
        tags_models.Tag.objects.all()
        .order_by("-creation_date")
        .annotate(items_count=Count("tagged_items"))
        .annotate(length=Length("name"))
        .annotate(tag_deterministic=Collate("name", "und-x-icu"))
    )
    serializer_class = serializers.ManageTagSerializer
    filterset_class = filters.ManageTagFilterSet
    required_scope = "instance:libraries"
    ordering_fields = ["id", "creation_date", "name", "items_count", "length"]

    def get_queryset(self):
        queryset = super().get_queryset()
        from django.contrib.contenttypes.models import ContentType

        album_ct = ContentType.objects.get_for_model(music_models.Album)
        track_ct = ContentType.objects.get_for_model(music_models.Track)
        artist_ct = ContentType.objects.get_for_model(music_models.Artist)
        queryset = queryset.annotate(
            _albums_count=Count(
                "tagged_items", filter=Q(tagged_items__content_type=album_ct)
            ),
            _tracks_count=Count(
                "tagged_items", filter=Q(tagged_items__content_type=track_ct)
            ),
            _artists_count=Count(
                "tagged_items", filter=Q(tagged_items__content_type=artist_ct)
            ),
        )
        return queryset

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


class ManageUserRequestViewSet(
    mixins.ListModelMixin,
    mixins.RetrieveModelMixin,
    mixins.UpdateModelMixin,
    viewsets.GenericViewSet,
):
    lookup_field = "uuid"
    queryset = (
        moderation_models.UserRequest.objects.all()
        .order_by("-creation_date")
        .select_related("submitter", "assigned_to")
        .prefetch_related(
            Prefetch(
                "notes",
                queryset=moderation_models.Note.objects.order_by(
                    "creation_date"
                ).select_related("author"),
                to_attr="_prefetched_notes",
            )
        )
    )
    serializer_class = serializers.ManageUserRequestSerializer
    filterset_class = filters.ManageUserRequestFilterSet
    required_scope = "instance:requests"
    ordering_fields = ["id", "creation_date", "handled_date"]

    def get_queryset(self):
        queryset = super().get_queryset()
        if self.action in ["update", "partial_update"]:
            # approved requests cannot be edited
            queryset = queryset.exclude(status="approved")
        return queryset

    @transaction.atomic
    def perform_update(self, serializer):
        old_status = serializer.instance.status
        new_status = serializer.validated_data.get("status")

        if old_status != new_status and new_status != "pending":
            # report was resolved, we assign to the mod making the request
            serializer.save(assigned_to=self.request.user.actor)
            common_utils.on_commit(
                moderation_tasks.user_request_handle.delay,
                user_request_id=serializer.instance.pk,
                new_status=new_status,
                old_status=old_status,
            )
        else:
            serializer.save()


class ManageChannelViewSet(
    MultipleLookupDetailMixin,
    mixins.ListModelMixin,
    mixins.RetrieveModelMixin,
    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"],
            ),
        },
    ]
    queryset = (
        audio_models.Channel.objects.all()
        .order_by("-id")
        .select_related(
            "attributed_to",
            "actor",
        )
        .prefetch_related(
            Prefetch(
                "artist",
                queryset=(
                    music_models.Artist.objects.all()
                    .order_by("-id")
                    .select_related("attributed_to", "attachment_cover", "channel")
                    .annotate(_tracks_count=Count("artist_credit__tracks"))
                    .annotate(_albums_count=Count("artist_credit__albums"))
                    .prefetch_related(music_views.TAG_PREFETCH)
                ),
            )
        )
    )
    serializer_class = serializers.ManageChannelSerializer
    filterset_class = filters.ManageChannelFilterSet
    required_scope = "instance:libraries"
    ordering_fields = ["creation_date", "name"]

    @extend_schema(operation_id="admin_get_channel_stats")
    @rest_decorators.action(methods=["get"], detail=True)
    def stats(self, request, *args, **kwargs):
        channel = self.get_object()
        tracks = music_models.Track.objects.filter(
            Q(artist_credit__artist=channel.artist)
            | Q(album__artist_credit__artist=channel.artist)
        )
        data = get_stats(tracks, channel, ignore_fields=["libraries", "channels"])
        data["follows"] = channel.actor.received_follows.count()
        return response.Response(data, status=200)

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