import logging
import os
import urllib.parse
import uuid

from django.core.exceptions import ObjectDoesNotExist
from django.core.paginator import Paginator
from django.db import transaction
from django.db.models import Q
from django.urls import reverse
from django.utils import timezone
from rest_framework import serializers

from funkwhale_api.common import models as common_models
from funkwhale_api.common import utils as common_utils
from funkwhale_api.favorites import models as favorites_models
from funkwhale_api.federation import activity, actors, contexts, jsonld, models, utils
from funkwhale_api.history import models as history_models
from funkwhale_api.moderation import models as moderation_models
from funkwhale_api.moderation import serializers as moderation_serializers
from funkwhale_api.moderation import signals as moderation_signals
from funkwhale_api.music import licenses
from funkwhale_api.music import models as music_models
from funkwhale_api.music import tasks as music_tasks
from funkwhale_api.playlists import models as playlists_models
from funkwhale_api.tags import models as tags_models

logger = logging.getLogger(__name__)


def include_if_not_none(data, value, field):
    if value is not None:
        data[field] = value


class MultipleSerializer(serializers.Serializer):
    """
    A serializer that will try multiple serializers in turn
    """

    def __init__(self, *args, **kwargs):
        self.allowed = kwargs.pop("allowed")
        super().__init__(*args, **kwargs)

    def to_internal_value(self, v):
        last_exception = None
        for serializer_class in self.allowed:
            s = serializer_class(data=v)
            try:
                s.is_valid(raise_exception=True)
            except serializers.ValidationError as e:
                last_exception = e
            else:
                return s.validated_data

        raise last_exception


class TruncatedCharField(serializers.CharField):
    def __init__(self, *args, **kwargs):
        self.truncate_length = kwargs.pop("truncate_length")
        super().__init__(*args, **kwargs)

    def to_internal_value(self, v):
        v = super().to_internal_value(v)
        if v:
            v = v[: self.truncate_length]
        return v


class TagSerializer(jsonld.JsonLdSerializer):
    type = serializers.ChoiceField(choices=[contexts.AS.Hashtag])
    name = serializers.CharField(max_length=100)

    class Meta:
        jsonld_mapping = {"name": jsonld.first_val(contexts.AS.name)}

    def validate_name(self, value):
        if value.startswith("#"):
            # remove trailing #
            value = value[1:]
        return value


def tag_list(tagged_items):
    return [
        repr_tag(item.tag.name)
        for item in sorted(set(tagged_items.all()), key=lambda i: i.tag.name)
    ]


def is_mimetype(mt, allowed_mimetypes):
    for allowed in allowed_mimetypes:
        if allowed.endswith("/*"):
            if mt.startswith(allowed.replace("*", "")):
                return True
        else:
            if mt == allowed:
                return True
    return False


class MediaSerializer(jsonld.JsonLdSerializer):
    mediaType = serializers.CharField()

    def __init__(self, *args, **kwargs):
        self.allowed_mimetypes = kwargs.pop("allowed_mimetypes", [])
        self.allow_empty_mimetype = kwargs.pop("allow_empty_mimetype", False)
        super().__init__(*args, **kwargs)
        self.fields["mediaType"].required = not self.allow_empty_mimetype
        self.fields["mediaType"].allow_null = self.allow_empty_mimetype

    def validate_mediaType(self, v):
        if not self.allowed_mimetypes:
            # no restrictions
            return v
        if self.allow_empty_mimetype and not v:
            return None

        if not is_mimetype(v, self.allowed_mimetypes):
            raise serializers.ValidationError(
                f"Invalid mimetype {v}. Allowed: {self.allowed_mimetypes}"
            )
        return v


class LinkSerializer(MediaSerializer):
    type = serializers.ChoiceField(choices=[contexts.AS.Link])
    href = serializers.URLField(max_length=500)
    bitrate = serializers.IntegerField(min_value=0, required=False)
    size = serializers.IntegerField(min_value=0, required=False)

    class Meta:
        jsonld_mapping = {
            "href": jsonld.first_id(contexts.AS.href),
            "mediaType": jsonld.first_val(contexts.AS.mediaType),
            "bitrate": jsonld.first_val(contexts.FW.bitrate),
            "size": jsonld.first_val(contexts.FW.size),
        }


class LinkListSerializer(serializers.ListField):
    def __init__(self, *args, **kwargs):
        kwargs.setdefault("child", LinkSerializer(jsonld_expand=False))
        self.keep_mediatype = kwargs.pop("keep_mediatype", [])
        super().__init__(*args, **kwargs)

    def to_internal_value(self, v):
        links = super().to_internal_value(v)
        if not self.keep_mediatype:
            # no further filtering required
            return links
        links = [
            link
            for link in links
            if link.get("mediaType")
            and is_mimetype(link["mediaType"], self.keep_mediatype)
        ]
        if not self.allow_empty and len(links) == 0:
            self.fail("empty")

        return links


class ImageSerializer(MediaSerializer):
    type = serializers.ChoiceField(choices=[contexts.AS.Image, contexts.AS.Link])
    href = serializers.URLField(max_length=500, required=False)
    url = serializers.URLField(max_length=500, required=False)

    class Meta:
        jsonld_mapping = {
            "url": jsonld.first_id(contexts.AS.url),
            "href": jsonld.first_id(contexts.AS.href),
            "mediaType": jsonld.first_val(contexts.AS.mediaType),
        }

    def validate(self, data):
        validated_data = super().validate(data)
        if "url" not in validated_data:
            try:
                validated_data["url"] = validated_data["href"]
            except KeyError:
                if self.required:
                    raise serializers.ValidationError(
                        "You need to provide a url or href"
                    )

        return validated_data


class URLSerializer(jsonld.JsonLdSerializer):
    href = serializers.URLField(max_length=500)
    mediaType = serializers.CharField(required=False)

    class Meta:
        jsonld_mapping = {
            "href": jsonld.first_id(contexts.AS.href, aliases=[jsonld.raw("@id")]),
            "mediaType": jsonld.first_val(contexts.AS.mediaType),
        }


class EndpointsSerializer(jsonld.JsonLdSerializer):
    sharedInbox = serializers.URLField(max_length=500, required=False)

    class Meta:
        jsonld_mapping = {"sharedInbox": jsonld.first_id(contexts.AS.sharedInbox)}


class PublicKeySerializer(jsonld.JsonLdSerializer):
    publicKeyPem = serializers.CharField(trim_whitespace=False)

    class Meta:
        jsonld_mapping = {"publicKeyPem": jsonld.first_val(contexts.SEC.publicKeyPem)}


def get_by_media_type(urls, media_type):
    for url in urls:
        if url.get("mediaType", "text/html") == media_type:
            return url


class BasicActorSerializer(jsonld.JsonLdSerializer):
    id = serializers.URLField(max_length=500)
    type = serializers.ChoiceField(
        choices=[getattr(contexts.AS, c[0]) for c in models.TYPE_CHOICES]
    )

    class Meta:
        jsonld_mapping = {}


