import base64
import datetime
import logging
from collections.abc import Mapping

import arrow
import magic
import mutagen._util
import mutagen.flac
import mutagen.oggtheora
import mutagen.oggvorbis
from rest_framework import serializers

logger = logging.getLogger(__name__)
NODEFAULT = object()
# default title used when imported tracks miss the `Album` tag, see #122
UNKNOWN_ALBUM = "[Unknown Album]"


class TagNotFound(KeyError):
    pass


class UnsupportedTag(KeyError):
    pass


class ParseError(ValueError):
    pass


def get_id3_tag(f, k):
    if k == "pictures":
        return f.tags.getall("APIC")
    # First we try to grab the standard key
    possible_attributes = [("text", True), ("url", False)]
    for attr, select_first in possible_attributes:
        try:
            v = getattr(f.tags[k], attr)
            if select_first:
                v = v[0]
            return v
        except KeyError:
            break
        except IndexError:
            break
        except AttributeError:
            continue

    # then we fallback on parsing non standard tags
    all_tags = f.tags.getall("TXXX")
    try:
        matches = [t for t in all_tags if t.desc.lower() == k.lower()]
        return matches[0].text[0]
    except (KeyError, IndexError):
        raise TagNotFound(k)


def clean_id3_pictures(apic):
    pictures = []
    for p in list(apic):
        pictures.append(
            {
                "mimetype": p.mime,
                "content": p.data,
                "description": p.desc,
                "type": p.type.real,
            }
        )
    return pictures


def get_mp4_tag(f, k):
    if k == "pictures":
        return f.get("covr", [])
    raw_value = f.get(k, None)

    if not raw_value:
        raise TagNotFound(k)

    value = raw_value[0]
    try:
        return value.decode()
    except AttributeError:
        return value


def get_mp4_position(raw_value):
    return raw_value[0]


def clean_mp4_pictures(raw_pictures):
    pictures = []
    for p in list(raw_pictures):
        if p.imageformat == p.FORMAT_JPEG:
            mimetype = "image/jpeg"
        elif p.imageformat == p.FORMAT_PNG:
            mimetype = "image/png"
        else:
            continue
        pictures.append(
            {
                "mimetype": mimetype,
                "content": bytes(p),
                "description": "",
                "type": mutagen.id3.PictureType.COVER_FRONT,
            }
        )
    return pictures


def get_flac_tag(f, k):
    if k == "pictures":
        return f.pictures
    try:
        return f.get(k, [])[0]
    except (KeyError, IndexError):
        raise TagNotFound(k)


def clean_flac_pictures(apic):
    pictures = []
    for p in list(apic):
        pictures.append(
            {
                "mimetype": p.mime,
                "content": p.data,
                "description": p.desc,
                "type": p.type.real,
            }
        )
    return pictures


def clean_ogg_coverart(metadata_block_picture):
    pictures = []
    for b64_data in [metadata_block_picture]:
        try:
            data = base64.b64decode(b64_data)
        except (TypeError, ValueError):
            continue

        mime = magic.Magic(mime=True)
        mime.from_buffer(data)

        pictures.append(
            {
                "mimetype": mime.from_buffer(data),
                "content": data,
                "description": "",
                "type": mutagen.id3.PictureType.COVER_FRONT,
            }
        )
    return pictures


def clean_ogg_pictures(metadata_block_picture):
    pictures = []
    for b64_data in [metadata_block_picture]:
        try:
            data = base64.b64decode(b64_data)
        except (TypeError, ValueError):
            continue

        try:
            picture = mutagen.flac.Picture(data)
        except mutagen.flac.FLACError:
            continue

        pictures.append(
            {
                "mimetype": picture.mime,
                "content": picture.data,
                "description": "",
                "type": picture.type.real,
            }
        )
    return pictures


def get_mp3_recording_id(f, k):
    try:
        return [t for t in f.tags.getall("UFID") if "musicbrainz.org" in t.owner][
            0
        ].data.decode("utf-8")
    except IndexError:
        raise TagNotFound(k)


def get_mp3_comment(f, k):
    keys_to_try = ["COMM", "COMM::eng"]
    for key in keys_to_try:
        try:
            return get_id3_tag(f, key)
        except TagNotFound:
            pass

    raise TagNotFound("COMM")


VALIDATION = {}

