from datetime import datetime, time, timedelta

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

DRINK_MEDIA_TYPES = [
    Scrobble.MediaType.DRINK,
    Scrobble.MediaType.BEER,
    Scrobble.MediaType.WINE,
    Scrobble.MediaType.COFFEE,
]


def _food_scrobbles(user, start, end):
    filters = Q(user=user, media_type=Scrobble.MediaType.FOOD)
    if start:
        filters &= Q(timestamp__gte=start)
    if end:
        filters &= Q(timestamp__lte=end)
    return Scrobble.objects.filter(filters).order_by("timestamp")


def _drinks_on_day(user, day):
    day_start = timezone.make_aware(datetime.combine(day, time.min))
    day_end = timezone.make_aware(datetime.combine(day, time.max))
    return Scrobble.objects.filter(
        user=user,
        media_type__in=DRINK_MEDIA_TYPES,
        timestamp__gte=day_start,
        timestamp__lte=day_end,
    ).exists()


def _vacation_ranges(user, start, end):
    filters = Q(
        user=user,
        media_type=Scrobble.MediaType.LIFE_EVENT,
        life_event__title__icontains="vacation",
    )
    if start:
        filters &= Q(timestamp__gte=start)
    if end:
        filters &= Q(timestamp__lte=end)
    ranges = []
    for s in Scrobble.objects.filter(filters).order_by("timestamp"):
        vacation_start = s.timestamp
        vacation_end = s.stop_timestamp or (s.timestamp + timedelta(hours=12))
        ranges.append((vacation_start.date(), vacation_end.date()))
    return ranges


def _is_vacation_day(date, vacation_ranges):
    for v_start, v_end in vacation_ranges:
        if v_start <= date <= v_end:
            return True
    return False


def _classify_gap(gap_hours, periodic_hours, full_hours):
    if gap_hours >= full_hours:
        return "full"
    if gap_hours >= periodic_hours:
        return "periodic"
    return "normal"


def _add_fasting_entry(fasting_by_date, day, entry, fasting_type):
    existing = fasting_by_date.get(day)
    if existing is None or (fasting_type == "full" and existing["type"] == "periodic"):
        fasting_by_date[day] = entry


def compute_fasting(user, period="last_30"):
    from trends.utils import get_date_range

    start, end = get_date_range(period)

    profile = user.profile
    periodic_hours = profile.fasting_periodic_hours or 16
    full_hours = profile.fasting_full_hours or 24
    vacation_exempt = profile.fasting_vacation_exempt

    vacation_ranges = []
    if vacation_exempt:
        vacation_ranges = _vacation_ranges(user, start, end)

    food = list(_food_scrobbles(user, start, end))

    if len(food) < 2:
        return {
            "total_days": 0,
            "fasting_days_count": 0,
            "periodic_fasting_count": 0,
            "full_fasting_count": 0,
            "liquid_fasting_count": 0,
            "fasting_rate_pct": 0.0,
            "fasting_days": [],
            "current_streak": {"count": 0, "type": None},
            "longest_streaks": [],
            "periodic_threshold_hours": periodic_hours,
            "full_threshold_hours": full_hours,
        }

    food_times = [s.timestamp for s in food]

    now = timezone.now()
    period_start = start.date() if start else food_times[0].date()
    period_end = end.date() if end else now.date()

    fasting_by_date = {}

    for i in range(len(food_times) - 1):
        t1 = food_times[i]
        t2 = food_times[i + 1]
        gap_hours = (t2 - t1).total_seconds() / 3600
        fasting_type = _classify_gap(gap_hours, periodic_hours, full_hours)

        if fasting_type == "normal":
            continue

        d1 = t1.date()
        d2 = t2.date()

        day = d1 + timedelta(days=1)
        while day < d2:
            if vacation_exempt and _is_vacation_day(day, vacation_ranges):
                day += timedelta(days=1)
                continue

            entry = {"date": str(day), "type": fasting_type, "had_drink": False}
            if fasting_type == "full":
                entry["had_drink"] = _drinks_on_day(user, day)

            _add_fasting_entry(fasting_by_date, day, entry, fasting_type)
            day += timedelta(days=1)

        if (d2 - d1).days == 1 and fasting_type == "periodic":
            if vacation_exempt and _is_vacation_day(d2, vacation_ranges):
                continue
            entry = {"date": str(d2), "type": fasting_type, "had_drink": False}
            _add_fasting_entry(fasting_by_date, d2, entry, fasting_type)

    fasting_days_list = sorted(fasting_by_date.values(), key=lambda x: x["date"])

    total_days = (period_end - period_start).days
    fasting_count = len(fasting_days_list)
    periodic_count = sum(1 for d in fasting_days_list if d["type"] == "periodic")
    full_count = sum(1 for d in fasting_days_list if d["type"] == "full")
    liquid_count = sum(
        1 for d in fasting_days_list if d["type"] == "full" and d["had_drink"]
    )
    rate = round((fasting_count / total_days) * 100, 1) if total_days > 0 else 0.0

    longest_streaks = _compute_streaks(fasting_days_list)
    current_streak = _compute_current_streak(fasting_days_list, now.date())

    return {
        "total_days": total_days,
        "fasting_days_count": fasting_count,
        "periodic_fasting_count": periodic_count,
        "full_fasting_count": full_count,
        "liquid_fasting_count": liquid_count,
        "fasting_rate_pct": rate,
        "fasting_days": fasting_days_list,
        "current_streak": current_streak,
        "longest_streaks": longest_streaks,
        "periodic_threshold_hours": periodic_hours,
        "full_threshold_hours": full_hours,
    }


def _compute_streaks(fasting_days):
    if not fasting_days:
        return []

    streaks = []
    streak_start = fasting_days[0]["date"]
    streak_type = fasting_days[0]["type"]
    streak_count = 1

    for i in range(1, len(fasting_days)):
        prev = timezone.datetime.strptime(
            fasting_days[i - 1]["date"], "%Y-%m-%d"
        ).date()
        curr = timezone.datetime.strptime(fasting_days[i]["date"], "%Y-%m-%d").date()
        gap = (curr - prev).days

        if gap == 1 and fasting_days[i]["type"] == streak_type:
            streak_count += 1
        else:
            streaks.append(
                {
                    "count": streak_count,
                    "type": streak_type,
                    "start": streak_start,
                    "end": fasting_days[i - 1]["date"],
                }
            )
            streak_start = fasting_days[i]["date"]
            streak_type = fasting_days[i]["type"]
            streak_count = 1

    streaks.append(
        {
            "count": streak_count,
            "type": streak_type,
            "start": streak_start,
            "end": fasting_days[-1]["date"],
        }
    )

    streaks.sort(key=lambda x: x["count"], reverse=True)
    return streaks[:10]


def _compute_current_streak(fasting_days, today):
    if not fasting_days:
        return {"count": 0, "type": None}

    streak_type = fasting_days[-1]["type"]
    count = 0
    check_date = today

    date_set = {d["date"]: d["type"] for d in fasting_days}

    while True:
        date_str = check_date.strftime("%Y-%m-%d")
        if date_str in date_set and date_set[date_str] == streak_type:
            count += 1
            check_date -= timedelta(days=1)
        else:
            break

    if count == 0:
        return {"count": 0, "type": None}

    return {"count": count, "type": streak_type}