class ActorSerializer(jsonld.JsonLdSerializer):
    id = serializers.URLField(max_length=500)
    outbox = serializers.URLField(max_length=500, required=False)
    inbox = serializers.URLField(max_length=500, required=False)
    url = serializers.ListField(
        child=URLSerializer(jsonld_expand=False), required=False, min_length=0
    )
    type = serializers.ChoiceField(
        choices=[getattr(contexts.AS, c[0]) for c in models.TYPE_CHOICES]
    )
    preferredUsername = serializers.CharField()
    manuallyApprovesFollowers = serializers.BooleanField(
        required=False, allow_null=True
    )
    name = serializers.CharField(
        required=False, max_length=200, allow_blank=True, allow_null=True
    )
    summary = TruncatedCharField(
        truncate_length=common_models.CONTENT_TEXT_MAX_LENGTH,
        required=False,
        allow_null=True,
        allow_blank=True,
    )
    followers = serializers.URLField(max_length=500, required=False)
    following = serializers.URLField(max_length=500, required=False, allow_null=True)
    publicKey = PublicKeySerializer(required=False)
    endpoints = EndpointsSerializer(required=False)
    icon = ImageSerializer(
        allowed_mimetypes=["image/*"],
        allow_null=True,
        required=False,
        allow_empty_mimetype=True,
    )
    attributedTo = serializers.URLField(max_length=500, required=False)

    tags = serializers.ListField(min_length=0, required=False, allow_null=True)

    def validate_tags(self, tags):
        valid_tags = []
        for tag in tags:
            s = TagSerializer(data=tag)
            if s.is_valid():
                valid_tags.append(s.validated_data)
        return valid_tags

    category = serializers.CharField(required=False)
    # languages = serializers.Char(
    #     music_models.ARTIST_CONTENT_CATEGORY_CHOICES, required=False, default="music",
    # )

    class Meta:
        # not strictly necessary because it's not a model serializer
        # but used by tasks.py/fetch
        model = models.Actor

        jsonld_mapping = {
            "outbox": jsonld.first_id(contexts.AS.outbox),
            "inbox": jsonld.first_id(contexts.LDP.inbox),
            "following": jsonld.first_id(contexts.AS.following),
            "followers": jsonld.first_id(contexts.AS.followers),
            "preferredUsername": jsonld.first_val(contexts.AS.preferredUsername),
            "summary": jsonld.first_val(contexts.AS.summary),
            "name": jsonld.first_val(contexts.AS.name),
            "publicKey": jsonld.first_obj(contexts.SEC.publicKey),
            "manuallyApprovesFollowers": jsonld.first_val(
                contexts.AS.manuallyApprovesFollowers
            ),
            "mediaType": jsonld.first_val(contexts.AS.mediaType),
            "endpoints": jsonld.first_obj(contexts.AS.endpoints),
            "icon": jsonld.first_obj(contexts.AS.icon),
            "url": jsonld.raw(contexts.AS.url),
            "attributedTo": jsonld.first_id(contexts.AS.attributedTo),
            "tags": jsonld.raw(contexts.AS.tag),
            "category": jsonld.first_val(contexts.SC.category),
            # "language": jsonld.first_val(contexts.SC.inLanguage),
        }

    def validate_category(self, v):
        return (
            v
            if v in [t for t, _ in music_models.ARTIST_CONTENT_CATEGORY_CHOICES]
            else None
        )

    def to_representation(self, instance):
        ret = {
            "id": instance.fid,
            "outbox": instance.outbox_url,
            "inbox": instance.inbox_url,
            "preferredUsername": instance.preferred_username,
            "type": instance.type,
        }
        if instance.name:
            ret["name"] = instance.name
        if instance.followers_url:
            ret["followers"] = instance.followers_url
        if instance.following_url:
            ret["following"] = instance.following_url
        if instance.manually_approves_followers is not None:
            ret["manuallyApprovesFollowers"] = instance.manually_approves_followers

        if instance.summary_obj_id:
            ret["summary"] = instance.summary_obj.rendered
        urls = []
        if instance.url:
            urls.append(
                {"type": "Link", "href": instance.url, "mediaType": "text/html"}
            )

        channel = instance.get_channel()
        if channel:
            ret["url"] = [
                {
                    "type": "Link",
                    "href": (
                        instance.channel.get_absolute_url()
                        if instance.channel.artist.is_local
                        else instance.get_absolute_url()
                    ),
                    "mediaType": "text/html",
                },
                {
                    "type": "Link",
                    "href": instance.channel.get_rss_url(),
                    "mediaType": "application/rss+xml",
                },
            ]
            include_image(ret, channel.artist.attachment_cover, "icon")
            if channel.artist.description_id:
                ret["summary"] = channel.artist.description.rendered
            ret["attributedTo"] = channel.attributed_to.fid
            ret["category"] = channel.artist.content_category
            ret["tag"] = tag_list(channel.artist.tagged_items.all())
        else:
            ret["url"] = [
                {
                    "type": "Link",
                    "href": instance.get_absolute_url(),
                    "mediaType": "text/html",
                }
            ]
            include_image(ret, instance.attachment_icon, "icon")

        ret["@context"] = jsonld.get_default_context()
        if instance.public_key:
            ret["publicKey"] = {
                "owner": instance.fid,
                "publicKeyPem": instance.public_key,
                "id": f"{instance.fid}#main-key",
            }
        ret["endpoints"] = {}

        if instance.shared_inbox_url:
            ret["endpoints"]["sharedInbox"] = instance.shared_inbox_url
        return ret

    def prepare_missing_fields(self):
        kwargs = {
            "fid": self.validated_data["id"],
            "outbox_url": self.validated_data.get("outbox"),
            "inbox_url": self.validated_data.get("inbox"),
            "following_url": self.validated_data.get("following"),
            "followers_url": self.validated_data.get("followers"),
            "type": self.validated_data["type"],
            "name": self.validated_data.get("name"),
            "preferred_username": self.validated_data["preferredUsername"],
        }
        url = get_by_media_type(self.validated_data.get("url", []), "text/html")
        if url:
            kwargs["url"] = url["href"]

        maf = self.validated_data.get("manuallyApprovesFollowers")
        if maf is not None:
            kwargs["manually_approves_followers"] = maf
        domain = urllib.parse.urlparse(kwargs["fid"]).netloc
        domain, domain_created = models.Domain.objects.get_or_create(pk=domain)
        if domain_created and not domain.is_local:
            from . import tasks

            # first time we see the domain, we trigger nodeinfo fetching
            tasks.update_domain_nodeinfo(domain_name=domain.name)

        kwargs["domain"] = domain
        for endpoint, url in self.validated_data.get("endpoints", {}).items():
            if endpoint == "sharedInbox":
                kwargs["shared_inbox_url"] = url
                break
        try:
            kwargs["public_key"] = self.validated_data["publicKey"]["publicKeyPem"]
        except KeyError:
            pass
        return kwargs

    def validate_type(self, v):
        return v.split("#")[-1]

    def build(self):
        d = self.prepare_missing_fields()
        return models.Actor(**d)

    def save(self, **kwargs):
        d = self.prepare_missing_fields()
        d.update(kwargs)
        actor = models.Actor.objects.update_or_create(fid=d["fid"], defaults=d)[0]
        common_utils.attach_content(
            actor, "summary_obj", self.validated_data["summary"]
        )
        if "icon" in self.validated_data:
            new_value = self.validated_data["icon"]
            common_utils.attach_file(
                actor,
                "attachment_icon",
                (
                    {"url": new_value["url"], "mimetype": new_value.get("mediaType")}
                    if new_value
                    else None
                ),
            )

        rss_url = get_by_media_type(
            self.validated_data.get("url", []), "application/rss+xml"
        )
        if rss_url:
            rss_url = rss_url["href"]
        attributed_to = self.validated_data.get("attributedTo", actor.fid)
        if rss_url:
            # if the actor is attributed to another actor, and there is a RSS url,
            # then we consider it's a channel
            create_or_update_channel(
                actor,
                rss_url=rss_url,
                attributed_to_fid=attributed_to,
                **self.validated_data,
            )
        return actor

    def validate(self, data):
        validated_data = super().validate(data)
        if "summary" in data:
            validated_data["summary"] = {
                "content_type": "text/html",
                "text": data["summary"],
            }
        else:
            validated_data["summary"] = None
        return validated_data


def create_or_update_channel(actor, rss_url, attributed_to_fid, **validated_data):
    from funkwhale_api.audio import models as audio_models

    attributed_to = actors.get_actor(attributed_to_fid)
    artist_defaults = {
        "name": validated_data.get("name", validated_data["preferredUsername"]),
        "fid": validated_data["id"],
        "content_category": validated_data.get("category", "music") or "music",
        "attributed_to": attributed_to,
    }
    artist, created = music_models.Artist.objects.update_or_create(
        channel__attributed_to=attributed_to,
        channel__actor=actor,
        defaults=artist_defaults,
    )
    common_utils.attach_content(artist, "description", validated_data.get("summary"))
    if "icon" in validated_data:
        new_value = validated_data["icon"]
        common_utils.attach_file(
            artist,
            "attachment_cover",
            (
                {"url": new_value["url"], "mimetype": new_value.get("mediaType")}
                if new_value
                else None
            ),
        )
    tags = [t["name"] for t in validated_data.get("tags", []) or []]
    tags_models.set_tags(artist, *tags)
    if created:
        uid = uuid.uuid4()
        fid = utils.full_url(
            reverse("federation:music:libraries-detail", kwargs={"uuid": uid})
        )
        library = attributed_to.libraries.create(
            privacy_level="everyone",
            name=artist_defaults["name"],
            fid=fid,
            uuid=uid,
        )
    else:
        library = artist.channel.library
    channel_defaults = {
        "actor": actor,
        "attributed_to": attributed_to,
        "rss_url": rss_url,
        "artist": artist,
        "library": library,
    }
    channel, created = audio_models.Channel.objects.update_or_create(
        actor=actor,
        attributed_to=attributed_to,
        defaults=channel_defaults,
    )
    return channel


class APIActorSerializer(serializers.ModelSerializer):
    class Meta:
        model = models.Actor
        fields = [
            "fid",
            "url",
            "creation_date",
            "summary",
            "preferred_username",
            "name",
            "last_fetch_date",
            "domain",
            "type",
            "manually_approves_followers",
            "full_username",
            "is_local",
        ]


class BaseActivitySerializer(serializers.Serializer):
    id = serializers.URLField(max_length=500, required=False)
    type = serializers.CharField(max_length=100)
    actor = serializers.URLField(max_length=500)
    object = serializers.JSONField(required=False, allow_null=True)

    def validate_actor(self, v):
        expected = self.context.get("actor")
        if expected and expected.fid != v:
            raise serializers.ValidationError("Invalid actor")
        if expected:
            # avoid a DB lookup
            return expected
        try:
            return models.Actor.objects.get(fid=v)
        except models.Actor.DoesNotExist:
            raise serializers.ValidationError("Actor not found")

    def create(self, validated_data):
        return models.Activity.objects.create(
            fid=validated_data.get("id"),
            actor=validated_data["actor"],
            payload=self.initial_data,
            type=validated_data["type"],
        )

    def validate(self, data):
        self.validate_recipients(data, self.initial_data)
        return super().validate(data)

    def validate_recipients(self, data, payload):
        """
        Ensure we have at least a to/cc field with valid actors
        """
        data["to"] = payload.get("to", [])
        data["cc"] = payload.get("cc", [])

        if (
            not data["to"]
            and data.get("type") in ["Follow", "Accept"]
            and data.get("object")
        ):
            # there isn't always a to field for Accept/Follow
            # in their follow activity, so we consider the recipient
            # to be the follow object
            if data["type"] == "Follow":
                data["to"].append(str(data.get("object")))
            else:
                data["to"].append(data.get("object", {}).get("actor"))

        if not data["to"] and not data["cc"] and not self.context.get("recipients"):
            raise serializers.ValidationError(
                "We cannot handle an activity with no recipient"
            )


