import calendar
from datetime import timedelta

from charts.models import ChartRecord
from django.db.models import Count, Q
from django.http import Http404
from django.utils import timezone
from django.views.generic import TemplateView
from profiles.utils import now_user_timezone
from scrobbles.models import Scrobble

MEDIA_TYPE_FILTERS = {
    "artist": Q(artist__isnull=False),
    "album": Q(album__isnull=False),
    "track": Q(track__isnull=False),
    "tv_series": Q(tv_series__isnull=False),
    "video": Q(video__isnull=False),
    "podcast": Q(podcast__isnull=False),
    "podcast_episode": Q(podcast_episode__isnull=False),
    "board_game": Q(board_game__isnull=False),
    "trail": Q(trail__isnull=False),
    "geo_location": Q(geo_location__isnull=False),
    "food": Q(food__isnull=False),
    "book": Q(book__isnull=False),
}


class ChartRecordView(TemplateView):
    template_name = "charts/chart_index.html"

    def get_charts(
        self,
        user,
        media_type: str,
        year=None,
        month=None,
        week=None,
        day=None,
        limit=20,
    ):
        params = {"user": user}
        if year is not None:
            params["year"] = year
        if month is not None:
            params["month"] = month
        if week is not None:
            params["week"] = week
        if day is not None:
            params["day"] = day

        filter_key = media_type.lower()
        media_filter = MEDIA_TYPE_FILTERS.get(filter_key, Q())

        return ChartRecord.objects.filter(media_filter, **params).order_by("rank")[
            :limit
        ]

    def get_charts_for_period(
        self,
        user,
        media_type: str,
        year=None,
        month=None,
        week=None,
        day=None,
        limit=20,
    ):
        """Get charts for a specific period, properly filtering by period type."""
        params = {"user": user}
        if year is not None:
            params["year"] = year
        if month is not None:
            params["month"] = month
        if week is not None:
            params["week"] = week
        if day is not None:
            params["day"] = day

        filter_key = media_type.lower()
        media_filter = MEDIA_TYPE_FILTERS.get(filter_key, Q())

        qs = ChartRecord.objects.filter(media_filter, **params)

        if day is not None:
            qs = qs.filter(day__isnull=False)
        elif week is not None:
            qs = qs.filter(week__isnull=False, day__isnull=True)
        elif month is not None:
            qs = qs.filter(month__isnull=False, week__isnull=True, day__isnull=True)
        else:
            qs = qs.filter(month__isnull=True, week__isnull=True, day__isnull=True)

        return qs.order_by("rank")[:limit]

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        user = self.request.user
        chart_type = self.request.GET.get("chart_type", "standard")
        context["chart_type"] = chart_type

        date_param = self.request.GET.get("date")

        now = timezone.now()
        if user.is_authenticated:
            now = now_user_timezone(user.profile)
        today = now.date()
        current_year = today.year
        current_month = today.month
        current_week = today.isocalendar()[1]
        current_day = today.day

        context["current_year"] = current_year
        context["current_month"] = current_month
        context["current_week"] = current_week
        context["current_day"] = current_day

        # Resolve date parameters
        if date_param:
            parts = date_param.split("-")
            year = int(parts[0])
            week = None
            month = None
            day = None
            if len(parts) >= 2 and parts[1].startswith("W"):
                week = int(parts[1].lstrip("W"))
            elif len(parts) >= 2 and parts[1]:
                try:
                    month = int(parts[1])
                except ValueError:
                    pass
            if len(parts) >= 3:
                if parts[2].startswith("W"):
                    week = int(parts[2].lstrip("W"))
                elif not parts[2].startswith("W"):
                    day = int(parts[2])
            context["period"] = "historical"
            context["year"] = year
            context["month"] = month
            context["month_name"] = calendar.month_name[month] if month else None
            context["week"] = week
            context["day"] = day
            period_str = str(year)
            if month:
                period_str = f"{calendar.month_name[month]} {period_str}"
            if week:
                period_str = f"Week {week}, {period_str}"
            if day:
                period_str = f"{calendar.month_name[month]} {day}, {year}"
            context["period_str"] = period_str
        else:
            year = current_year
            month = current_month
            week = current_week
            day = current_day
            context["period"] = "current"
            context["year"] = current_year
            context["month"] = current_month
            context["month_name"] = calendar.month_name[current_month]
            context["week"] = current_week
            context["day"] = current_day

        # List-group tables default to week-level when no date param (matches active tab)
        if not date_param:
            list_year = current_year
            list_month = None
            list_week = current_week
            list_day = None
        else:
            list_year = year
            list_month = month
            list_week = week
            list_day = day

        context["charts"] = {
            "artist": list(
                self.get_charts_for_period(
                    user, "artist", year=list_year, month=list_month, week=list_week, day=list_day, limit=20
                )
            ),
            "album": list(
                self.get_charts_for_period(
                    user, "album", year=list_year, month=list_month, week=list_week, day=list_day, limit=20
                )
            ),
            "track": list(
                self.get_charts_for_period(
                    user, "track", year=list_year, month=list_month, week=list_week, day=list_day, limit=20
                )
            ),
            "tv_series": list(
                self.get_charts_for_period(
                    user, "tv_series", year=list_year, month=list_month, week=list_week, day=list_day, limit=20
                )
            ),
            "video": list(
                self.get_charts_for_period(
                    user, "video", year=list_year, month=list_month, week=list_week, day=list_day, limit=20
                )
            ),
            "board_game": list(
                self.get_charts_for_period(
                    user, "board_game", year=list_year, month=list_month, week=list_week, day=list_day, limit=20
                )
            ),
            "book": list(
                self.get_charts_for_period(
                    user, "book", year=list_year, month=list_month, week=list_week, day=list_day, limit=20
                )
            ),
            "food": list(
                self.get_charts_for_period(
                    user, "food", year=list_year, month=list_month, week=list_week, day=list_day, limit=20
                )
            ),
            "podcast": list(
                self.get_charts_for_period(
                    user, "podcast", year=list_year, month=list_month, week=list_week, day=list_day, limit=20
                )
            ),
            "trail": list(
                self.get_charts_for_period(
                    user, "trail", year=list_year, month=list_month, week=list_week, day=list_day, limit=20
                )
            ),
        }

        bird_data = self.get_bird_chart_data(
            user,
            year=context.get("year"),
            month=context.get("month"),
            week=context.get("week"),
            day=context.get("day"),
        )
        context["birds_chart"] = bird_data["top_birds"]
        context["bird_stats"] = {
            "total_species": bird_data["total_species"],
            "total_outings": bird_data["total_outings"],
            "total_individuals": bird_data["total_individuals"],
        }

        context["chart_years"] = self.get_available_years(user)
        context["period_type"] = self.get_period_type()
        context["prev_period"] = self.get_prev_period_url(user)
        context["next_period"] = self.get_next_period_url(user)

        return context

    def get_available_years(self, user):
        if not hasattr(self, "_available_years"):
            self._available_years = list(
                ChartRecord.objects.filter(user=user)
                .values_list("year", flat=True)
                .distinct()
                .order_by("-year")
            )
        return self._available_years

    def get_period_type(self):
        date_param = self.request.GET.get("date")
        if not date_param:
            return "current"
        parts = date_param.split("-")
        if len(parts) >= 3 and not parts[2].startswith("W"):
            return "day"
        if len(parts) >= 2 and parts[1].startswith("W"):
            return "week"
        if len(parts) >= 2:
            return "month"
        return "year"

    def parse_date_params(self):
        date_param = self.request.GET.get("date")
        if not date_param:
            return None, None, None, None

        parts = date_param.split("-")
        year = int(parts[0])
        week = None
        month = None
        day = None

        if len(parts) >= 2 and parts[1].startswith("W"):
            week = int(parts[1].lstrip("W"))
        elif len(parts) >= 2:
            try:
                month = int(parts[1])
            except ValueError:
                pass

        if len(parts) >= 3:
            if parts[2].startswith("W"):
                week = int(parts[2].lstrip("W"))
            else:
                day = int(parts[2])

        return year, month, week, day

    def get_prev_period_url(self, user):
        period_type = self.get_period_type()

        if period_type == "current":
            return None

        year, month, week, day = self.parse_date_params()
        if year is None:
            return None

        available_years = self.get_available_years(user)

        if period_type == "day" and month and day:
            if month == 1:
                prev_month = 12
                prev_year = year - 1
            else:
                prev_month = month - 1
                prev_year = year
            return f"/charts/?date={prev_year}-{prev_month:02d}"
        elif period_type == "month" and month:
            if month == 1:
                prev_month = 12
                prev_year = year - 1
            else:
                prev_month = month - 1
                prev_year = year
            return f"/charts/?date={prev_year}-{prev_month:02d}"
        elif period_type == "week" and week:
            if week == 1:
                prev_week = 52
                prev_year = year - 1
            else:
                prev_week = week - 1
                prev_year = year
            return f"/charts/?date={prev_year}-W{prev_week:02d}"
        elif period_type == "year":
            if year in available_years:
                idx = available_years.index(year)
                if idx + 1 < len(available_years):
                    return f"/charts/?date={available_years[idx + 1]}"
            return f"/charts/?date={year - 1}"
        return None

    def get_next_period_url(self, user):
        period_type = self.get_period_type()

        if period_type == "current":
            return None

        year, month, week, day = self.parse_date_params()
        if year is None:
            return None

        available_years = self.get_available_years(user)

        if period_type == "day" and month and day:
            if month == 12:
                next_month = 1
                next_year = year + 1
            else:
                next_month = month + 1
                next_year = year
            return f"/charts/?date={next_year}-{next_month:02d}"
        elif period_type == "month" and month:
            if month == 12:
                next_month = 1
                next_year = year + 1
            else:
                next_month = month + 1
                next_year = year
            return f"/charts/?date={next_year}-{next_month:02d}"
        elif period_type == "week" and week:
            if week == 52:
                next_week = 1
                next_year = year + 1
            else:
                next_week = week + 1
                next_year = year
            return f"/charts/?date={next_year}-W{next_week:02d}"
        elif period_type == "year":
            if year in available_years:
                idx = available_years.index(year)
                if idx > 0:
                    return f"/charts/?date={available_years[idx - 1]}"
            return f"/charts/?date={year + 1}"
        return None

    def get_bird_chart_data(
        self, user, year=None, month=None, week=None, day=None, limit=20
    ):
        from birds.models import Bird

        filters = {
            "user": user,
            "media_type": Scrobble.MediaType.BIRDING_LOCATION,
        }
        if year:
            filters["timestamp__year"] = year
        if month:
            filters["timestamp__month"] = month
        if week:
            filters["timestamp__week"] = week
        if day:
            filters["timestamp__day"] = day

        scrobbles = Scrobble.objects.filter(**filters)

        bird_counts = {}
        outings = 0
        for scrobble in scrobbles.iterator():
            outings += 1
            birds_data = scrobble.log.get("birds", []) if scrobble.log else []
            for entry in birds_data:
                bird_id = entry.get("bird_id")
                quantity = entry.get("quantity", 1)
                if bird_id:
                    bird_counts[bird_id] = bird_counts.get(bird_id, 0) + quantity

        sorted_birds = sorted(bird_counts.items(), key=lambda x: x[1], reverse=True)[
            :limit
        ]

        bird_ids = [bid for bid, _ in sorted_birds]
        birds = Bird.objects.filter(id__in=bird_ids)
        bird_map = {b.id: b for b in birds}

        top_birds = [
            {"bird": bird_map[bid], "count": count}
            for bid, count in sorted_birds
            if bid in bird_map
        ]

        return {
            "top_birds": top_birds,
            "total_outings": outings,
            "total_species": len(bird_counts),
            "total_individuals": sum(bird_counts.values()),
        }