CONF = {
    "OggOpus": {
        "getter": lambda f, k: f[k][0],
        "fields": {
            "position": {"field": "TRACKNUMBER"},
            "disc_number": {"field": "DISCNUMBER"},
            "title": {},
            "artist": {},
            "artists": {},
            "album_artist": {"field": "albumartist"},
            "album": {},
            "date": {"field": "date"},
            "musicbrainz_albumid": {},
            "musicbrainz_artistid": {},
            "musicbrainz_albumartistid": {},
            "mbid": {"field": "musicbrainz_trackid"},
            "license": {},
            "copyright": {},
            "genre": {},
            "pictures": [
                {
                    "field": "metadata_block_picture",
                    "to_application": clean_ogg_pictures,
                },
                {
                    "field": "coverart",
                    "to_application": clean_ogg_coverart,
                },
            ],
            "comment": {"field": "comment"},
        },
    },
    "OggVorbis": {
        "getter": lambda f, k: f[k][0],
        "fields": {
            "position": {"field": "TRACKNUMBER"},
            "disc_number": {"field": "DISCNUMBER"},
            "title": {},
            "artist": {},
            "artists": {},
            "album_artist": {"field": "albumartist"},
            "album": {},
            "date": {"field": "date"},
            "musicbrainz_albumid": {},
            "musicbrainz_artistid": {},
            "musicbrainz_albumartistid": {},
            "mbid": {"field": "musicbrainz_trackid"},
            "license": {},
            "copyright": {},
            "genre": {},
            "pictures": [
                {
                    "field": "metadata_block_picture",
                    "to_application": clean_ogg_pictures,
                },
                {
                    "field": "coverart",
                    "to_application": clean_ogg_coverart,
                },
            ],
            "comment": {"field": "comment"},
        },
    },
    "OggTheora": {
        "getter": lambda f, k: f[k][0],
        "fields": {
            "position": {"field": "TRACKNUMBER"},
            "disc_number": {"field": "DISCNUMBER"},
            "title": {},
            "artist": {},
            "artists": {},
            "album_artist": {"field": "albumartist"},
            "album": {},
            "date": {"field": "date"},
            "musicbrainz_albumid": {"field": "MusicBrainz Album Id"},
            "musicbrainz_artistid": {"field": "MusicBrainz Artist Id"},
            "musicbrainz_albumartistid": {"field": "MusicBrainz Album Artist Id"},
            "mbid": {"field": "MusicBrainz Track Id"},
            "license": {},
            "copyright": {},
            "genre": {},
            "comment": {"field": "comment"},
        },
    },
    "ID3": {
        "getter": get_id3_tag,
        "clean_pictures": clean_id3_pictures,
        "fields": {
            "position": {"field": "TRCK"},
            "disc_number": {"field": "TPOS"},
            "title": {"field": "TIT2"},
            "artist": {"field": "TPE1"},
            "artists": {"field": "ARTISTS"},
            "album_artist": {"field": "TPE2"},
            "album": {"field": "TALB"},
            "date": {"field": "TDRC"},
            "musicbrainz_albumid": {"field": "MusicBrainz Album Id"},
            "musicbrainz_artistid": {"field": "MusicBrainz Artist Id"},
            "genre": {"field": "TCON"},
            "musicbrainz_albumartistid": {"field": "MusicBrainz Album Artist Id"},
            "mbid": {"field": "UFID", "getter": get_mp3_recording_id},
            "pictures": {},
            "license": {"field": "WCOP"},
            "copyright": {"field": "TCOP"},
            "comment": {"field": "COMM", "getter": get_mp3_comment},
        },
    },
    "MP4": {
        "getter": get_mp4_tag,
        "clean_pictures": clean_mp4_pictures,
        "fields": {
            "position": {"field": "trkn", "to_application": get_mp4_position},
            "disc_number": {"field": "disk", "to_application": get_mp4_position},
            "title": {"field": "©nam"},
            "artist": {"field": "©ART"},
            "artists": {"field": "----:com.apple.iTunes:ARTISTS"},
            "album_artist": {"field": "aART"},
            "album": {"field": "©alb"},
            "date": {"field": "©day"},
            "musicbrainz_albumid": {
                "field": "----:com.apple.iTunes:MusicBrainz Album Id"
            },
            "musicbrainz_artistid": {
                "field": "----:com.apple.iTunes:MusicBrainz Artist Id"
            },
            "genre": {"field": "©gen"},
            "musicbrainz_albumartistid": {
                "field": "----:com.apple.iTunes:MusicBrainz Album Artist Id"
            },
            "mbid": {"field": "----:com.apple.iTunes:MusicBrainz Track Id"},
            "pictures": {},
            "license": {"field": "----:com.apple.iTunes:LICENSE"},
            "copyright": {"field": "cprt"},
            "comment": {"field": "©cmt"},
        },
    },
    "FLAC": {
        "getter": get_flac_tag,
        "clean_pictures": clean_flac_pictures,
        "fields": {
            "position": {"field": "tracknumber"},
            "disc_number": {"field": "discnumber"},
            "title": {},
            "artist": {},
            "artists": {},
            "album_artist": {"field": "albumartist"},
            "album": {},
            "date": {"field": "date"},
            "musicbrainz_albumid": {},
            "musicbrainz_artistid": {},
            "musicbrainz_albumartistid": {},
            "genre": {},
            "mbid": {"field": "musicbrainz_trackid"},
            "test": {},
            "pictures": {},
            "license": {},
            "copyright": {},
            "comment": {},
        },
    },
}