class FollowSerializer(serializers.Serializer):
    id = serializers.URLField(max_length=500)
    object = serializers.URLField(max_length=500)
    actor = serializers.URLField(max_length=500)
    type = serializers.ChoiceField(choices=["Follow"])

    def validate_object(self, v):
        expected = self.context.get("follow_target")
        if self.parent:
            # it's probably an accept, so everything is inverted, the actor
            # the recipient does not matter
            recipient = None
        else:
            recipient = self.context.get("recipient")
        if expected and expected.fid != v:
            raise serializers.ValidationError("Invalid target")
        try:
            obj = models.Actor.objects.get(fid=v)
            if recipient and recipient.fid != obj.fid:
                raise serializers.ValidationError("Invalid target")
            return obj
        except models.Actor.DoesNotExist:
            pass
        try:
            qs = music_models.Library.objects.filter(fid=v)
            if recipient:
                qs = qs.filter(actor=recipient)
            return qs.get()
        except music_models.Library.DoesNotExist:
            pass

        raise serializers.ValidationError("Target not found")

    def validate_actor(self, v):
        expected = self.context.get("follow_actor")
        if expected and expected.fid != v:
            raise serializers.ValidationError("Invalid actor")
        try:
            return models.Actor.objects.get(fid=v)
        except models.Actor.DoesNotExist:
            raise serializers.ValidationError("Actor not found")

    def save(self, **kwargs):
        target = self.validated_data["object"]
        if target._meta.label == "music.Library":
            follow_class = models.LibraryFollow
        else:
            follow_class = models.Follow
        defaults = kwargs
        defaults["fid"] = self.validated_data["id"]
        approved = kwargs.pop("approved", None)
        follow, created = follow_class.objects.update_or_create(
            actor=self.validated_data["actor"],
            target=self.validated_data["object"],
            defaults=defaults,
        )
        if not created:
            # We likely received a new follow when we had an existing one in database
            # this can happen when two instances are out of sync, e.g because some
            # messages are not delivered properly. In this case, we don't change
            # the follow approved status and return the follow as is.
            # We set a new UUID to ensure the follow urls are updated properly
            # cf #830
            follow.uuid = uuid.uuid4()
            follow.save(update_fields=["uuid"])
            return follow

        # it's a brand new follow, we use the approved value stored earlier
        if approved != follow.approved:
            follow.approved = approved
            follow.save(update_fields=["approved"])

        return follow

    def to_representation(self, instance):
        return {
            "@context": jsonld.get_default_context(),
            "actor": instance.actor.fid,
            "id": instance.get_federation_id(),
            "object": instance.target.fid,
            "type": "Follow",
        }


class APIFollowSerializer(serializers.ModelSerializer):
    actor = APIActorSerializer()
    target = APIActorSerializer()

    class Meta:
        model = models.Follow
        fields = [
            "uuid",
            "id",
            "approved",
            "creation_date",
            "modification_date",
            "actor",
            "target",
        ]


class FollowActionSerializer(serializers.Serializer):
    id = serializers.URLField(max_length=500, required=False)
    actor = serializers.URLField(max_length=500)
    object = FollowSerializer()

    def validate_actor(self, v):
        expected = self.context.get("actor")
        if expected and expected.fid != v:
            raise serializers.ValidationError("Invalid actor")
        try:
            return models.Actor.objects.get(fid=v)
        except models.Actor.DoesNotExist:
            raise serializers.ValidationError("Actor not found")

    def validate(self, validated_data):
        # we ensure the accept actor actually match the follow target / library owner
        target = validated_data["object"]["object"]

        if target._meta.label == "music.Library":
            expected = target.actor
            follow_class = models.LibraryFollow
        else:
            expected = target
            follow_class = models.Follow
        if validated_data["actor"] != expected:
            raise serializers.ValidationError("Actor mismatch")
        try:
            validated_data["follow"] = (
                follow_class.objects.filter(
                    target=target, actor=validated_data["object"]["actor"]
                )
                .select_related()
                .get()
            )
        except follow_class.DoesNotExist:
            raise serializers.ValidationError(f"No follow to {self.action_type}")
        return validated_data

    def to_representation(self, instance):
        if instance.target._meta.label == "music.Library":
            actor = instance.target.actor
        else:
            actor = instance.target

        return {
            "@context": jsonld.get_default_context(),
            "id": instance.get_federation_id() + f"/{self.action_type}",
            "type": self.action_type.title(),
            "actor": actor.fid,
            "object": FollowSerializer(instance).data,
        }


class AcceptFollowSerializer(FollowActionSerializer):
    type = serializers.ChoiceField(choices=["Accept"])
    action_type = "accept"

    def save(self):
        follow = self.validated_data["follow"]
        follow.approved = True
        follow.save()
        if follow.target._meta.label == "music.Library":
            follow.target.schedule_scan(actor=follow.actor)
        return follow


class RejectFollowSerializer(FollowActionSerializer):
    type = serializers.ChoiceField(choices=["Reject"])
    action_type = "reject"

    def save(self):
        follow = self.validated_data["follow"]
        follow.approved = False
        follow.save()
        return follow


class UndoFollowSerializer(serializers.Serializer):
    id = serializers.URLField(max_length=500)
    actor = serializers.URLField(max_length=500)
    object = FollowSerializer()
    type = serializers.ChoiceField(choices=["Undo"])

    def validate_actor(self, v):
        expected = self.context.get("actor")

        if expected and expected.fid != v:
            raise serializers.ValidationError("Invalid actor")
        try:
            return models.Actor.objects.get(fid=v)
        except models.Actor.DoesNotExist:
            raise serializers.ValidationError("Actor not found")

    def validate(self, validated_data):
        # we ensure the accept actor actually match the follow actor
        if validated_data["actor"] != validated_data["object"]["actor"]:
            raise serializers.ValidationError("Actor mismatch")

        target = validated_data["object"]["object"]

        if target._meta.label == "music.Library":
            follow_class = models.LibraryFollow
        else:
            follow_class = models.Follow

        try:
            validated_data["follow"] = follow_class.objects.filter(
                actor=validated_data["actor"], target=target
            ).get()
        except follow_class.DoesNotExist:
            raise serializers.ValidationError(
                f"No follow to remove follow_class = {follow_class}"
            )
        return validated_data

    def to_representation(self, instance):
        return {
            "@context": jsonld.get_default_context(),
            "id": instance.get_federation_id() + "/undo",
            "type": "Undo",
            "actor": instance.actor.fid,
            "object": FollowSerializer(instance).data,
        }

    def save(self):
        return self.validated_data["follow"].delete()


class ActorWebfingerSerializer(serializers.Serializer):
    subject = serializers.CharField()
    aliases = serializers.ListField(child=serializers.URLField(max_length=500))
    links = serializers.ListField()
    actor_url = serializers.URLField(max_length=500, required=False)

    def validate(self, validated_data):
        validated_data["actor_url"] = None
        for l in validated_data["links"]:
            try:
                if not l["rel"] == "self":
                    continue
                if not l["type"] == "application/activity+json":
                    continue
                validated_data["actor_url"] = l["href"]
                break
            except KeyError:
                pass
        if validated_data["actor_url"] is None:
            raise serializers.ValidationError("No valid actor url found")
        return validated_data

    def to_representation(self, instance):
        data = {}
        data["subject"] = f"acct:{instance.webfinger_subject}"
        data["links"] = [
            {"rel": "self", "href": instance.fid, "type": "application/activity+json"}
        ]
        data["aliases"] = [instance.fid]
        return data


class ActivitySerializer(serializers.Serializer):
    actor = serializers.URLField(max_length=500)
    id = serializers.URLField(max_length=500, required=False)
    type = serializers.ChoiceField(choices=[(c, c) for c in activity.ACTIVITY_TYPES])
    object = serializers.JSONField(required=False)
    target = serializers.JSONField(required=False)

    def validate_object(self, value):
        try:
            type = value["type"]
        except KeyError:
            raise serializers.ValidationError("Missing object type")
        except TypeError:
            # probably a URL
            return value
        try:
            object_serializer = OBJECT_SERIALIZERS[type]
        except KeyError:
            raise serializers.ValidationError(f"Unsupported type {type}")
        serializer = object_serializer(data=value)
        serializer.is_valid(raise_exception=True)
        return serializer.data

    def validate_actor(self, value):
        request_actor = self.context.get("actor")
        if request_actor and request_actor.fid != value:
            raise serializers.ValidationError(
                "The actor making the request do not match" " the activity actor"
            )
        return value

    def to_representation(self, conf):
        d = {}
        d.update(conf)

        if self.context.get("include_ap_context", True):
            d["@context"] = jsonld.get_default_context()
        return d


class ObjectSerializer(serializers.Serializer):
    id = serializers.URLField(max_length=500)
    url = serializers.URLField(max_length=500, required=False, allow_null=True)
    type = serializers.ChoiceField(choices=[(c, c) for c in activity.OBJECT_TYPES])
    content = serializers.CharField(required=False, allow_null=True)
    summary = serializers.CharField(required=False, allow_null=True)
    name = serializers.CharField(required=False, allow_null=True)
    published = serializers.DateTimeField(required=False, allow_null=True)
    updated = serializers.DateTimeField(required=False, allow_null=True)
    to = serializers.ListField(
        child=serializers.URLField(max_length=500), required=False, allow_null=True
    )
    cc = serializers.ListField(
        child=serializers.URLField(max_length=500), required=False, allow_null=True
    )
    bto = serializers.ListField(
        child=serializers.URLField(max_length=500), required=False, allow_null=True
    )
    bcc = serializers.ListField(
        child=serializers.URLField(max_length=500), required=False, allow_null=True
    )


