import logging
from dataclasses import dataclass
from typing import Optional
from uuid import uuid4

import musicbrainzngs
import requests
from django.conf import settings
from django.core.files.base import ContentFile
from django.db import models
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from django_extensions.db.models import TimeStampedModel
from imagekit.models import ImageSpecField
from imagekit.processors import ResizeToFit
from music.allmusic import get_allmusic_slug, scrape_data_from_allmusic
from music.bandcamp import get_bandcamp_slug
from music.musicbrainz import (
    get_album_metadata_with_artist,
    get_recording_mbid_exact,
    get_track_metadata_with_artist,
)
from music.theaudiodb import lookup_album_from_tadb, lookup_artist_from_tadb
from music.utils import clean_artist_name
from scrobbles.dataclasses import BaseLogData
from scrobbles.constants import MediaType
from scrobbles.mixins import ScrobblableConstants, ScrobblableItem, ObjectWithGenres
from taggit.managers import TaggableManager

logger = logging.getLogger(__name__)
BNULL = {"blank": True, "null": True}


@dataclass
class TrackLogData(BaseLogData):
    mopidy_source: Optional[str] = None
    rockbox_info: Optional[str] = None
    rating: Optional[int] = None


class Artist(TimeStampedModel):
    """Represents a music artist.

    # Lookup or create by title alone
    >>> Artist.find_or_create(name="Bon Iver")

    # Lookup or create by MB id alone
    >>> Artist.find_or_create(musicbrainz_id="0307edfc-437c-4b48-8700-80680e66a228")

    """

    uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
    name = models.CharField(max_length=255)
    biography = models.TextField(**BNULL)
    theaudiodb_id = models.CharField(max_length=255, unique=True, **BNULL)
    theaudiodb_genre = models.CharField(max_length=255, **BNULL)
    theaudiodb_mood = models.CharField(max_length=255, **BNULL)
    musicbrainz_id = models.CharField(max_length=255, **BNULL)
    allmusic_id = models.CharField(max_length=100, **BNULL)
    bandcamp_id = models.CharField(max_length=100, **BNULL)
    thumbnail = models.ImageField(upload_to="artist/", **BNULL)
    thumbnail_small = ImageSpecField(
        source="thumbnail",
        processors=[ResizeToFit(100, 100)],
        format="JPEG",
        options={"quality": 60},
    )
    thumbnail_medium = ImageSpecField(
        source="thumbnail",
        processors=[ResizeToFit(300, 300)],
        format="JPEG",
        options={"quality": 75},
    )
    alt_names = models.TextField(**BNULL)

    class Meta:
        unique_together = [["name", "musicbrainz_id"]]

    def __str__(self):
        return self.name

    @property
    def primary_image_url(self) -> str:
        if self.thumbnail:
            return self.thumbnail.url
        if self.album_set.first().cover_image:
            return self.album_set.first().cover_image.url
        return ""

    @property
    def mb_link(self) -> str:
        if self.musicbrainz_id:
            return f"https://musicbrainz.org/artist/{self.musicbrainz_id}"
        return ""

    @property
    def allmusic_link(self):
        if self.allmusic_id:
            return f"https://www.allmusic.com/artist/{self.allmusic_id}"
        return ""

    @property
    def bandcamp_link(self):
        if self.bandcamp_id:
            return f"https://{self.bandcamp_id}.bandcamp.com/"
        return ""

    def get_absolute_url(self):
        return reverse("music:artist_detail", kwargs={"slug": self.uuid})

    def scrobbles(self):
        from scrobbles.models import Scrobble

        return Scrobble.objects.filter(
            track__in=self.track_set.all()
        ).order_by("-timestamp")

    @property
    def tracks(self):
        return (
            self.track_set.all()
            .annotate(scrobble_count=models.Count("scrobble"))
            .order_by("-scrobble_count")
        )

    def charts(self):
        from scrobbles.models import ChartRecord

        return ChartRecord.objects.filter(track__artist=self).order_by("-year")

    def scrape_allmusic(self, force=False) -> None:
        if not self.allmusic_id or force:
            slug = get_allmusic_slug(self.name)
            if not slug:
                logger.info(
                    "No allmusic link found", extra={"track_id": self.id}
                )
                return
            self.allmusic_id = slug
            self.save(update_fields=["allmusic_id"])

    def scrape_bandcamp(self, force=False) -> None:
        if not self.bandcamp_id or force:
            slug = get_bandcamp_slug(self.name)
            if not slug:
                logger.info(
                    "No bandcamp link found", extra={"track_id": self.id}
                )
                return
            self.bandcamp_id = slug
            self.save(update_fields=["bandcamp_id"])

    def fix_metadata(self, force_update=False):
        tadb_info = {}
        if self.theaudiodb_id and force_update:
            tadb_info = lookup_artist_from_tadb(self.theaudiodb_id)

        if not self.theaudiodb_id or (force_update and not tadb_info):
            tadb_info = lookup_artist_from_tadb(self.name)

        if not tadb_info:
            logger.warn(
                f"No response from TADB for artist {self.name}, try force_update=True"
            )
            return

        self.biography = tadb_info["biography"]
        self.theaudiodb_genre = tadb_info["genre"]
        self.theaudiodb_mood = tadb_info["mood"]

        thumb_url = tadb_info.get("thumb_url", "")
        if thumb_url:
            r = requests.get(thumb_url)
            if r.status_code == 200:
                fname = f"{self.name}_{self.uuid}.jpg"
                self.thumbnail.save(fname, ContentFile(r.content), save=True)

    @property
    def rym_link(self):
        artist_slug = self.name.lower().replace(" ", "-").replace(",", "")
        return f"https://rateyourmusic.com/artist/{artist_slug}/"

    @property
    def bandcamp_search_link(self):
        artist = self.name.lower()
        return f"https://bandcamp.com/search?q={artist}&item_type=b"

    @classmethod
    def find_or_create(
        cls, name: str, album_name: str = "", track_name: str = ""
    ) -> "Artist":
        """The biggest challenge to finding artists is that the search often
        fails miserably unless you can look it up along with an album or a track name.

        Thus, when we find or create an artist, we should always provide an optional
        album name or track name, but probably not both."""
        if album_name:
            logger.info(
                f"Looking for artist with name {name} and album {album_name}"
            )
        if track_name:
            logger.info(
                f"Looking for artist with name {name} and track {track_name}"
            )
        keys = {}

        name = clean_artist_name(name)
        keys["name"] = name
        artist = cls.objects.filter(name=name).first()

        if artist:
            return artist

        # alt_name = None
        artist_dict = {}
        if album_name:
            album_dict = get_album_metadata_with_artist(album_name, name)
            if album_dict:
                artist_dict = album_dict.get("primary_artist")
        if track_name:
            track_dict = get_track_metadata_with_artist(track_name, name)
            if track_dict:
                artist_dict = track_dict.get("primary_artist")

        if not artist_dict:
            artist, created = cls.objects.get_or_create(name=name)
            if created:
                artist.fix_metadata()
            return artist

        musicbrainz_id = artist_dict.get("mbid")
        found_name = artist_dict.get("name", name)
        if found_name and name != found_name:
            alt_name = found_name

        artist = cls.objects.filter(
            name=found_name, musicbrainz_id=musicbrainz_id
        ).first()
        if not artist:
            artist = cls.objects.create(
                name=found_name,
                musicbrainz_id=musicbrainz_id,
            )
            artist.fix_metadata()

        # TODO: See if this alt_names stuff actually works or causes hard to debug problems
        # If we did find our artist, but the found name is slightly differnt, record that
        # if artist and alt_name:
        #    if not artist.alt_names:
        #        artist.alt_names = alt_name
        #    else:
        #        artist.alt_names += f"\\{alt_name}"
        #    logger.info(
        #        f"Add alt_name {alt_name} to artist {artist}",
        #        extra={"alt_name": alt_name, "artist_id": artist.id},
        #    )
        #    artist.save(update_fields=["alt_names"])

        return artist