CONF["MP3"] = CONF["ID3"]
CONF["AIFF"] = CONF["ID3"]

ALL_FIELDS = [
    "position",
    "disc_number",
    "title",
    "artist",
    "album_artist",
    "album",
    "date",
    "musicbrainz_albumid",
    "musicbrainz_artistid",
    "musicbrainz_albumartistid",
    "mbid",
    "license",
    "copyright",
    "comment",
]


class Metadata(Mapping):
    def __init__(self, filething, kind=mutagen.File):
        self._file = kind(filething)
        if self._file is None:
            raise ValueError(f"Cannot parse metadata from {filething}")
        if len(self._file) == 0:
            raise ValueError(f"No tags found in {filething}")
        self.fallback = self.load_fallback(filething, self._file)
        ft = self.get_file_type(self._file)
        try:
            self._conf = CONF[ft]
        except KeyError:
            raise ValueError(f"Unsupported format {ft}")

    def get_file_type(self, f):
        return f.__class__.__name__

    def load_fallback(self, filething, parent):
        """
        In some situations, such as Ogg Theora files tagged with MusicBrainz Picard,
        part of the tags are only available in the ogg vorbis comments
        """
        try:
            filething.seek(0)
        except AttributeError:
            pass
        if isinstance(parent, mutagen.oggtheora.OggTheora):
            try:
                return Metadata(filething, kind=mutagen.oggvorbis.OggVorbis)
            except (ValueError, mutagen._util.MutagenError):
                raise
                pass

    def get(self, key, default=NODEFAULT):
        try:
            return self._get_from_self(key)
        except TagNotFound:
            if not self.fallback:
                if default != NODEFAULT:
                    return default
                else:
                    raise
            else:
                return self.fallback.get(key, default=default)
        except UnsupportedTag:
            if not self.fallback:
                raise
            else:
                return self.fallback.get(key, default=default)

    def all(self):
        """
        Return a dict with all support metadata fields, if they are available
        """
        final = {}
        for field in self._conf["fields"]:
            if field in ["pictures"]:
                continue
            value = self.get(field, None)
            if value is None:
                continue
            final[field] = str(value)

        return final

    def _get_from_self(self, key, default=NODEFAULT):
        try:
            field_confs = self._conf["fields"][key]
        except KeyError:
            raise UnsupportedTag(f"{key} is not supported for this file format")
        if not isinstance(field_confs, list):
            field_confs = [field_confs]

        for field_conf in field_confs:
            real_key = field_conf.get("field", key)
            try:
                getter = field_conf.get("getter", self._conf["getter"])
                v = getter(self._file, real_key)
            except KeyError:
                continue

            converter = field_conf.get("to_application")
            if converter:
                v = converter(v)
            field = VALIDATION.get(key)
            if field:
                v = field.to_python(v)
            return v
        if default == NODEFAULT:
            raise TagNotFound(real_key)
        return default

    def get_picture(self, *picture_types):
        if not picture_types:
            raise ValueError("You need to request at least one picture type")
        ptypes = [
            getattr(mutagen.id3.PictureType, picture_type.upper())
            for picture_type in picture_types
        ]

        try:
            pictures = self.get("pictures")
        except (UnsupportedTag, TagNotFound):
            return

        cleaner = self._conf.get("clean_pictures", lambda v: v)
        pictures = cleaner(pictures)
        if not pictures:
            return
        for ptype in ptypes:
            for p in pictures:
                if p["type"] == ptype:
                    return p

    def __getitem__(self, key):
        return self.get(key)

    def __len__(self):
        return 1

    def __iter__(self):
        yield from self._conf["fields"]