OBJECT_SERIALIZERS = {t: ObjectSerializer for t in activity.OBJECT_TYPES}


def get_additional_fields(data):
    UNSET = object()
    additional_fields = {}
    for field in ["name", "summary"]:
        v = data.get(field, UNSET)
        if v == UNSET:
            continue
        additional_fields[field] = v

    return additional_fields


PAGINATED_COLLECTION_JSONLD_MAPPING = {
    "totalItems": jsonld.first_val(contexts.AS.totalItems),
    "first": jsonld.first_id(contexts.AS.first),
    "last": jsonld.first_id(contexts.AS.last),
    "partOf": jsonld.first_id(contexts.AS.partOf),
}


class PaginatedCollectionSerializer(jsonld.JsonLdSerializer):
    type = serializers.ChoiceField(
        choices=[contexts.AS.Collection, contexts.AS.OrderedCollection]
    )
    totalItems = serializers.IntegerField(min_value=0)
    id = serializers.URLField(max_length=500)
    first = serializers.URLField(max_length=500)
    last = serializers.URLField(max_length=500)

    class Meta:
        jsonld_mapping = PAGINATED_COLLECTION_JSONLD_MAPPING

    def to_representation(self, conf):
        paginator = Paginator(conf["items"], conf.get("page_size", 20))
        first = common_utils.set_query_parameter(conf["id"], page=1)
        current = first
        last = common_utils.set_query_parameter(conf["id"], page=paginator.num_pages)
        data = {
            "id": conf["id"],
            "attributedTo": conf["actor"].fid,
            "totalItems": paginator.count,
            "type": conf.get("type", "Collection"),
            "current": current,
            "first": first,
            "last": last,
        }
        data.update(get_additional_fields(conf))
        if self.context.get("include_ap_context", True):
            data["@context"] = jsonld.get_default_context()
        return data


class LibrarySerializer(PaginatedCollectionSerializer):
    type = serializers.ChoiceField(
        choices=[contexts.AS.Collection, contexts.FW.Library]
    )
    actor = serializers.URLField(max_length=500, required=False)
    attributedTo = serializers.URLField(max_length=500, required=False)
    name = serializers.CharField()
    audience = serializers.ChoiceField(
        choices=["", "./", None, "https://www.w3.org/ns/activitystreams#Public"],
        required=False,
        allow_null=True,
        allow_blank=True,
    )

    class Meta:
        # not strictly necessary because it's not a model serializer
        # but used by tasks.py/fetch
        model = music_models.Library

        jsonld_mapping = common_utils.concat_dicts(
            PAGINATED_COLLECTION_JSONLD_MAPPING,
            {
                "name": jsonld.first_val(contexts.AS.name),
                "audience": jsonld.first_id(contexts.AS.audience),
                "actor": jsonld.first_id(contexts.AS.actor),
                "attributedTo": jsonld.first_id(contexts.AS.attributedTo),
            },
        )

    def validate(self, validated_data):
        d = super().validate(validated_data)
        actor = d.get("actor")
        attributed_to = d.get("attributedTo")
        if not actor and not attributed_to:
            raise serializers.ValidationError(
                "You need to provide at least actor or attributedTo"
            )

        d["attributedTo"] = attributed_to or actor
        return d

    def to_representation(self, library):
        conf = {
            "id": library.fid,
            "name": library.name,
            "page_size": 100,
            "attributedTo": library.actor,
            "actor": library.actor,
            "items": library.uploads.for_federation(),
            "type": "Library",
        }
        r = super().to_representation(conf)
        r["audience"] = (
            contexts.AS.Public if library.privacy_level == "everyone" else ""
        )
        return r

    def create(self, validated_data):
        if self.instance:
            actor = self.instance.actor
        else:
            actor = utils.retrieve_ap_object(
                validated_data["attributedTo"],
                actor=self.context.get("fetch_actor"),
                queryset=models.Actor,
                serializer_class=ActorSerializer,
            )
        privacy = {"": "me", "./": "me", None: "me", contexts.AS.Public: "everyone"}
        library, created = music_models.Library.objects.update_or_create(
            fid=validated_data["id"],
            actor=actor,
            defaults={
                "uploads_count": validated_data["totalItems"],
                "name": validated_data["name"],
                "privacy_level": privacy[validated_data["audience"]],
            },
        )
        return library

    def update(self, instance, validated_data):
        return self.create(validated_data)


class CollectionPageSerializer(jsonld.JsonLdSerializer):
    type = serializers.ChoiceField(choices=[contexts.AS.CollectionPage])
    totalItems = serializers.IntegerField(min_value=0)
    items = serializers.ListField()
    actor = serializers.URLField(max_length=500, required=False)
    attributedTo = serializers.URLField(max_length=500, required=False)
    id = serializers.URLField(max_length=500)
    first = serializers.URLField(max_length=500)
    last = serializers.URLField(max_length=500)
    next = serializers.URLField(max_length=500, required=False)
    prev = serializers.URLField(max_length=500, required=False)
    partOf = serializers.URLField(max_length=500)

    class Meta:
        jsonld_mapping = {
            "totalItems": jsonld.first_val(contexts.AS.totalItems),
            "items": jsonld.raw(contexts.AS.items),
            "actor": jsonld.first_id(contexts.AS.actor),
            "attributedTo": jsonld.first_id(contexts.AS.attributedTo),
            "first": jsonld.first_id(contexts.AS.first),
            "last": jsonld.first_id(contexts.AS.last),
            "next": jsonld.first_id(contexts.AS.next),
            "prev": jsonld.first_id(contexts.AS.prev),
            "partOf": jsonld.first_id(contexts.AS.partOf),
        }

    def validate_items(self, v):
        item_serializer = self.context.get("item_serializer")
        if not item_serializer:
            return v
        raw_items = [item_serializer(data=i, context=self.context) for i in v]
        valid_items = []
        for i in raw_items:
            try:
                i.is_valid(raise_exception=True)
                valid_items.append(i)
            except serializers.ValidationError:
                logger.debug("Invalid item %s: %s", i.data, i.errors)

        return valid_items

    def to_representation(self, conf):
        page = conf["page"]
        first = common_utils.set_query_parameter(conf["id"], page=1)
        last = common_utils.set_query_parameter(
            conf["id"], page=page.paginator.num_pages
        )
        id = common_utils.set_query_parameter(conf["id"], page=page.number)
        d = {
            "id": id,
            "partOf": conf["id"],
            "totalItems": page.paginator.count,
            "type": "CollectionPage",
            "first": first,
            "last": last,
            "items": [
                conf["item_serializer"](
                    i, context={"actor": conf["actor"], "include_ap_context": False}
                ).data
                for i in page.object_list
            ],
        }
        if conf["actor"]:
            d["attributedTo"] = conf["actor"].fid

        if page.has_previous():
            d["prev"] = common_utils.set_query_parameter(
                conf["id"], page=page.previous_page_number()
            )

        if page.has_next():
            d["next"] = common_utils.set_query_parameter(
                conf["id"], page=page.next_page_number()
            )
        d.update(get_additional_fields(conf))
        if self.context.get("include_ap_context", True):
            d["@context"] = jsonld.get_default_context()
        return d


MUSIC_ENTITY_JSONLD_MAPPING = {
    "name": jsonld.first_val(contexts.AS.name),
    "published": jsonld.first_val(contexts.AS.published),
    "musicbrainzId": jsonld.first_val(contexts.FW.musicbrainzId),
    "attributedTo": jsonld.first_id(contexts.AS.attributedTo),
    "tags": jsonld.raw(contexts.AS.tag),
    "mediaType": jsonld.first_val(contexts.AS.mediaType),
    "content": jsonld.first_val(contexts.AS.content),
}


def repr_tag(tag_name):
    return {"type": "Hashtag", "name": f"#{tag_name}"}


def include_content(repr, content_obj):
    if not content_obj:
        return

    repr["content"] = common_utils.render_html(
        content_obj.text, content_obj.content_type
    )
    repr["mediaType"] = "text/html"


def include_image(repr, attachment, field="image"):
    if attachment:
        repr[field] = {
            "type": "Image",
            "url": attachment.download_url_original,
            "mediaType": attachment.mimetype or "image/jpeg",
        }
    else:
        repr[field] = None