class MalojaChartsView(ChartRecordView):
    """Three maloja-themed image grid widgets (artists, albums, TV series)
    with Today/Week/Month/Year/All tabs. Each tab computes its own period
    from the current date — no query param needed."""

    template_name = "charts/maloja_charts.html"

    def get_context_data(self, **kwargs):
        context = super(ChartRecordView, self).get_context_data(**kwargs)
        user = self.request.user

        if not user.is_authenticated:
            context["maloja_charts"] = {}
            context["chart_keys"] = {}
            return context

        now = timezone.now()
        now = now_user_timezone(user.profile)
        today = now.date()

        context["chart_keys"] = {
            "today": "Today",
            "week": "This Week",
            "month": "This Month",
            "year": "This Year",
            "all": "All Time",
        }

        tab_params = {
            "today": {"year": today.year, "month": today.month, "day": today.day},
            "week": {"year": today.year, "week": today.isocalendar()[1]},
            "month": {"year": today.year, "month": today.month},
            "year": {"year": today.year},
        }

        maloja_charts = {}
        for media_type in ("artist", "album", "tv_series"):
            tabs = {}
            for key in ("today", "week", "month", "year"):
                tabs[key] = list(
                    self.get_charts_for_period(user, media_type, **tab_params[key])
                )
            tabs["all"] = list(
                self.get_charts_for_period(user, media_type)
            )
            maloja_charts[media_type] = tabs

        context["maloja_charts"] = maloja_charts
        return context