class Album(TimeStampedModel):
    uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
    name = models.CharField(max_length=255)
    album_artist = models.ForeignKey(
        Artist, related_name="albums", on_delete=models.DO_NOTHING, **BNULL
    )
    artists = models.ManyToManyField(Artist)
    year = models.IntegerField(**BNULL)
    musicbrainz_id = models.CharField(max_length=255, unique=True, **BNULL)
    musicbrainz_releasegroup_id = models.CharField(max_length=255, **BNULL)
    musicbrainz_albumartist_id = models.CharField(max_length=255, **BNULL)
    cover_image = models.ImageField(upload_to="albums/", **BNULL)
    cover_image_small = ImageSpecField(
        source="cover_image",
        processors=[ResizeToFit(100, 100)],
        format="JPEG",
        options={"quality": 60},
    )
    cover_image_medium = ImageSpecField(
        source="cover_image",
        processors=[ResizeToFit(300, 300)],
        format="JPEG",
        options={"quality": 75},
    )
    theaudiodb_id = models.CharField(max_length=255, unique=True, **BNULL)
    theaudiodb_description = models.TextField(**BNULL)
    theaudiodb_year_released = models.IntegerField(**BNULL)
    theaudiodb_score = models.FloatField(**BNULL)
    theaudiodb_score_votes = models.IntegerField(**BNULL)
    theaudiodb_genre = models.CharField(max_length=255, **BNULL)
    theaudiodb_style = models.CharField(max_length=255, **BNULL)
    theaudiodb_mood = models.CharField(max_length=255, **BNULL)
    theaudiodb_speed = models.CharField(max_length=255, **BNULL)
    theaudiodb_theme = models.CharField(max_length=255, **BNULL)
    allmusic_id = models.CharField(max_length=255, **BNULL)
    allmusic_rating = models.IntegerField(**BNULL)
    allmusic_review = models.TextField(**BNULL)
    bandcamp_id = models.CharField(max_length=100, **BNULL)
    rateyourmusic_id = models.CharField(max_length=255, **BNULL)
    wikipedia_slug = models.CharField(max_length=255, **BNULL)
    discogs_id = models.CharField(max_length=255, **BNULL)
    wikidata_id = models.CharField(max_length=255, **BNULL)
    alt_names = models.TextField(**BNULL)

    def __str__(self) -> str:
        return "{} by {}".format(self.name, self.album_artist)

    def get_absolute_url(self):
        return reverse("music:album_detail", kwargs={"slug": self.uuid})

    def scrobbles(self):
        from scrobbles.models import Scrobble

        return Scrobble.objects.filter(
            track__in=self.track_set.all()
        ).order_by("-timestamp")

    @property
    def primary_image_url(self) -> str:
        if self.cover_image:
            return self.cover_image_medium.url
        return ""

    @property
    def tracks(self):
        return (
            self.track_set.all()
            .annotate(scrobble_count=models.Count("scrobble"))
            .order_by("-scrobble_count")
        )

    def fix_album_artist(self):
        from music.utils import get_or_create_various_artists

        multiple_artists = self.artists.count() > 1
        if multiple_artists:
            self.album_artist = get_or_create_various_artists()
        else:
            self.album_artist = self.artists.first()
        self.save(update_fields=["album_artist"])

    def scrape_allmusic(self, force=False) -> None:
        if not self.name:
            logger.warning(
                "Album without a name cannot be scraped",
                extra={"album_id": self.id},
            )
            return

        if self.album_artist and (not self.allmusic_id or force):
            slug = get_allmusic_slug(self.album_artist.name, self.name)
            if not slug:
                logger.info(
                    f"No allmsuic link for {self} by {self.album_artist}"
                )
                return
            self.allmusic_id = slug
            self.save(update_fields=["allmusic_id"])

        allmusic_data = None
        if self.allmusic_link:
            allmusic_data = scrape_data_from_allmusic(self.allmusic_link)

        if not allmusic_data:
            logger.info(f"No allmsuic data for {self} by {self.album_artist}")
            return

        self.allmusic_review = allmusic_data["review"]
        self.allmusic_rating = allmusic_data["rating"]
        self.save(update_fields=["allmusic_review", "allmusic_rating"])

    def scrape_theaudiodb(self) -> None:
        artist = "Various Artists"
        if self.album_artist:
            artist = self.album_artist.name
        album_data = lookup_album_from_tadb(self.name, artist)
        if not album_data.get("theaudiodb_id"):
            logger.info(f"No data for {self} found in TheAudioDB")
            return

        try:
            Album.objects.filter(pk=self.pk).update(**album_data)
        except:
            logger.info(
                f"Could not save info for album {self} with data {album_data}"
            )

    def scrape_bandcamp(self, force=False) -> None:
        if not self.bandcamp_id or force:
            slug = get_bandcamp_slug(self.album_artist.name, self.name)
            if not slug:
                logger.info(f"No bandcamp link for {self}")
                return
            self.bandcamp_id = slug
            self.save(update_fields=["bandcamp_id"])

    def fix_metadata(self):
        if (
            not self.musicbrainz_albumartist_id
            or not self.year
            or not self.musicbrainz_releasegroup_id
        ):
            musicbrainzngs.set_useragent("vrobbler", "0.3.0")
            mb_data = musicbrainzngs.get_release_by_id(
                self.musicbrainz_id, includes=["artists", "release-groups"]
            )
            if not self.musicbrainz_releasegroup_id:
                self.musicbrainz_releasegroup_id = mb_data["release"][
                    "release-group"
                ]["id"]
            if not self.musicbrainz_albumartist_id:
                self.musicbrainz_albumartist_id = mb_data["release"][
                    "artist-credit"
                ][0]["artist"]["id"]
            if not self.year:
                try:
                    self.year = mb_data["release"]["date"][0:4]
                except KeyError:
                    pass
                except IndexError:
                    pass

            self.save(
                update_fields=[
                    "musicbrainz_albumartist_id",
                    "musicbrainz_releasegroup_id",
                    "year",
                ]
            )

            new_artist = Artist.objects.filter(
                musicbrainz_id=self.musicbrainz_albumartist_id
            ).first()
            if self.musicbrainz_albumartist_id and new_artist:
                self.artists.add(new_artist)
            if not new_artist:
                for t in self.track_set.all():
                    self.artists.add(t.artist)
            if (
                not self.cover_image
                or self.cover_image == "default-image-replace-me"
            ):
                self.fetch_artwork()
        self.fix_album_artist()
        self.scrape_theaudiodb()
        self.scrape_allmusic()

    def fetch_artwork(self, force=False):
        if not self.cover_image and not force:
            if self.musicbrainz_id:
                try:
                    img_data = musicbrainzngs.get_image_front(
                        self.musicbrainz_id
                    )
                    name = f"{self.name}_{self.uuid}.jpg"
                    self.cover_image = ContentFile(img_data, name=name)
                    logger.info(f"Setting image to {name}")
                except musicbrainzngs.ResponseError:
                    logger.warning(
                        f"No cover art found for {self.name} by release"
                    )

            if (
                not self.cover_image
                or self.cover_image == "default-image-replace-me"
            ) and self.musicbrainz_releasegroup_id:
                try:
                    img_data = musicbrainzngs.get_release_group_image_front(
                        self.musicbrainz_releasegroup_id
                    )
                    name = f"{self.name}_{self.uuid}.jpg"
                    self.cover_image = ContentFile(img_data, name=name)
                    logger.info(f"Setting image to {name}")
                except musicbrainzngs.ResponseError:
                    logger.warning(
                        f"No cover art found for {self.name} by release group"
                    )
            if not self.cover_image:
                logger.debug(
                    f"No cover art found for release or release group for {self.name}, setting to default"
                )
            self.save()

    @property
    def mb_link(self) -> str:
        return f"https://musicbrainz.org/release/{self.musicbrainz_id}"

    @property
    def allmusic_link(self) -> str:
        if self.allmusic_id:
            return f"https://www.allmusic.com/album/{self.allmusic_id}"
        return ""

    @property
    def wikipedia_link(self):
        if self.wikipedia_slug:
            return f"https://www.wikipedia.org/en/{self.wikipedia_slug}"
        return ""

    @property
    def tadb_link(self):
        if self.theaudiodb_id:
            return f"https://www.theaudiodb.com/album/{self.theaudiodb_id}"
        return ""

    @property
    def rym_link(self):
        artist_slug = self.album_artist.name.lower().replace(" ", "-")
        album_slug = self.name.lower().replace(" ", "-")
        return f"https://rateyourmusic.com/release/album/{artist_slug}/{album_slug}/"

    @property
    def bandcamp_link(self):
        if self.bandcamp_id and self.album_artist.bandcamp_id:
            return f"https://{self.album_artist.bandcamp_id}.bandcamp.com/album/{self.bandcamp_id}"
        return ""

    @property
    def bandcamp_search_link(self):
        artist = self.album_artist.name.lower()
        album = self.name.lower()
        return f"https://bandcamp.com/search?q={album} {artist}&item_type=a"

    @classmethod
    def find_or_create(cls, name: str, artist_name: str) -> "Album":
        logger.info(
            f"Looking for album with name {name} and artist_name {artist_name}"
        )
        artist = Artist.find_or_create(artist_name, album_name=name)
        album_dict = get_album_metadata_with_artist(name, artist.name)

        if not album_dict:
            logger.info(
                f"Could not find album {name} with artist {artist.name} on musicbrainz"
            )
            album = Album.objects.filter(name=name).first()
            if not album:
                album, created = Album.objects.get_or_create(
                    name=name,
                )
                if created:
                    # album.fix_metadata()
                    # album.fetch_artwork()
                    ...
            return album

        if not artist:
            artist_dict = album_dict.get("primary_artist", {})
            if artist_dict:
                artist = Artist.objects.filter(
                    musicbrainz_id=artist_dict.get("mbid"),
                ).first()
                if not artist:
                    artist = Artist.objects.create(
                        musicbrainz_id=artist_dict.get("mbid"),
                    )

        extra_artists = []
        if not artist and len(album_dict.get("all_artists")) > 1:
            artist = Artist.objects.filter(name="Various Artists").first()
            extra_artists.append(artist)

        if not artist:
            raise Exception("No album artist found, and not a compliation")

        album = cls.objects.filter(
            models.Q(name=name) | models.Q(alt_names__icontains=name),
            album_artist=artist,
        ).first()

        alt_name = None
        found_name = album_dict.get("album_title", name)
        if found_name and name != found_name:
            alt_name = name

        album = Album.objects.filter(
            musicbrainz_id=album_dict.get("mbid")
        ).first()

        if not album:
            year = None
            if album_dict.get("release_date"):
                year = album_dict.get("release_date", "").split("-")[0]
            album = Album.objects.create(
                name=found_name,
                musicbrainz_id=album_dict.get("mbid"),
                musicbrainz_releasegroup_id=album_dict.get(
                    "release_group_mbid"
                ),
                year=year,
                album_artist=artist,
                alt_names=alt_name,
            )
            album.artists.add(*extra_artists)
            album.fetch_artwork()

        return album