class MusicEntitySerializer(jsonld.JsonLdSerializer):
    id = serializers.URLField(max_length=500)
    published = serializers.DateTimeField()
    musicbrainzId = serializers.UUIDField(allow_null=True, required=False)
    name = serializers.CharField(max_length=1000)
    attributedTo = serializers.URLField(max_length=500, allow_null=True, required=False)
    updateable_fields = []
    tags = serializers.ListField(
        child=TagSerializer(), min_length=0, required=False, allow_null=True
    )
    mediaType = serializers.ChoiceField(
        choices=common_models.CONTENT_TEXT_SUPPORTED_TYPES,
        default="text/html",
        required=False,
    )
    content = TruncatedCharField(
        truncate_length=common_models.CONTENT_TEXT_MAX_LENGTH,
        required=False,
        allow_null=True,
    )

    def update(self, instance, validated_data):
        return self.update_or_create(validated_data)

    @transaction.atomic
    def update_or_create(self, validated_data):
        instance = self.instance or self.Meta.model(fid=validated_data["id"])
        creating = instance.pk is None
        attributed_to_fid = validated_data.get("attributedTo")
        if attributed_to_fid:
            validated_data["attributedTo"] = actors.get_actor(attributed_to_fid)
        updated_fields = common_utils.get_updated_fields(
            self.updateable_fields, validated_data, instance
        )
        updated_fields = self.validate_updated_data(instance, updated_fields)

        set_ac = False
        if "artist_credit" in updated_fields:
            artist_credit = updated_fields.pop("artist_credit")
            set_ac = True

        if creating:
            instance, created = self.Meta.model.objects.get_or_create(
                fid=validated_data["id"], defaults=updated_fields
            )
            if set_ac:
                instance.artist_credit.set(artist_credit)
        else:
            obj = music_tasks.update_library_entity(instance, updated_fields)
            if set_ac:
                obj.artist_credit.set(artist_credit)

        tags = [t["name"] for t in validated_data.get("tags", []) or []]
        tags_models.set_tags(instance, *tags)
        common_utils.attach_content(
            instance, "description", validated_data.get("description")
        )
        return instance

    def get_tags_repr(self, instance):
        return tag_list(instance.tagged_items.all())

    def validate_updated_data(self, instance, validated_data):
        try:
            attachment_cover = validated_data.pop("attachment_cover")
        except KeyError:
            return validated_data

        if (
            instance.attachment_cover
            and instance.attachment_cover.url == attachment_cover["url"]
        ):
            # we already have the proper attachment
            return validated_data
        # create the attachment by hand so it can be attached as the cover
        validated_data["attachment_cover"] = common_models.Attachment.objects.create(
            mimetype=attachment_cover.get("mediaType"),
            url=attachment_cover["url"],
            actor=instance.attributed_to,
        )
        return validated_data

    def validate(self, data):
        validated_data = super().validate(data)
        if data.get("content"):
            validated_data["description"] = {
                "content_type": data["mediaType"],
                "text": data["content"],
            }
        return validated_data


class ArtistSerializer(MusicEntitySerializer):
    image = ImageSerializer(
        allowed_mimetypes=["image/*"],
        allow_null=True,
        required=False,
        allow_empty_mimetype=True,
    )
    updateable_fields = [
        ("name", "name"),
        ("musicbrainzId", "mbid"),
        ("attributedTo", "attributed_to"),
        ("image", "attachment_cover"),
    ]

    class Meta:
        model = music_models.Artist
        jsonld_mapping = common_utils.concat_dicts(
            MUSIC_ENTITY_JSONLD_MAPPING,
            {
                "released": jsonld.first_val(contexts.FW.released),
                "image": jsonld.first_obj(contexts.AS.image),
            },
        )

    def to_representation(self, instance):
        d = {
            "type": "Artist",
            "id": instance.fid,
            "name": instance.name,
            "published": instance.creation_date.isoformat(),
            "musicbrainzId": str(instance.mbid) if instance.mbid else None,
            "attributedTo": (
                instance.attributed_to.fid if instance.attributed_to else None
            ),
            "tag": self.get_tags_repr(instance),
        }
        include_content(d, instance.description)
        include_image(d, instance.attachment_cover)
        if self.context.get("include_ap_context", self.parent is None):
            d["@context"] = jsonld.get_default_context()
        return d

    create = MusicEntitySerializer.update_or_create


class ArtistCreditSerializer(jsonld.JsonLdSerializer):
    artist = ArtistSerializer()
    joinphrase = serializers.CharField(
        trim_whitespace=False, required=False, allow_null=True, allow_blank=True
    )
    credit = serializers.CharField(
        trim_whitespace=False, required=False, allow_null=True, allow_blank=True
    )
    published = serializers.DateTimeField()
    id = serializers.URLField(max_length=500)

    updateable_fields = [
        ("credit", "credit"),
        ("artist", "artist"),
        ("joinphrase", "joinphrase"),
    ]

    class Meta:
        model = music_models.ArtistCredit
        jsonld_mapping = {
            "artist": jsonld.first_obj(contexts.FW.artist),
            "credit": jsonld.first_val(contexts.FW.credit),
            "index": jsonld.first_val(contexts.FW.index),
            "joinphrase": jsonld.first_val(contexts.FW.joinphrase),
            "published": jsonld.first_val(contexts.AS.published),
        }

    def to_representation(self, instance):
        data = {
            "type": "ArtistCredit",
            "id": instance.fid,
            "artist": ArtistSerializer(
                instance.artist, context={"include_ap_context": False}
            ).data,
            "joinphrase": instance.joinphrase,
            "credit": instance.credit,
            "index": instance.index,
            "published": instance.creation_date.isoformat(),
        }
        if self.context.get("include_ap_context", self.parent is None):
            data["@context"] = jsonld.get_default_context()
        return data


class AlbumSerializer(MusicEntitySerializer):
    released = serializers.DateField(allow_null=True, required=False)
    artist_credit = serializers.ListField(child=ArtistCreditSerializer(), min_length=1)
    image = ImageSerializer(
        allowed_mimetypes=["image/*"],
        allow_null=True,
        required=False,
        allow_empty_mimetype=True,
    )
    updateable_fields = [
        ("name", "title"),
        ("image", "attachment_cover"),
        ("musicbrainzId", "mbid"),
        ("attributedTo", "attributed_to"),
        ("released", "release_date"),
        ("artist_credit", "artist_credit"),
    ]

    class Meta:
        model = music_models.Album
        jsonld_mapping = common_utils.concat_dicts(
            MUSIC_ENTITY_JSONLD_MAPPING,
            {
                "released": jsonld.first_val(contexts.FW.released),
                "artist_credit": jsonld.first_attr(contexts.FW.artist_credit, "@list"),
                "image": jsonld.first_obj(contexts.AS.image),
            },
        )

    def to_representation(self, instance):
        data = {
            "type": "Album",
            "id": instance.fid,
            "name": instance.title,
            "published": instance.creation_date.isoformat(),
            "musicbrainzId": str(instance.mbid) if instance.mbid else None,
            "released": (
                instance.release_date.isoformat() if instance.release_date else None
            ),
            "attributedTo": (
                instance.attributed_to.fid if instance.attributed_to else None
            ),
            "tag": self.get_tags_repr(instance),
        }

        data["artist_credit"] = ArtistCreditSerializer(
            instance.artist_credit.all(),
            context={"include_ap_context": False},
            many=True,
        ).data
        include_content(data, instance.description)
        if instance.attachment_cover:
            include_image(data, instance.attachment_cover)

        if self.context.get("include_ap_context", self.parent is None):
            data["@context"] = jsonld.get_default_context()
        return data

    def validate(self, data):
        validated_data = super().validate(data)
        if not self.parent:
            artist_credit_data = validated_data["artist_credit"]
            if artist_credit_data[0]["artist"].get("type", "Artist") == "Artist":
                acs = []
                for ac in validated_data["artist_credit"]:
                    acs.append(
                        utils.retrieve_ap_object(
                            ac["id"],
                            actor=self.context.get("fetch_actor"),
                            queryset=music_models.ArtistCredit,
                            serializer_class=ArtistCreditSerializer,
                        )
                    )
                validated_data["artist_credit"] = acs
            else:
                # we have an actor as an artist, so it's a channel
                actor = actors.get_actor(artist_credit_data[0]["artist"]["id"])
                validated_data["artist_credit"] = [{"artist": actor.channel.artist}]

        return validated_data

    create = MusicEntitySerializer.update_or_create