MEDIA_TYPE_LABELS = {
    "artist": ("🎤", "Top Artists"),
    "album": ("💿", "Top Albums"),
    "track": ("🎵", "Top Tracks"),
    "tv_series": ("📺", "Top TV Series"),
    "video": ("🎬", "Top Videos"),
    "podcast": ("🎙️", "Top Podcasts"),
    "podcast_episode": ("🎙️", "Top Podcast Episodes"),
    "board_game": ("🎲", "Top Board Games"),
    "book": ("📚", "Top Books"),
    "food": ("🍽️", "Top Foods"),
    "trail": ("🥾", "Top Trails"),
    "geo_location": ("📍", "Top Locations"),
}


class ChartDetailView(TemplateView):
    template_name = "charts/chart_detail.html"

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        user = self.request.user
        media_type = kwargs.get("media_type")

        if media_type not in MEDIA_TYPE_FILTERS:
            raise Http404

        date_param = self.request.GET.get("date")
        now = timezone.now()
        if user.is_authenticated:
            now = now_user_timezone(user.profile)
        today = now.date()

        year = today.year
        month = None
        week = None
        day = None

        if date_param:
            parts = date_param.split("-")
            year = int(parts[0])
            if len(parts) >= 2 and parts[1].startswith("W"):
                week = int(parts[1].lstrip("W"))
                month = None
            elif len(parts) >= 2:
                try:
                    month = int(parts[1])
                except ValueError:
                    pass
            if len(parts) >= 3:
                if parts[2].startswith("W"):
                    week = int(parts[2].lstrip("W"))
                else:
                    day = int(parts[2])

        params = {"user": user}
        if year:
            params["year"] = year
        if month:
            params["month"] = month
        if week:
            params["week"] = week
        if day:
            params["day"] = day

        media_filter = MEDIA_TYPE_FILTERS.get(media_type, Q())
        qs = ChartRecord.objects.filter(media_filter, **params)

        if day is not None:
            qs = qs.filter(day__isnull=False)
        elif week is not None:
            qs = qs.filter(week__isnull=False, day__isnull=True)
        elif month is not None:
            qs = qs.filter(month__isnull=False, week__isnull=True, day__isnull=True)
        else:
            qs = qs.filter(month__isnull=True, week__isnull=True, day__isnull=True)

        context["page_charts"] = qs.order_by("rank")
        context["media_type"] = media_type
        emoji, label = MEDIA_TYPE_LABELS.get(media_type, ("", media_type))
        context["media_label"] = label
        context["media_emoji"] = emoji
        context["year"] = year
        context["month"] = month
        context["week"] = week
        context["day"] = day

        context["chart_years"] = list(
            ChartRecord.objects.filter(Q(user=user) & media_filter)
            .values_list("year", flat=True)
            .distinct()
            .order_by("-year")
        )

        return context


