from collections import defaultdict

from django.core.management.base import BaseCommand
from django.db import transaction

from api.models import Craptitude, GameSession


class Command(BaseCommand):
    help = (
        "Remove duplicate craptitudes, keeping ONE row per (category, roundNumber). "
        "GameSession references pointing at a discarded duplicate are reassigned to the kept row "
        "first, so no game sessions are cascade-deleted. Run this before applying the "
        "unique (category, roundNumber) constraint migration."
    )

    def add_arguments(self, parser):
        parser.add_argument(
            "--dry-run",
            action="store_true",
            help="Report what would change without writing anything.",
        )

    def handle(self, *args, **options):
        dry_run = options["dry_run"]

        # Group every craptitude by (category, roundNumber).
        groups = defaultdict(list)
        for c in Craptitude.objects.all().order_by("pk"):
            groups[(c.category_id, c.roundNumber)].append(c)

        dup_groups = {key: rows for key, rows in groups.items() if len(rows) > 1}
        if not dup_groups:
            self.stdout.write(self.style.SUCCESS("✅ No duplicate craptitudes found."))
            return

        total_deleted = 0
        total_reassigned = 0

        with transaction.atomic():
            for (category_id, round_number), rows in sorted(dup_groups.items()):
                keeper = self._pick_keeper(rows)
                dupes = [r for r in rows if r.pk != keeper.pk]
                dupe_ids = [r.pk for r in dupes]

                # Preserve prefix text: backfill any prefix the keeper is missing from a duplicate
                # that has it, so deduping never loses already-added prefixes.
                carried = self._backfill_prefixes(keeper, dupes)

                sessions = GameSession.objects.filter(craptitude_id__in=dupe_ids)
                session_count = sessions.count()

                self.stdout.write(
                    f"  category={category_id} round={round_number}: "
                    f"keep id={keeper.pk}, delete {len(dupe_ids)} duplicate(s)"
                    + (f", reassign {session_count} session(s)" if session_count else "")
                    + (f", carried prefixes {carried}" if carried else "")
                )

                if not dry_run:
                    if carried:
                        keeper.save(update_fields=carried)
                    if session_count:
                        sessions.update(craptitude=keeper)
                    Craptitude.objects.filter(pk__in=dupe_ids).delete()

                total_deleted += len(dupe_ids)
                total_reassigned += session_count

            if dry_run:
                # Nothing should persist on a dry run.
                transaction.set_rollback(True)

        prefix = "[dry-run] would delete" if dry_run else "✅ Deleted"
        self.stdout.write(
            self.style.SUCCESS(
                f"{prefix} {total_deleted} duplicate craptitude(s) across {len(dup_groups)} slot(s); "
                f"reassigned {total_reassigned} game session(s)."
            )
        )
        if not dry_run:
            self.stdout.write(
                "Next: run `python manage.py makemigrations api` and `python manage.py migrate` "
                "to add the unique (category, roundNumber) constraint."
            )

    def _backfill_prefixes(self, keeper, dupes):
        """
        Fill any empty prefix on the keeper from a duplicate that has it.
        Mutates keeper in memory and returns the list of field names to save (empty if nothing changed).
        """
        carried = []
        for field in ("diaperPrefix", "dumpsterPrefix"):
            if (getattr(keeper, field) or "").strip():
                continue  # keeper already has this prefix
            donor = next((d for d in dupes if (getattr(d, field) or "").strip()), None)
            if donor:
                setattr(keeper, field, getattr(donor, field))
                carried.append(field)
        return carried

    def _pick_keeper(self, rows):
        """
        Choose which duplicate to keep. Preference order:
          1. active (deletedAt is None) over soft-deleted,
          2. has prefix text over empty,
          3. lowest primary key.
        """
        def score(r):
            active = r.deletedAt is None
            has_prefix = bool((r.diaperPrefix or "").strip() or (r.dumpsterPrefix or "").strip())
            return (active, has_prefix, -r.pk)  # higher tuple wins; -pk keeps the lowest id

        return max(rows, key=score)