class TrackSerializer(MusicEntitySerializer):
    position = serializers.IntegerField(min_value=0, allow_null=True, required=False)
    disc = serializers.IntegerField(min_value=1, allow_null=True, required=False)
    artist_credit = serializers.ListField(child=ArtistCreditSerializer(), min_length=1)
    album = AlbumSerializer()
    license = serializers.URLField(allow_null=True, required=False)
    copyright = serializers.CharField(allow_null=True, required=False)
    image = ImageSerializer(
        allowed_mimetypes=["image/*"],
        allow_null=True,
        required=False,
        allow_empty_mimetype=True,
    )

    updateable_fields = [
        ("name", "title"),
        ("musicbrainzId", "mbid"),
        ("attributedTo", "attributed_to"),
        ("disc", "disc_number"),
        ("position", "position"),
        ("copyright", "copyright"),
        ("license", "license"),
        ("image", "attachment_cover"),
    ]

    class Meta:
        model = music_models.Track
        jsonld_mapping = common_utils.concat_dicts(
            MUSIC_ENTITY_JSONLD_MAPPING,
            {
                "album": jsonld.first_obj(contexts.FW.album),
                "artist_credit": jsonld.first_attr(contexts.FW.artist_credit, "@list"),
                "copyright": jsonld.first_val(contexts.FW.copyright),
                "disc": jsonld.first_val(contexts.FW.disc),
                "license": jsonld.first_id(contexts.FW.license),
                "position": jsonld.first_val(contexts.FW.position),
                "image": jsonld.first_obj(contexts.AS.image),
            },
        )

    def to_representation(self, instance):
        data = {
            "type": "Track",
            "id": instance.fid,
            "name": instance.title,
            "published": instance.creation_date.isoformat(),
            "musicbrainzId": str(instance.mbid) if instance.mbid else None,
            "position": instance.position,
            "disc": instance.disc_number,
            "license": (
                instance.local_license["identifiers"][0]
                if instance.local_license
                else None
            ),
            "copyright": instance.copyright if instance.copyright else None,
            "artist_credit": ArtistCreditSerializer(
                instance.artist_credit.all(),
                context={"include_ap_context": False},
                many=True,
            ).data,
            "album": AlbumSerializer(
                instance.album, context={"include_ap_context": False}
            ).data,
            "attributedTo": (
                instance.attributed_to.fid if instance.attributed_to else None
            ),
            "tag": self.get_tags_repr(instance),
        }
        include_content(data, instance.description)
        include_image(data, instance.attachment_cover)
        if self.context.get("include_ap_context", self.parent is None):
            data["@context"] = jsonld.get_default_context()
        return data

    @transaction.atomic
    def create(self, validated_data):
        from funkwhale_api.music import tasks as music_tasks

        references = {}
        actors_to_fetch = set()
        actors_to_fetch.add(
            common_utils.recursive_getattr(
                validated_data, "attributedTo", permissive=True
            )
        )
        actors_to_fetch.add(
            common_utils.recursive_getattr(
                validated_data, "album.attributedTo", permissive=True
            )
        )
        artist_credit = (
            common_utils.recursive_getattr(
                validated_data, "artist_credit", permissive=True
            )
            or []
        )
        album_artists_credit = (
            common_utils.recursive_getattr(
                validated_data, "album.artist_credit", permissive=True
            )
            or []
        )

        for ac in artist_credit + album_artists_credit:
            actors_to_fetch.add(ac["artist"].get("attributedTo"))

        for url in actors_to_fetch:
            if not url:
                continue
            references[url] = actors.get_actor(url)
        metadata = music_tasks.federation_audio_track_to_metadata(
            validated_data, references
        )

        from_activity = self.context.get("activity")
        if from_activity:
            metadata["from_activity_id"] = from_activity.pk
        track = music_tasks.get_track_from_import_metadata(
            metadata, update_cover=True, query_mb=False
        )
        return track

    def update(self, obj, validated_data):
        if validated_data.get("license"):
            validated_data["license"] = licenses.match(validated_data["license"])
        return super().update(obj, validated_data)


class UploadSerializer(jsonld.JsonLdSerializer):
    type = serializers.ChoiceField(choices=[contexts.AS.Audio])
    id = serializers.URLField(max_length=500)
    library = serializers.URLField(max_length=500)
    url = LinkSerializer(allowed_mimetypes=["audio/*"])
    published = serializers.DateTimeField()
    updated = serializers.DateTimeField(required=False, allow_null=True)
    bitrate = serializers.IntegerField(min_value=0)
    size = serializers.IntegerField(min_value=0)
    duration = serializers.IntegerField(min_value=0)

    track = TrackSerializer(required=True)

    class Meta:
        model = music_models.Upload
        jsonld_mapping = {
            "track": jsonld.first_obj(contexts.FW.track),
            "library": jsonld.first_id(contexts.FW.library),
            "url": jsonld.first_obj(contexts.AS.url),
            "published": jsonld.first_val(contexts.AS.published),
            "updated": jsonld.first_val(contexts.AS.updated),
            "duration": jsonld.first_val(contexts.AS.duration),
            "bitrate": jsonld.first_val(contexts.FW.bitrate),
            "size": jsonld.first_val(contexts.FW.size),
        }

    def validate_url(self, v):
        try:
            v["href"]
        except (KeyError, TypeError):
            raise serializers.ValidationError("Missing href")

        try:
            media_type = v["mediaType"]
        except (KeyError, TypeError):
            raise serializers.ValidationError("Missing mediaType")

        if not media_type or not media_type.startswith("audio/"):
            raise serializers.ValidationError("Invalid mediaType")

        return v

    def validate_library(self, v):
        lb = self.context.get("library")
        if lb:
            if lb.fid != v:
                raise serializers.ValidationError("Invalid library")
            return lb

        actor = self.context.get("actor")

        try:
            library = utils.retrieve_ap_object(
                v,
                actor=self.context.get("fetch_actor"),
                queryset=music_models.Library,
                serializer_class=LibrarySerializer,
            )
        except Exception:
            raise serializers.ValidationError("Invalid library")
        if actor and library.actor != actor:
            raise serializers.ValidationError("Invalid library")
        return library

    def update(self, instance, validated_data):
        return self.create(validated_data)

    @transaction.atomic
    def create(self, validated_data):
        instance = self.instance or None
        if not self.instance:
            try:
                instance = music_models.Upload.objects.get(fid=validated_data["id"])
            except music_models.Upload.DoesNotExist:
                pass

        if instance:
            data = {
                "mimetype": validated_data["url"]["mediaType"],
                "source": validated_data["url"]["href"],
                "creation_date": validated_data["published"],
                "modification_date": validated_data.get("updated"),
                "duration": validated_data["duration"],
                "size": validated_data["size"],
                "bitrate": validated_data["bitrate"],
                "import_status": "finished",
            }
            return music_models.Upload.objects.update_or_create(
                fid=validated_data["id"], defaults=data
            )[0]
        else:
            track = TrackSerializer(
                context={"activity": self.context.get("activity")}
            ).create(validated_data["track"])

            data = {
                "fid": validated_data["id"],
                "mimetype": validated_data["url"]["mediaType"],
                "source": validated_data["url"]["href"],
                "creation_date": validated_data["published"],
                "modification_date": validated_data.get("updated"),
                "track": track,
                "duration": validated_data["duration"],
                "size": validated_data["size"],
                "bitrate": validated_data["bitrate"],
                "library": validated_data["library"],
                "from_activity": self.context.get("activity"),
                "import_status": "finished",
            }
            return music_models.Upload.objects.create(**data)

    def to_representation(self, instance):
        track = instance.track
        d = {
            "type": "Audio",
            "id": instance.get_federation_id(),
            "library": instance.library.fid,
            "name": track.full_name,
            "published": instance.creation_date.isoformat(),
            "bitrate": instance.bitrate,
            "size": instance.size,
            "duration": instance.duration,
            "url": [
                {
                    "href": utils.full_url(instance.listen_url_no_download),
                    "type": "Link",
                    "mediaType": instance.mimetype,
                },
                {
                    "type": "Link",
                    "mediaType": "text/html",
                    "href": utils.full_url(instance.track.get_absolute_url()),
                },
            ],
            "track": TrackSerializer(track, context={"include_ap_context": False}).data,
            "to": (
                contexts.AS.Public
                if instance.library.privacy_level == "everyone"
                else ""
            ),
            "attributedTo": instance.library.actor.fid,
        }
        if instance.modification_date:
            d["updated"] = instance.modification_date.isoformat()

        if self.context.get("include_ap_context", self.parent is None):
            d["@context"] = jsonld.get_default_context()
        return d


class ActorDeleteSerializer(jsonld.JsonLdSerializer):
    fid = serializers.URLField(max_length=500)

    class Meta:
        jsonld_mapping = {"fid": jsonld.first_id(contexts.AS.object)}


class FlagSerializer(jsonld.JsonLdSerializer):
    type = serializers.ChoiceField(choices=[contexts.AS.Flag])
    id = serializers.URLField(max_length=500)
    object = serializers.URLField(max_length=500)
    content = serializers.CharField(required=False, allow_null=True, allow_blank=True)
    actor = serializers.URLField(max_length=500)
    type = serializers.ListField(
        child=TagSerializer(), min_length=0, required=False, allow_null=True
    )

    class Meta:
        jsonld_mapping = {
            "object": jsonld.first_id(contexts.AS.object),
            "content": jsonld.first_val(contexts.AS.content),
            "actor": jsonld.first_id(contexts.AS.actor),
            "type": jsonld.raw(contexts.AS.tag),
        }

    def validate_object(self, v):
        try:
            return utils.get_object_by_fid(v, local=True)
        except ObjectDoesNotExist:
            raise serializers.ValidationError(f"Unknown id {v} for reported object")

    def validate_type(self, tags):
        if tags:
            for tag in tags:
                if tag["name"] in dict(moderation_models.REPORT_TYPES):
                    return tag["name"]
        return "other"

    def validate_actor(self, v):
        try:
            return models.Actor.objects.get(fid=v, domain=self.context["actor"].domain)
        except models.Actor.DoesNotExist:
            raise serializers.ValidationError("Invalid actor")

    def validate(self, data):
        validated_data = super().validate(data)

        return validated_data

    def create(self, validated_data):
        kwargs = {
            "target": validated_data["object"],
            "target_owner": moderation_serializers.get_target_owner(
                validated_data["object"]
            ),
            "target_state": moderation_serializers.get_target_state(
                validated_data["object"]
            ),
            "type": validated_data.get("type", "other"),
            "summary": validated_data.get("content"),
            "submitter": validated_data["actor"],
        }

        report, created = moderation_models.Report.objects.update_or_create(
            fid=validated_data["id"],
            defaults=kwargs,
        )
        moderation_signals.report_created.send(sender=None, report=report)
        return report

    def to_representation(self, instance):
        d = {
            "type": "Flag",
            "id": instance.get_federation_id(),
            "actor": actors.get_service_actor().fid,
            "object": [instance.target.fid],
            "content": instance.summary,
            "tag": [repr_tag(instance.type)],
        }

        if self.context.get("include_ap_context", self.parent is None):
            d["@context"] = jsonld.get_default_context()
        return d