class ArtistField(serializers.Field):
    def __init__(self, *args, **kwargs):
        self.for_album = kwargs.pop("for_album", False)
        super().__init__(*args, **kwargs)

    def get_value(self, data):
        if self.for_album:
            keys = [
                ("artists", "album_artist"),
                ("names", "artists"),
                ("mbids", "musicbrainz_albumartistid"),
            ]
        else:
            keys = [
                ("artists", "artists"),
                ("names", "artist"),
                ("mbids", "musicbrainz_artistid"),
            ]

        final = {}
        for field, key in keys:
            final[field] = data.get(key, None)

        return final

    def _get_artist_credit_tuple(self, mbids, data):
        from . import tasks

        names_artists_credits_tuples = tasks.parse_credits(
            data.get("names", ""), None, None
        )

        artist_artists_credits_tuples = tasks.parse_credits(
            data.get("artists", ""), None, None
        )

        len_mbids = len(mbids)
        if (
            len(names_artists_credits_tuples) != len_mbids
            and len(artist_artists_credits_tuples) != len_mbids
        ):
            logger.warning(
                "Error parsing artist data, not the same amount of mbids and parsed artists. \
                Probably because the artist parser found more artists than there is."
            )

        if len(names_artists_credits_tuples) > len(artist_artists_credits_tuples):
            return names_artists_credits_tuples
        return artist_artists_credits_tuples

    def _get_mbids(self, raw_mbids):
        # we have multiple mbid values that can be separated by various separators
        separators = [";", ",", "/"]
        # we get a list like that if tagged via musicbrainz
        # ae29aae4-abfb-4609-8f54-417b1f4d64cc; 3237b5a8-ae44-400c-aa6d-cea51f0b9074;
        mbids = [raw_mbids]
        if raw_mbids:
            for separator in separators:
                if separator in raw_mbids:
                    mbids = [m.strip() for m in raw_mbids.split(separator)]
                    break
        return mbids

    def _format_artist_credit_list(self, artists_credits_tuples, mbids):
        final_artist_credits = []
        for i, ac in enumerate(artists_credits_tuples):
            artist_credit = {
                "credit": ac[0],
                "mbid": (mbids[i] if 0 <= i < len(mbids) else None),
                "joinphrase": ac[1],
                "index": i,
            }
            final_artist_credits.append(artist_credit)

        return final_artist_credits

    def to_internal_value(self, data):
        if (
            self.context.get("strict", True)
            and not data.get("artists", [])
            and not data.get("names", [])
        ):
            raise serializers.ValidationError("This field is required.")
        mbids = self._get_mbids(data["mbids"])
        # now, we split on artist names
        artists_credits_tuples = self._get_artist_credit_tuple(mbids, data)
        final_artist_credits = self._format_artist_credit_list(
            artists_credits_tuples, mbids
        )

        field = serializers.ListField(
            child=ArtistSerializer(strict=self.context.get("strict", True)),
            min_length=1,
        )
        return field.to_internal_value(final_artist_credits)


class AlbumField(serializers.Field):
    def get_value(self, data):
        return data

    def to_internal_value(self, data):
        try:
            title = data.get("album") or ""
        except TagNotFound:
            title = ""

        title = title.strip() or UNKNOWN_ALBUM
        final = {
            "title": title,
            "release_date": data.get("date", None),
            "mbid": data.get("musicbrainz_albumid", None),
        }
        artist_credit_field = ArtistField(for_album=True)
        payload = artist_credit_field.get_value(data)
        try:
            artist_credit = artist_credit_field.to_internal_value(payload)
        except serializers.ValidationError as e:
            artist_credit = []
            logger.debug("Ignoring validation error on album artist_credit: %s", e)
        album_serializer = AlbumSerializer(data=final)
        album_serializer.is_valid(raise_exception=True)
        album_serializer.validated_data["artist_credit"] = artist_credit

        return album_serializer.validated_data


class CoverDataField(serializers.Field):
    def get_value(self, data):
        return data

    def to_internal_value(self, data):
        return data.get_picture("cover_front", "other")


class PermissiveDateField(serializers.CharField):
    def to_internal_value(self, value):
        if not value:
            return None
        value = super().to_internal_value(str(value))
        ADDITIONAL_FORMATS = [
            "%Y-%d-%m %H:%M",  # deezer date format
            "%Y-%W",  # weird date format based on week number, see #718
        ]

        for date_format in ADDITIONAL_FORMATS:
            try:
                parsed = datetime.datetime.strptime(value, date_format)
            except ValueError:
                continue
            else:
                return datetime.date(parsed.year, parsed.month, parsed.day)

        try:
            parsed = arrow.get(str(value))
            return datetime.date(parsed.year, parsed.month, parsed.day)
        except (arrow.parser.ParserError, ValueError):
            pass

        return None


