import logging
import time

from django.core.management.base import BaseCommand
from django.db import transaction
from music.models import Album, Artist, Track
from music.utils import (
    get_raw_artist_data,
    normalize_name,
    resolve_artist_names,
)

logger = logging.getLogger(__name__)


class Command(BaseCommand):
    help = (
        "Reconcile track artists against the raw artist data stored in "
        "scrobble logs from Mopidy/Jellyfin webhooks. Repairs tracks whose "
        "artists were wrongly split (e.g. 'Simon & Garfunkel' stored as two "
        "artists) and deletes any artists left with no references."
    )

    def add_arguments(self, parser):
        parser.add_argument(
            "--commit",
            action="store_true",
            help="Commit changes to the database",
        )
        parser.add_argument(
            "--batch-size",
            type=int,
            default=100,
            help="Number of scrobbles to process per batch (default: 100)",
        )

    def _resolve_names(
        self, cache: dict, raw_artist: str, artist_mbid: str
    ) -> list[str]:
        key = (normalize_name(raw_artist), artist_mbid)
        if key not in cache:
            cache[key] = resolve_artist_names(raw_artist, artist_mbid)
        return cache[key]

    def _resolve_artists(
        self, cache: dict, expected_names: list[str], track
    ) -> list[Artist]:
        artists = []
        for name in expected_names:
            if name not in cache:
                cache[name] = Artist.objects.filter(name=name).first()
            if not cache[name]:
                cache[name] = Artist.find_or_create(name, track_name=track.title)
            if cache[name]:
                artists.append(cache[name])
        return artists

    def _artist_is_orphan(self, artist: Artist) -> bool:
        if Track.objects.filter(artist_fk=artist).exists():
            return False
        if Track.objects.filter(artists=artist).exists():
            return False
        if Album.objects.filter(album_artist=artist).exists():
            return False
        if Album.objects.filter(artists=artist).exists():
            return False
        return True

    def handle(self, *args, **options):
        from scrobbles.models import Scrobble

        commit = options["commit"]
        batch_size = options["batch_size"]

        if not commit:
            self.stdout.write(
                "Dry run — no changes will be saved. Use --commit to apply."
            )

        qs = (
            Scrobble.objects.filter(media_type=Scrobble.MediaType.TRACK)
            .exclude(log__isnull=True)
            .exclude(log={})
            .order_by("id")
        )

        scrobble_ids = list(qs.values_list("pk", flat=True))
        self.stdout.write(f"Processing {len(scrobble_ids)} track scrobbles")

        resolution_cache = {}
        artist_cache = {}
        reconciled = 0
        orphaned_ids = set()

        for batch_num, offset in enumerate(range(0, len(scrobble_ids), batch_size)):
            batch_pks = scrobble_ids[offset : offset + batch_size]
            with transaction.atomic():
                batch = (
                    Scrobble.objects.filter(pk__in=batch_pks)
                    .select_related("track")
                    .prefetch_related("track__artists")
                )
                for scrobble in batch.iterator(chunk_size=batch_size):
                    track = scrobble.track
                    if not track:
                        continue

                    raw_data = scrobble.log.get("raw_data")
                    if not raw_data:
                        continue

                    raw_artist, _, raw_artist_mbid = get_raw_artist_data(raw_data)
                    if not raw_artist:
                        continue

                    expected_names = self._resolve_names(
                        resolution_cache, raw_artist, raw_artist_mbid or ""
                    )
                    stored_names = [
                        n for n in track.artists.all().values_list("name", flat=True)
                    ]
                    if track.artist_fk and track.artist_fk.name not in stored_names:
                        stored_names.append(track.artist_fk.name)

                    expected = {normalize_name(n) for n in expected_names}
                    stored = {normalize_name(n) for n in stored_names}
                    if expected == stored:
                        continue

                    reconciled += 1
                    self.stdout.write(
                        f"  Track '{track.title}' artists {stored_names} -> "
                        f"{expected_names} (raw: '{raw_artist}')"
                    )

                    if commit:
                        artists = self._resolve_artists(
                            artist_cache, expected_names, track
                        )
                        if not artists:
                            continue
                        old_artist_ids = set(
                            track.artists.all().values_list("id", flat=True)
                        )
                        track.artists.set(artists)
                        track.artist_fk = artists[0]
                        track.save(update_fields=["artist_fk"])
                        track.tags.add("artist-reconciled")
                        orphaned_ids.update(old_artist_ids - {a.id for a in artists})

            self.stdout.write(
                f"  Batch {batch_num + 1}: {offset + len(batch_pks)}/{len(scrobble_ids)} processed, "
                f"{reconciled} reconciled so far"
            )
            time.sleep(1)

        deleted = 0
        if commit and orphaned_ids:
            for artist_id in sorted(orphaned_ids):
                artist = Artist.objects.filter(id=artist_id).first()
                if artist and self._artist_is_orphan(artist):
                    artist.delete()
                    deleted += 1
                    self.stdout.write(f"  Deleted orphaned artist '{artist.name}'")

        self.stdout.write(
            f"\nResults (commit={commit}):\n"
            f"  Tracks reconciled: {reconciled}\n"
            f"  Orphaned artists deleted: {deleted}"
        )