class NodeInfoLinkSerializer(serializers.Serializer):
    href = serializers.URLField()
    rel = serializers.URLField()


class NodeInfoSerializer(serializers.Serializer):
    links = serializers.ListField(child=NodeInfoLinkSerializer(), min_length=1)


class ChannelOutboxSerializer(PaginatedCollectionSerializer):
    type = serializers.ChoiceField(choices=[contexts.AS.OrderedCollection])

    class Meta:
        jsonld_mapping = PAGINATED_COLLECTION_JSONLD_MAPPING

    def to_representation(self, channel):
        conf = {
            "id": channel.actor.outbox_url,
            "page_size": 100,
            "attributedTo": channel.actor,
            "actor": channel.actor,
            "items": channel.library.uploads.for_federation()
            .order_by("-creation_date")
            .filter(track__artist_credit__artist=channel.artist),
            "type": "OrderedCollection",
        }
        r = super().to_representation(conf)
        return r


class ChannelUploadSerializer(jsonld.JsonLdSerializer):
    id = serializers.URLField(max_length=500)
    type = serializers.ChoiceField(choices=[contexts.AS.Audio])
    url = LinkListSerializer(keep_mediatype=["audio/*"], min_length=1)
    name = serializers.CharField()
    published = serializers.DateTimeField(required=False)
    duration = serializers.IntegerField(min_value=0, required=False)
    position = serializers.IntegerField(min_value=0, allow_null=True, required=False)
    disc = serializers.IntegerField(min_value=1, allow_null=True, required=False)
    album = serializers.URLField(max_length=500, required=False)
    license = serializers.URLField(allow_null=True, required=False)
    attributedTo = serializers.URLField(max_length=500, required=False)
    copyright = serializers.CharField(
        allow_null=True,
        required=False,
    )
    image = ImageSerializer(
        allowed_mimetypes=["image/*"],
        allow_null=True,
        required=False,
        allow_empty_mimetype=True,
    )

    mediaType = serializers.ChoiceField(
        choices=common_models.CONTENT_TEXT_SUPPORTED_TYPES,
        default="text/html",
        required=False,
    )
    content = TruncatedCharField(
        truncate_length=common_models.CONTENT_TEXT_MAX_LENGTH,
        required=False,
        allow_blank=True,
        allow_null=True,
    )

    tags = serializers.ListField(
        child=TagSerializer(), min_length=0, required=False, allow_null=True
    )

    class Meta:
        jsonld_mapping = {
            "name": jsonld.first_val(contexts.AS.name),
            "url": jsonld.raw(contexts.AS.url),
            "published": jsonld.first_val(contexts.AS.published),
            "mediaType": jsonld.first_val(contexts.AS.mediaType),
            "content": jsonld.first_val(contexts.AS.content),
            "duration": jsonld.first_val(contexts.AS.duration),
            "album": jsonld.first_id(contexts.FW.album),
            "copyright": jsonld.first_val(contexts.FW.copyright),
            "disc": jsonld.first_val(contexts.FW.disc),
            "license": jsonld.first_id(contexts.FW.license),
            "position": jsonld.first_val(contexts.FW.position),
            "image": jsonld.first_obj(contexts.AS.image),
            "tags": jsonld.raw(contexts.AS.tag),
            "attributedTo": jsonld.first_id(contexts.AS.attributedTo),
        }

    def _validate_album(self, v):
        return utils.retrieve_ap_object(
            v,
            actor=actors.get_service_actor(),
            serializer_class=AlbumSerializer,
            queryset=music_models.Album.objects.filter(
                artist_credit__artist__channel=self.context["channel"]
            ),
        )

    def validate(self, data):
        if not self.context.get("channel"):
            if not data.get("attributedTo"):
                raise serializers.ValidationError(
                    "Missing channel context and no attributedTo available"
                )
            actor = actors.get_actor(data["attributedTo"])
            if not actor.get_channel():
                raise serializers.ValidationError("Not a channel")
            self.context["channel"] = actor.get_channel()
        if data.get("album"):
            data["album"] = self._validate_album(data["album"])
        validated_data = super().validate(data)
        if data.get("content"):
            validated_data["description"] = {
                "content_type": data["mediaType"],
                "text": data["content"],
            }
        return validated_data

    def to_representation(self, upload):
        data = {
            "id": upload.fid,
            "type": "Audio",
            "name": upload.track.title,
            "attributedTo": upload.library.channel.actor.fid,
            "published": upload.creation_date.isoformat(),
            "to": (
                contexts.AS.Public if upload.library.privacy_level == "everyone" else ""
            ),
            "url": [
                {
                    "type": "Link",
                    "mediaType": "text/html",
                    "href": utils.full_url(upload.track.get_absolute_url()),
                },
                {
                    "type": "Link",
                    "mediaType": upload.mimetype,
                    "href": utils.full_url(upload.listen_url_no_download),
                },
            ],
        }
        if upload.track.album:
            data["album"] = upload.track.album.fid
        if upload.track.local_license:
            data["license"] = upload.track.local_license["identifiers"][0]

        include_if_not_none(data, upload.duration, "duration")
        include_if_not_none(data, upload.track.position, "position")
        include_if_not_none(data, upload.track.disc_number, "disc")
        include_if_not_none(data, upload.track.copyright, "copyright")
        include_if_not_none(data["url"][1], upload.bitrate, "bitrate")
        include_if_not_none(data["url"][1], upload.size, "size")
        include_content(data, upload.track.description)
        include_image(data, upload.track.attachment_cover)
        tags = [item.tag.name for item in upload.get_all_tagged_items()]
        if tags:
            data["tag"] = [repr_tag(name) for name in sorted(set(tags))]
            data["summary"] = " ".join([f"#{name}" for name in tags])

        if self.context.get("include_ap_context", True):
            data["@context"] = jsonld.get_default_context()

        return data

    def update(self, instance, validated_data):
        return self.update_or_create(validated_data)

    @transaction.atomic
    def update_or_create(self, validated_data):
        channel = self.context["channel"]
        now = timezone.now()
        track_defaults = {
            "fid": validated_data["id"],
            "position": validated_data.get("position", 1),
            "disc_number": validated_data.get("disc", 1),
            "title": validated_data["name"],
            "copyright": validated_data.get("copyright"),
            "attributed_to": channel.attributed_to,
            "album": validated_data.get("album"),
            "creation_date": validated_data.get("published", now),
        }
        if validated_data.get("license"):
            track_defaults["license"] = licenses.match(validated_data["license"])

        track, created = music_models.Track.objects.update_or_create(
            fid=validated_data["id"],
            defaults=track_defaults,
        )

        # only one artist_credit per channel
        query = (
            Q(
                artist=channel.artist,
            )
            & Q(credit__iexact=channel.artist.name)
            & Q(joinphrase="")
        )
        defaults = {
            "artist": channel.artist,
            "joinphrase": "",
            "credit": channel.artist.name,
        }

        ac_obj = music_tasks.get_best_candidate_or_create(
            music_models.ArtistCredit,
            query,
            defaults=defaults,
            sort_fields=["mbid", "fid"],
        )
        track.artist_credit.set([ac_obj[0].id])

        if "image" in validated_data:
            new_value = self.validated_data["image"]
            common_utils.attach_file(
                track,
                "attachment_cover",
                (
                    {"url": new_value["url"], "mimetype": new_value.get("mediaType")}
                    if new_value
                    else None
                ),
            )

        common_utils.attach_content(
            track, "description", validated_data.get("description")
        )

        tags = [t["name"] for t in validated_data.get("tags", []) or []]
        tags_models.set_tags(track, *tags)

        upload_defaults = {
            "fid": validated_data["id"],
            "track": track,
            "library": channel.library,
            "creation_date": validated_data.get("published", now),
            "duration": validated_data.get("duration"),
            "bitrate": validated_data["url"][0].get("bitrate"),
            "size": validated_data["url"][0].get("size"),
            "mimetype": validated_data["url"][0]["mediaType"],
            "source": validated_data["url"][0]["href"],
            "import_status": "finished",
        }
        upload, created = music_models.Upload.objects.update_or_create(
            fid=validated_data["id"], defaults=upload_defaults
        )
        return upload

    def create(self, validated_data):
        return self.update_or_create(validated_data)


class ChannelCreateUploadSerializer(jsonld.JsonLdSerializer):
    object = serializers.DictField()

    class Meta:
        jsonld_mapping = {
            "object": jsonld.first_obj(contexts.AS.object),
        }

    def to_representation(self, upload):
        payload = {
            "@context": jsonld.get_default_context(),
            "type": self.context.get("type", "Create"),
            "id": utils.full_url(
                reverse(
                    "federation:music:uploads-activity", kwargs={"uuid": upload.uuid}
                )
            ),
            "actor": upload.library.channel.actor.fid,
            "object": ChannelUploadSerializer(
                upload, context={"include_ap_context": False}
            ).data,
        }
        if self.context.get("activity_id_suffix"):
            payload["id"] = os.path.join(
                payload["id"], self.context["activity_id_suffix"]
            )

        return payload

    def validate(self, validated_data):
        serializer = ChannelUploadSerializer(
            data=validated_data["object"], context=self.context, jsonld_expand=False
        )
        serializer.is_valid(raise_exception=True)
        return {"audio_serializer": serializer}

    def save(self, **kwargs):
        return self.validated_data["audio_serializer"].save(**kwargs)


