import datetime
from collections import defaultdict

from django.db.models import Q
from scrobbles.models import Scrobble


def _range_for(scrobble):
    start = scrobble.timestamp
    end = scrobble.stop_timestamp
    if end is None:
        try:
            end = start + datetime.timedelta(hours=12)
        except AttributeError:
            end = start
    return start, end


def _find_concurrent(anchor_scrobbles, paired_scrobbles):
    """Find paired scrobbles that overlap in time with anchor scrobbles.

    Returns a dict mapping each anchor scrobble PK to a list of
    paired scrobble PKs that overlap with it.
    """
    anchor_ranges = {s.pk: _range_for(s) for s in anchor_scrobbles}
    paired_ranges = {s.pk: _range_for(s) for s in paired_scrobbles}

    anchor_to_paired = defaultdict(list)

    for a_pk, (a_start, a_end) in anchor_ranges.items():
        for p_pk, (p_start, p_end) in paired_ranges.items():
            if a_start <= p_end and p_start <= a_end:
                anchor_to_paired[a_pk].append(p_pk)

    return anchor_to_paired


CONCURRENT_MEDIA_TYPES = {
    "Trail": "Trails",
    "Book": "Books",
    "Food": "Foods",
    "Beer": "Drinks",
    "Wine": "Drinks",
    "Coffee": "Drinks",
    "Drink": "Drinks",
    "Task": "Tasks",
    "BoardGame": "Board Games",
    "VideoGame": "Video Games",
}


def _get_media_name(scrobble):
    """Return the name of the media object associated with a scrobble."""
    for attr in [
        "trail",
        "geo_location",
        "book",
        "track",
    ]:
        obj = getattr(scrobble, attr, None)
        if obj is not None:
            return str(obj)
    return "Unknown"


def compute_concurrent_listening(user, period="all_time"):
    """Find what music was listened to while on trails or at locations.

    Returns a dict with two keys: 'trails' and 'locations', each containing
    a list of entries with the trail/location name and the tracks listened to.
    """
    from trends.utils import get_date_range

    start, end = get_date_range(period)
    base_filters = Q(user=user, timestamp__isnull=False)
    if start:
        base_filters &= Q(timestamp__gte=start)
    if end:
        base_filters &= Q(timestamp__lte=end)

    media_types_to_exclude_from_anchor = (
        "Track",
        "Book",
        "Video",
        "PodcastEpisode",
        "VideoGame",
        "BoardGame",
        "Puzzle",
        "Food",
        "Beer",
        "Task",
        "WebPage",
        "LifeEvent",
        "Mood",
        "BrickSet",
        "Channel",
        "BirdingLocation",
        "Paper",
        "SportEvent",
    )

    anchor_scrobbles = list(
        Scrobble.objects.filter(
            base_filters,
            played_to_completion=True,
        )
        .exclude(media_type__in=media_types_to_exclude_from_anchor)
        .select_related("trail", "geo_location")
        .order_by("-timestamp")
    )

    paired_scrobbles = list(
        Scrobble.objects.filter(
            base_filters,
            media_type="Track",
            stop_timestamp__isnull=False,
            played_to_completion=True,
        )
        .select_related("track")
        .order_by("-timestamp")
    )

    if not anchor_scrobbles or not paired_scrobbles:
        return {"trails": [], "locations": []}

    anchor_to_paired = _find_concurrent(anchor_scrobbles, paired_scrobbles)

    paired_by_pk = {s.pk: s for s in paired_scrobbles}

    trails = []
    locations = []

    for anchor in anchor_scrobbles:
        paired_pks = anchor_to_paired.get(anchor.pk, [])
        if not paired_pks:
            continue

        tracks_by_name = defaultdict(int)
        track_details = {}
        for p_pk in paired_pks:
            ps = paired_by_pk[p_pk]
            track = ps.track
            if track is None:
                continue
            name = str(track)
            tracks_by_name[name] += 1
            if name not in track_details:
                track_details[name] = {
                    "track_name": name,
                    "track_uuid": str(track.uuid) if track.uuid else "",
                    "artist_name": str(track.artist) if track.artist else "",
                }

        anchor_name = _get_media_name(anchor)
        entry = {
            "name": anchor_name,
            "uuid": "",
            "total_sessions": len(paired_pks),
            "tracks": sorted(
                [
                    {**track_details[name], "count": count}
                    for name, count in tracks_by_name.items()
                ],
                key=lambda x: x["count"],
                reverse=True,
            )[:20],
        }

        if anchor.media_type == "Trail":
            entry["uuid"] = (
                str(anchor.trail.uuid) if anchor.trail and anchor.trail.uuid else ""
            )
            trails.append(entry)
        else:
            entry["uuid"] = (
                str(anchor.geo_location.uuid)
                if anchor.geo_location and anchor.geo_location.uuid
                else ""
            )
            locations.append(entry)

    return {
        "trails": sorted(trails, key=lambda x: x["total_sessions"], reverse=True)[:20],
        "locations": sorted(locations, key=lambda x: x["total_sessions"], reverse=True)[
            :20
        ],
    }


def compute_concurrent_media_listening(user, period="last_30"):
    """Find what music (by artist) you listen to while doing other activities.

    Groups concurrent listening by media type (Trails, Books, Foods, Drinks,
    Tasks, Board Games, Video Games) and shows the top 3 artists per type.
    """
    from trends.utils import get_date_range

    start, end = get_date_range(period)
    base_filters = Q(user=user, timestamp__isnull=False)
    if start:
        base_filters &= Q(timestamp__gte=start)
    if end:
        base_filters &= Q(timestamp__lte=end)

    anchor_media_types = list(CONCURRENT_MEDIA_TYPES.keys())

    anchor_scrobbles = list(
        Scrobble.objects.filter(
            base_filters,
            media_type__in=anchor_media_types,
            stop_timestamp__isnull=False,
            played_to_completion=True,
        ).order_by("-timestamp")
    )

    paired_scrobbles = list(
        Scrobble.objects.filter(
            base_filters,
            media_type="Track",
            stop_timestamp__isnull=False,
            played_to_completion=True,
        )
        .select_related("track", "track__artist_fk")
        .order_by("-timestamp")
    )

    if not anchor_scrobbles or not paired_scrobbles:
        return {slug: [] for slug in set(CONCURRENT_MEDIA_TYPES.values())}

    anchor_to_paired = _find_concurrent(anchor_scrobbles, paired_scrobbles)
    paired_by_pk = {s.pk: s for s in paired_scrobbles}

    media_type_artists = defaultdict(lambda: defaultdict(int))

    for anchor in anchor_scrobbles:
        paired_pks = anchor_to_paired.get(anchor.pk, [])
        if not paired_pks:
            continue

        for p_pk in paired_pks:
            ps = paired_by_pk[p_pk]
            track = ps.track
            if track is None:
                continue
            artist = track.artist_fk
            artist_name = str(artist) if artist else "Unknown"
            category = CONCURRENT_MEDIA_TYPES.get(anchor.media_type, anchor.media_type)
            media_type_artists[category][artist_name] += 1

    result = {}
    for category, artists in sorted(media_type_artists.items()):
        sorted_artists = sorted(
            [{"artist_name": name, "count": count} for name, count in artists.items()],
            key=lambda x: x["count"],
            reverse=True,
        )[:3]

        anchor_count = sum(
            1
            for a in anchor_scrobbles
            if CONCURRENT_MEDIA_TYPES.get(a.media_type, a.media_type) == category
        )
        result[category] = {
            "artists": sorted_artists,
            "total_anchor_sessions": anchor_count,
        }

    return result