class SpotifyTracksView(TemplateView):
    template_name = "charts/spotify_tracks.html"

    def get_spotify_tracks(self, user, limit=50):
        non_spotify_mopidy_tracks = (
            Scrobble.objects.filter(
                user=user,
                source="Mopidy",
                track__isnull=False,
            )
            .exclude(log__mopidy_source="spotify")
            .values("track")
        )
        track_ids = (
            Scrobble.objects.filter(
                user=user,
                track__isnull=False,
            )
            .filter(Q(source="Last.fm") | Q(log__mopidy_source="spotify"))
            .exclude(track__in=non_spotify_mopidy_tracks)
            .values("track")
            .annotate(count=Count("id"))
            .order_by("-count")[:limit]
        )
        from music.models import Track

        track_id_list = [item["track"] for item in track_ids]
        tracks = Track.objects.filter(id__in=track_id_list)
        track_map = {t.id: t for t in tracks}
        return [
            {
                "track": track_map[tid],
                "count": next(
                    (item["count"] for item in track_ids if item["track"] == tid), 0
                ),
            }
            for tid in track_id_list
            if tid in track_map
        ]

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        user = self.request.user
        context["spotify_tracks"] = self.get_spotify_tracks(user)
        return context


class BandcampTracksView(TemplateView):
    template_name = "charts/bandcamp_tracks.html"

    def get_bandcamp_tracks(self, user, limit=50):
        non_bandcamp_mopidy_tracks = (
            Scrobble.objects.filter(
                user=user,
                source="Mopidy",
                track__isnull=False,
            )
            .exclude(log__mopidy_source="bandcamp")
            .values("track")
        )
        track_ids = (
            Scrobble.objects.filter(
                user=user,
                track__isnull=False,
                log__mopidy_source="bandcamp",
            )
            .exclude(track__in=non_bandcamp_mopidy_tracks)
            .values("track")
            .annotate(count=Count("id"))
            .order_by("-count")[:limit]
        )
        from music.models import Track

        track_id_list = [item["track"] for item in track_ids]
        tracks = Track.objects.filter(id__in=track_id_list)
        track_map = {t.id: t for t in tracks}
        return [
            {
                "track": track_map[tid],
                "count": next(
                    (item["count"] for item in track_ids if item["track"] == tid), 0
                ),
            }
            for tid in track_id_list
            if tid in track_map
        ]

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        user = self.request.user
        context["bandcamp_tracks"] = self.get_bandcamp_tracks(user)
        return context