class DeleteSerializer(jsonld.JsonLdSerializer):
    object = serializers.URLField(max_length=500)
    type = serializers.ChoiceField(choices=[contexts.AS.Delete])

    class Meta:
        jsonld_mapping = {"object": jsonld.first_id(contexts.AS.object)}

    def validate_object(self, url):
        try:
            obj = utils.get_object_by_fid(url)
        except utils.ObjectDoesNotExist:
            raise serializers.ValidationError(f"No object matching {url}")
        if isinstance(obj, music_models.Upload):
            obj = obj.track

        return obj

    def validate(self, validated_data):
        if not utils.can_manage(
            validated_data["object"].attributed_to, self.context["actor"]
        ):
            raise serializers.ValidationError("You cannot delete this object")
        return validated_data


class IndexSerializer(jsonld.JsonLdSerializer):
    type = serializers.ChoiceField(
        choices=[contexts.AS.Collection, contexts.AS.OrderedCollection]
    )
    totalItems = serializers.IntegerField(min_value=0)
    id = serializers.URLField(max_length=500)
    first = serializers.URLField(max_length=500)
    last = serializers.URLField(max_length=500)

    class Meta:
        jsonld_mapping = PAGINATED_COLLECTION_JSONLD_MAPPING

    def to_representation(self, conf):
        paginator = Paginator(conf["items"], conf["page_size"])
        first = common_utils.set_query_parameter(conf["id"], page=1)
        current = first
        last = common_utils.set_query_parameter(conf["id"], page=paginator.num_pages)
        d = {
            "id": conf["id"],
            "totalItems": paginator.count,
            "type": "OrderedCollection",
            "current": current,
            "first": first,
            "last": last,
        }
        if self.context.get("include_ap_context", True):
            d["@context"] = jsonld.get_default_context()
        return d


class TrackFavoriteSerializer(jsonld.JsonLdSerializer):
    type = serializers.ChoiceField(choices=[contexts.AS.Like])
    id = serializers.URLField(max_length=500)
    object = serializers.URLField(max_length=500)
    actor = serializers.URLField(max_length=500)

    class Meta:
        jsonld_mapping = {
            "object": jsonld.first_id(contexts.AS.object),
            "actor": jsonld.first_id(contexts.AS.actor),
        }

    def to_representation(self, favorite):
        payload = {
            "type": "Like",
            "id": favorite.fid,
            "actor": favorite.actor.fid,
            "object": favorite.track.fid,
        }
        if self.context.get("include_ap_context", True):
            payload["@context"] = jsonld.get_default_context()
        return payload

    def create(self, validated_data):
        actor = actors.get_actor(validated_data["actor"])
        track = utils.retrieve_ap_object(
            validated_data["object"],
            actor=actors.get_service_actor(),
            serializer_class=TrackSerializer,
        )
        return favorites_models.TrackFavorite.objects.create(
            fid=validated_data.get("id"),
            uuid=uuid.uuid4(),
            actor=actor,
            track=track,
        )


class ListeningSerializer(jsonld.JsonLdSerializer):
    type = serializers.ChoiceField(choices=[contexts.AS.Listen])
    id = serializers.URLField(max_length=500)
    object = serializers.URLField(max_length=500)
    actor = serializers.URLField(max_length=500)

    class Meta:
        jsonld_mapping = {
            "object": jsonld.first_id(contexts.AS.object),
            "actor": jsonld.first_id(contexts.AS.actor),
        }

    def to_representation(self, listening):
        payload = {
            "type": "Listen",
            "id": listening.fid,
            "actor": listening.actor.fid,
            "object": listening.track.fid,
        }
        if self.context.get("include_ap_context", True):
            payload["@context"] = jsonld.get_default_context()
        return payload

    def create(self, validated_data):
        actor = actors.get_actor(validated_data["actor"])
        track = utils.retrieve_ap_object(
            validated_data["object"],
            actor=actors.get_service_actor(),
            serializer_class=TrackSerializer,
        )
        return history_models.Listening.objects.create(
            fid=validated_data.get("id"),
            uuid=validated_data["id"].rstrip("/").split("/")[-1],
            actor=actor,
            track=track,
        )


class PlaylistTrackSerializer(jsonld.JsonLdSerializer):
    type = serializers.ChoiceField(choices=[contexts.FW.PlaylistTrack])
    id = serializers.URLField(max_length=500)
    track = serializers.URLField(max_length=500)
    index = serializers.IntegerField()
    creation_date = serializers.DateTimeField()
    playlist = serializers.URLField(max_length=500, required=False)

    class Meta:
        model = playlists_models.PlaylistTrack
        jsonld_mapping = {
            "track": jsonld.first_id(contexts.FW.track),
            "playlist": jsonld.first_id(contexts.FW.playlist),
            "index": jsonld.first_val(contexts.FW.index),
            "creation_date": jsonld.first_val(contexts.AS.published),
        }

    def to_representation(self, plt):
        payload = {
            "type": "PlaylistTrack",
            "id": plt.fid,
            "track": plt.track.fid,
            "index": plt.index,
            "attributedTo": plt.playlist.actor.fid,
            "published": plt.creation_date.isoformat(),
        }
        if self.context.get("include_ap_context", True):
            payload["@context"] = jsonld.get_default_context()

        if self.context.get("include_playlist", True):
            payload["playlist"] = plt.playlist.fid
        return payload

    def create(self, validated_data):
        track = utils.retrieve_ap_object(
            validated_data["track"],
            actor=self.context.get("fetch_actor"),
            queryset=music_models.Track,
            serializer_class=TrackSerializer,
        )
        playlist = utils.retrieve_ap_object(
            validated_data["playlist"],
            actor=self.context.get("fetch_actor"),
            queryset=playlists_models.Playlist,
            serializer_class=PlaylistTrackSerializer,
        )

        defaults = {
            "track": track,
            "index": validated_data["index"],
            "creation_date": validated_data["creation_date"],
            "playlist": playlist,
        }

        plt, created = playlists_models.PlaylistTrack.objects.update_or_create(
            defaults,
            **{
                "uuid": validated_data["id"].rstrip("/").split("/")[-1],
                "fid": validated_data["id"],
            },
        )

        return plt


class PlaylistSerializer(jsonld.JsonLdSerializer):
    """
    Used for playlist activities
    """

    type = serializers.ChoiceField(choices=[contexts.FW.Playlist, contexts.AS.Create])
    id = serializers.URLField(max_length=500)
    uuid = serializers.UUIDField(required=False)
    name = serializers.CharField(required=False)
    attributedTo = serializers.URLField(max_length=500, required=False)
    published = serializers.DateTimeField(required=False)
    updated = serializers.DateTimeField(required=False)
    audience = serializers.ChoiceField(
        choices=[None, "https://www.w3.org/ns/activitystreams#Public"],
        required=False,
        allow_null=True,
        allow_blank=True,
    )
    updateable_fields = [
        ("name", "title"),
        ("attributedTo", "attributed_to"),
    ]

    class Meta:
        model = playlists_models.Playlist
        jsonld_mapping = common_utils.concat_dicts(
            MUSIC_ENTITY_JSONLD_MAPPING,
            {
                "updated": jsonld.first_val(contexts.AS.published),
                "audience": jsonld.first_id(contexts.AS.audience),
                "attributedTo": jsonld.first_id(contexts.AS.attributedTo),
            },
        )

    def to_representation(self, playlist):
        payload = {
            "type": "Playlist",
            "id": playlist.fid,
            "name": playlist.name,
            "attributedTo": playlist.actor.fid,
            "published": playlist.creation_date.isoformat(),
            "audience": playlist.privacy_level,
        }
        payload["audience"] = (
            contexts.AS.Public if playlist.privacy_level == "everyone" else ""
        )
        if playlist.modification_date:
            payload["updated"] = playlist.modification_date.isoformat()
        if self.context.get("include_ap_context", True):
            payload["@context"] = jsonld.get_default_context()
        return payload

    def create(self, validated_data):
        actor = utils.retrieve_ap_object(
            validated_data["attributedTo"],
            actor=self.context.get("fetch_actor"),
            queryset=models.Actor,
            serializer_class=ActorSerializer,
        )
        ap_to_fw_data = {
            "actor": actor,
            "name": validated_data["name"],
            "creation_date": validated_data["published"],
            "privacy_level": validated_data["audience"],
        }
        playlist, created = playlists_models.Playlist.objects.update_or_create(
            defaults=ap_to_fw_data,
            **{
                "fid": validated_data["id"],
                "uuid": validated_data.get(
                    "uuid", validated_data["id"].rstrip("/").split("/")[-1]
                ),
            },
        )
        return playlist

    def validate(self, data):
        validated_data = super().validate(data)
        if validated_data["audience"] not in [
            "https://www.w3.org/ns/activitystreams#Public",
            "everyone",
        ]:
            raise serializers.ValidationError("Privacy_level must be everyone")

        validated_data["audience"] = "everyone"
        return validated_data


class PlaylistCollectionSerializer(PaginatedCollectionSerializer):
    """
    Used for the federation view.
    """

    type = serializers.ChoiceField(choices=[contexts.FW.Playlist])

    def to_representation(self, playlist):
        conf = {
            "id": playlist.fid,
            "name": playlist.name,
            "page_size": 100,
            "actor": playlist.actor,
            "items": playlist.playlist_tracks.order_by("index").prefetch_related(
                "tracks",
            ),
            "type": "Playlist",
        }
        r = super().to_representation(conf)
        return r