class Track(ScrobblableItem):
    MEDIA_TYPE = MediaType.TRACK
    COMPLETION_PERCENT = getattr(settings, "MUSIC_COMPLETION_PERCENT", 100)

    artist = models.ForeignKey(Artist, on_delete=models.DO_NOTHING)
    albums = models.ManyToManyField(Album, related_name="tracks", blank=True)
    album = models.ForeignKey(Album, on_delete=models.DO_NOTHING, **BNULL)
    musicbrainz_id = models.CharField(max_length=255, **BNULL)

    genre = TaggableManager(through=ObjectWithGenres, blank=True)

    class Meta:
        unique_together = [["album", "musicbrainz_id"]]

    def __str__(self):
        return f"{self.title} by {self.artist}"

    def logdata_cls(self):
        return TrackLogData

    @property
    def primary_album(self):
        """Return the first chronological album that we've seen this track on"""
        if self.album:
            return self.album
        return self.albums.order_by("year").first()

    def get_absolute_url(self):
        return reverse("music:track_detail", kwargs={"slug": self.uuid})

    @property
    def subtitle(self) -> str:
        return str(self.artist)

    @property
    def strings(self) -> ScrobblableConstants:
        return ScrobblableConstants(verb="Listening", tags="notes")

    @property
    def mb_link(self):
        return f"https://musicbrainz.org/recording/{self.musicbrainz_id}"

    @property
    def info_link(self):
        return self.mb_link

    @property
    def primary_image_url(self) -> str:
        url = ""
        if self.artist.thumbnail:
            url = self.artist.thumbnail_medium.url
        if self.primary_album and self.primary_album.cover_image:
            url = self.primary_album.cover_image_medium.url
        return url

    @classmethod
    def find_or_create(
        cls,
        title: str = "",
        artist_name: str = "",
        album_name: str = "",
        run_time_seconds: int | None = None,
        enrich: bool = False,
        commit: bool = True,
    ) -> "Track":
        """Given a name, try to find the track by the artist from Musicbrainz.

        As a basic conceit we trust the source for giving us the track and artist
        name

        Optionally, we can update any found artists with overwrite."""
        album = None
        if album_name:
            logger.info("Looking up album for: {album_name}")
            album = Album.find_or_create(
                name=album_name, artist_name=artist_name
            )
            artist = album.album_artist
        else:
            artist = Artist.find_or_create(artist_name, track_name=title)
        if not artist:
            artist = Artist.find_or_create(artist_name)

        lookup_keys = {"title": title, "artist": artist}
        if run_time_seconds:
            lookup_keys["base_run_time_seconds"] = run_time_seconds
        logger.info(f"Looking up track using: {lookup_keys}")
        track = cls.objects.filter(**lookup_keys).first()
        if track:
            logger.info(
                "Found match for track by name and artist, not going to musicbrainz ",
                extra={
                    "track_id": track.id,
                    "title": title,
                    "artist_name": artist_name,
                    "run_time_seconds": run_time_seconds,
                },
            )
            return track

        track = cls.objects.filter(title=title, artist=artist).first()
        if not track:
            track, _ = cls.objects.get_or_create(title=title, artist=artist)

        if album:
            track.albums.add(album)

        if enrich or not track.base_run_time_seconds:
            logger.info(
                f"Enriching track {track}",
                extra={
                    "title": title,
                    "artist_name": artist_name,
                    "track_id": track.id,
                },
            )
            try:
                mbid, length = get_recording_mbid_exact(
                    title, artist_name, album_name
                )
            except Exception:
                print("No musicbrainz result found, cannot enrich")
                return track
            track.base_run_time_seconds = run_time_seconds or int(length / 1000)
            track.musicbrainz_id = mbid
            if commit:
                track.save()

        return track

    def fix_metadata(self, force_update=False):

        ...
