import logging
import re

from django.db import IntegrityError, models, transaction
from music.constants import VARIOUS_ARTIST_DICT
from music.musicbrainz import lookup_artist_from_mb

logger = logging.getLogger(__name__)


def clean_artist_name(name: str) -> str:
    """Remove featured names from artist string."""
    if " feat. " in name.lower():
        name = re.split(" feat. ", name, flags=re.IGNORECASE)[0].strip()
    if " w. " in name.lower():
        name = re.split(" w. ", name, flags=re.IGNORECASE)[0].strip()
    if " featuring " in name.lower():
        name = re.split(" featuring ", name, flags=re.IGNORECASE)[0].strip()

    return name


def parse_artist_names(name: str) -> list[str]:
    """Split a combined artist string (e.g. 'A & B') into individual names.

    First strips feat./featuring/w. prefixes, then splits on ' & ' to
    support collaboration-style credits like 'Matt Sweeney & Bonnie Prince Billy'.
    """
    name = clean_artist_name(name)
    if " & " in name.lower():
        return [
            part.strip()
            for part in re.split(r"\s+&\s+", name, flags=re.IGNORECASE)
            if part.strip()
        ]
    return [name]


def resolve_artist_names(name: str, artist_mbid: str = "") -> list[str]:
    """Resolve an artist string into one or more artist names.

    Prefers treating the full string as a single artist. An authoritative
    MusicBrainz artist id (e.g. from a webhook) or an exact match against a
    known single artist means we keep the name literal -- so acts like
    "Simon & Garfunkel" are never pulled apart. Splitting on ' & ' (for
    genuine collaborations) is only a last resort.
    """
    name = clean_artist_name(name)
    if artist_mbid:
        return [name]

    from music.models import Artist

    if Artist.objects.filter(name=name).exists():
        return [name]

    result = lookup_artist_from_mb(name)
    if result and result.get("name", "").strip().casefold() == name.casefold():
        return [name]

    return parse_artist_names(name)


def get_or_create_various_artists() -> "Artist":
    from music.models import Artist

    artist = Artist.objects.filter(name="Various Artists").first()
    if not artist:
        artist = Artist.objects.create(**VARIOUS_ARTIST_DICT)
        logger.info("Created Various Artists placeholder")
    return artist


def deduplicate_tracks(commit=False) -> int:
    from music.models import Track

    duplicates = (
        Track.objects.values("title")
        .annotate(dup_count=models.Count("id"))
        .filter(dup_count__gt=1)
    )

    query = models.Q()
    for dup in duplicates:
        query |= models.Q(title=dup["title"])

    duplicate_tracks = Track.objects.filter(query)

    for b in duplicate_tracks:
        tracks = Track.objects.filter(title=b.title)
        first = tracks.first()
        for other in tracks.exclude(id=first.id):
            print("Moving scrobbles for", other.id, " to ", first.id)
            if commit:
                with transaction.atomic():
                    other.scrobble_set.update(track=first)
                    print("deleting ", other.id, " - ", other)
                    try:
                        other.delete()
                    except IntegrityError as e:
                        print(
                            "could not delete ",
                            other.id,
                            f": IntegrityError {e}",
                        )
    return len(duplicate_tracks)


def condense_albums(commit: bool = False):
    from music.models import Track
    from scrobbles.models import Scrobble

    processed_ids = []
    for track in Track.objects.all():
        albums_to_add = []
        duplicates = (
            Track.objects.filter(title=track.title)
            .exclude(id=track.id)
            .exclude(id__in=processed_ids)
        )

        if commit and track.album:
            albums_to_add.append(track.album)

        for dup_track in duplicates:
            logger.info(f"Adding {dup_track.album} to {track} albums")
            if commit and dup_track.album:
                track.albums.add(dup_track.album)

        # Find out if this track appears more than once
        duplicates = Track.objects.filter(title=track.title)
        if duplicates.count() > 1:
            logger.info(f"Track appears more than once, condensing: {track}")

            albums_to_add.extend([d.album for d in duplicates])
            # Find all scrobbles
            duplicate_ids = duplicates.values_list("id", flat=True)
            scrobbles = Scrobble.objects.filter(track_id__in=duplicate_ids)
            logger.info(f"Found {scrobbles.count()} scrobbles to merge onto {track}")
            if commit:
                scrobbles.update(track=track)
                track.albums.add(*list(set(albums_to_add)))

            processed_ids.extend(duplicate_ids)

        if commit:
            Track.objects.filter(scrobble__isnull=True).delete()

    return len(set(processed_ids))


def get_artist_source(raw_data: dict) -> str:
    """Return the provider that produced raw webhook data ('Jellyfin'/'Mopidy')."""
    if "Artist" in raw_data:
        return "Jellyfin"
    if "artist" in raw_data:
        return "Mopidy"
    return ""


def get_raw_artist_data(raw_data: dict) -> tuple[str, str, str]:
    """Extract (artist_name, album_name, artist_mbid) from raw webhook data."""
    source = get_artist_source(raw_data)
    if source == "Jellyfin":
        return (
            raw_data.get("Artist", ""),
            raw_data.get("Album", ""),
            raw_data.get("Provider_musicbrainzartist", ""),
        )
    if source == "Mopidy":
        return (
            raw_data.get("artist", ""),
            raw_data.get("album", ""),
            raw_data.get("musicbrainz_artist_id", ""),
        )
    return "", "", ""


def normalize_name(name: str) -> str:
    return name.strip().casefold()


def artist_mismatch(raw_artist: str, track_artist_names: list[str]) -> bool:
    """True when a raw artist string doesn't match the stored track artists."""
    if not raw_artist or not track_artist_names:
        return False
    track_names = {normalize_name(n) for n in track_artist_names}
    raw = normalize_name(raw_artist)
    if raw in track_names:
        return False
    if raw == normalize_name(" / ".join(track_artist_names)):
        return False
    return True


def album_mismatch(raw_album: str, track_album_name: str) -> bool:
    if not raw_album or not track_album_name:
        return False
    return normalize_name(raw_album) != normalize_name(track_album_name)