class BirdsChartView(TemplateView):
    template_name = "charts/birds_chart.html"

    def get_bird_data(self, user, year=None, month=None, week=None, day=None, limit=50):
        from birds.models import Bird

        filters = {
            "user": user,
            "media_type": Scrobble.MediaType.BIRDING_LOCATION,
        }
        if year:
            filters["timestamp__year"] = year
        if month:
            filters["timestamp__month"] = month
        if week:
            filters["timestamp__week"] = week
        if day:
            filters["timestamp__day"] = day

        scrobbles = Scrobble.objects.filter(**filters)

        bird_counts = {}
        outings = 0
        for scrobble in scrobbles.iterator():
            outings += 1
            birds_data = scrobble.log.get("birds", []) if scrobble.log else []
            for entry in birds_data:
                bird_id = entry.get("bird_id")
                quantity = entry.get("quantity", 1)
                if bird_id:
                    bird_counts[bird_id] = bird_counts.get(bird_id, 0) + quantity

        sorted_birds = sorted(bird_counts.items(), key=lambda x: x[1], reverse=True)[
            :limit
        ]

        bird_ids = [bid for bid, _ in sorted_birds]
        birds = Bird.objects.filter(id__in=bird_ids)
        bird_map = {b.id: b for b in birds}

        top_birds = [
            {"bird": bird_map[bid], "count": count}
            for bid, count in sorted_birds
            if bid in bird_map
        ]

        return {
            "top_birds": top_birds,
            "total_outings": outings,
            "total_species": len(bird_counts),
            "total_individuals": sum(bird_counts.values()),
        }

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        user = self.request.user

        date_param = self.request.GET.get("date")
        now = timezone.now()
        if user.is_authenticated:
            now = now_user_timezone(user.profile)
        today = now.date()

        year = None
        month = None
        week = None
        day = None

        if date_param:
            parts = date_param.split("-")
            year = int(parts[0])
            if len(parts) >= 2 and parts[1].startswith("W"):
                week = int(parts[1].lstrip("W"))
            elif len(parts) >= 2:
                try:
                    month = int(parts[1])
                except ValueError:
                    pass
            if len(parts) >= 3:
                if parts[2].startswith("W"):
                    week = int(parts[2].lstrip("W"))
                else:
                    day = int(parts[2])

        bird_data = self.get_bird_data(user, year=year, month=month, week=week, day=day)
        context["birds_chart"] = bird_data["top_birds"]
        context["bird_stats"] = {
            "total_species": bird_data["total_species"],
            "total_outings": bird_data["total_outings"],
            "total_individuals": bird_data["total_individuals"],
        }
        return context