def extract_tags_from_genre(string):
    tags = []
    delimiter = "@@@@@"
    for d in [" - ", ", ", ",", "; ", ";", "/"]:
        # Replace common tags separators by a custom delimiter
        string = string.replace(d, delimiter)

    # loop on the parts (splitting on our custom delimiter)
    for tag in string.split(delimiter):
        if not tag:
            continue
        tags.append(tag)
    return tags


class TagsField(serializers.CharField):
    def get_value(self, data):
        return data

    def to_internal_value(self, data):
        try:
            value = data.get("genre") or ""
        except TagNotFound:
            return []
        value = super().to_internal_value(str(value))

        return extract_tags_from_genre(value)


class MBIDField(serializers.UUIDField):
    def __init__(self, *args, **kwargs):
        kwargs.setdefault("allow_null", True)
        kwargs.setdefault("required", False)
        super().__init__(*args, **kwargs)

    def to_internal_value(self, v):
        if v in ["", None]:
            return None
        return super().to_internal_value(v)


class ArtistSerializer(serializers.Serializer):
    credit = serializers.CharField(required=False, allow_null=True, allow_blank=True)
    mbid = MBIDField()
    joinphrase = serializers.CharField(
        trim_whitespace=False, required=False, allow_null=True, allow_blank=True
    )

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

    def validate_credit(self, v):
        if self.strict and not v:
            raise serializers.ValidationError("This field is required.")
        return v


class AlbumSerializer(serializers.Serializer):
    title = serializers.CharField(required=False, allow_null=True)
    mbid = MBIDField()
    release_date = PermissiveDateField(
        required=False, allow_null=True, allow_blank=True
    )

    def validate_title(self, v):
        if self.context.get("strict", True) and not v:
            raise serializers.ValidationError("This field is required.")
        return v


def get_valid_position(v):
    if v <= 0:
        v = 1
    return v


class PositionField(serializers.CharField):
    def to_internal_value(self, v):
        v = super().to_internal_value(v)
        if not v:
            return v

        try:
            return get_valid_position(int(v))
        except ValueError:
            # maybe the position is of the form "1/4"
            pass

        try:
            return get_valid_position(int(v.split("/")[0]))
        except (ValueError, AttributeError, IndexError):
            return


class DescriptionField(serializers.CharField):
    def get_value(self, data):
        return data

    def to_internal_value(self, data):
        try:
            value = data.get("comment") or None
        except TagNotFound:
            return None
        if not value:
            return None
        value = super().to_internal_value(value)
        return {"text": value, "content_type": "text/plain"}


class TrackMetadataSerializer(serializers.Serializer):
    title = serializers.CharField(required=False, allow_null=True)
    position = PositionField(allow_blank=True, allow_null=True, required=False)
    disc_number = PositionField(allow_blank=True, allow_null=True, required=False)
    copyright = serializers.CharField(allow_blank=True, allow_null=True, required=False)
    license = serializers.CharField(allow_blank=True, allow_null=True, required=False)
    mbid = MBIDField()
    tags = TagsField(allow_blank=True, allow_null=True, required=False)
    description = DescriptionField(allow_null=True, allow_blank=True, required=False)

    album = AlbumField()
    artist_credit = ArtistField()
    cover_data = CoverDataField(required=False)

    remove_blank_null_fields = [
        "copyright",
        "description",
        "license",
        "position",
        "disc_number",
        "mbid",
        "tags",
    ]

    def validate_title(self, v):
        if self.context.get("strict", True) and not v:
            raise serializers.ValidationError("This field is required.")
        return v

    def validate(self, validated_data):
        validated_data = super().validate(validated_data)
        for field in self.remove_blank_null_fields:
            try:
                v = validated_data[field]
            except KeyError:
                continue
            if v in ["", None, []]:
                validated_data.pop(field)
        validated_data["album"]["cover_data"] = validated_data.pop("cover_data", None)
        return validated_data


class FakeMetadata(Mapping):
    def __init__(self, data, picture=None):
        self.data = data
        self.picture = None

    def __getitem__(self, key):
        return self.data[key]

    def __len__(self):
        return len(self.data)

    def __iter__(self):
        yield from self.data

    def get_picture(self, *args):
        return self.picture
