import csv
import logging

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

logger = logging.getLogger(__name__)


class Command(BaseCommand):
    help = (
        "Outputs a CSV of track IDs where raw metadata from scrobble logs "
        "does not match the track's stored artists or album. The CSV can be "
        "edited (delete the rows for tracks you do NOT want touched) and "
        "passed back via --fix-file to resolve only the listed tracks from "
        "their raw scrobble data."
    )

    def add_arguments(self, parser):
        parser.add_argument(
            "--file-path",
            type=str,
            default="/tmp/metadata-report.csv",
            help="Output CSV file path (default: /tmp/metadata-report.csv)",
        )
        parser.add_argument(
            "--fix-file",
            type=str,
            default="",
            help=(
                "Path to a CSV produced by this command. When set, only the "
                "track_ids in the CSV are resolved from raw scrobble data."
            ),
        )
        parser.add_argument(
            "--commit",
            action="store_true",
            help="Commit fixes to the database (dry run by default)",
        )

    def handle(self, *args, **options):
        if options["fix_file"]:
            self._fix(options)
        else:
            self._report(options)

    # -- report ----------------------------------------------------------

    def _report(self, options):
        from scrobbles.models import Scrobble

        file_path = options["file_path"]

        qs = (
            Scrobble.objects.filter(media_type=Scrobble.MediaType.TRACK)
            .exclude(log__isnull=True)
            .exclude(log={})
            .select_related("track__album")
            .prefetch_related("track__artists")
            .iterator(chunk_size=2000)
        )

        rows = []
        for scrobble in qs:
            track = scrobble.track
            if not track:
                continue

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

            source = get_artist_source(raw_data)
            if not source:
                continue

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

            track_artist_names = [
                name for name in track.artists.all().values_list("name", flat=True)
            ]
            track_album_name = track.album.name if track.album else ""

            artist_bad = artist_mismatch(raw_artist, track_artist_names)
            album_bad = album_mismatch(raw_album, track_album_name)
            if artist_bad or album_bad:
                issues = []
                if artist_bad:
                    issues.append("artist")
                if album_bad:
                    issues.append("album")
                rows.append(
                    {
                        "track_id": track.id,
                        "track_artist_name": " / ".join(track_artist_names),
                        "track_album_name": track_album_name,
                        "raw_artist": raw_artist,
                        "raw_album": raw_album,
                        "source": source,
                        "mismatch": "+".join(issues),
                    }
                )

        fieldnames = [
            "track_id",
            "track_artist_name",
            "track_album_name",
            "raw_artist",
            "raw_album",
            "source",
            "mismatch",
        ]
        with open(file_path, "w", newline="") as f:
            writer = csv.DictWriter(f, fieldnames=fieldnames)
            writer.writeheader()
            writer.writerows(rows)

        self.stdout.write(
            self.style.SUCCESS(f"Wrote {len(rows)} mismatched track(s) to {file_path}")
        )

    # -- fix -------------------------------------------------------------

    def _load_fix_ids(self, fix_file: str) -> list[int]:
        ids = []
        with open(fix_file, newline="") as f:
            for row in csv.DictReader(f):
                track_id = (row.get("track_id") or "").strip()
                if track_id:
                    ids.append(int(track_id))
        return ids

    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 _fix(self, options):
        from scrobbles.models import Scrobble

        commit = options["commit"]
        fix_file = options["fix_file"]
        ids = self._load_fix_ids(fix_file)
        if not ids:
            self.stdout.write(self.style.ERROR(f"No track_ids found in {fix_file}"))
            return

        self.stdout.write(f"Fixing {len(ids)} track(s) listed in {fix_file}")
        if not commit:
            self.stdout.write(
                "Dry run — no changes will be saved. Use --commit to apply."
            )

        resolution_cache = {}
        artist_cache = {}
        fixed = 0
        skipped = 0
        orphaned_ids = set()

        tracks = (
            Track.objects.filter(id__in=ids)
            .select_related("album")
            .prefetch_related("artists")
            .order_by("id")
        )
        with transaction.atomic():
            for track in tracks:
                scrobble = (
                    Scrobble.objects.filter(track=track)
                    .exclude(log__isnull=True)
                    .exclude(log={})
                    .order_by("-created")
                    .first()
                )
                raw_data = (scrobble.log if scrobble else {}).get("raw_data")
                if not raw_data:
                    self.stdout.write(
                        f"  Skip track {track.id} '{track.title}': no raw_data"
                    )
                    skipped += 1
                    continue

                raw_artist, raw_album, artist_mbid = get_raw_artist_data(raw_data)
                if not raw_artist and not raw_album:
                    skipped += 1
                    continue

                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)
                stored_album = track.album.name if track.album else ""

                artist_bad = artist_mismatch(raw_artist, stored_names)
                album_bad = album_mismatch(raw_album, stored_album)
                if not artist_bad and not album_bad:
                    skipped += 1
                    continue

                fixed += 1
                self.stdout.write(
                    f"  Track {track.id} '{track.title}': "
                    f"artists {stored_names} -> '{raw_artist}', "
                    f"album '{stored_album}' -> '{raw_album}'"
                )

                if not commit:
                    continue

                if artist_bad and raw_artist:
                    expected_names = self._resolve_names(
                        resolution_cache, raw_artist, artist_mbid or ""
                    )
                    artists = self._resolve_artists(artist_cache, expected_names, track)
                    if artists:
                        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"])
                        orphaned_ids.update(old_artist_ids - {a.id for a in artists})

                if album_bad and raw_album:
                    try:
                        album = Album.find_or_create(raw_album, raw_artist)
                    except Exception as e:
                        logger.warning(
                            f"Could not resolve album '{raw_album}' for track "
                            f"{track.id}: {e}"
                        )
                        album = None
                    if album:
                        track.album = album
                        track.save(update_fields=["album"])

                track.tags.add("metadata-fixed")

        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 fixed: {fixed}\n"
            f"  Tracks skipped: {skipped}\n"
            f"  Orphaned artists deleted: {deleted}"
        )
