"""Time-bucket helpers.

Used by any analytical that aggregates weekly data into calendar
months. An ISO week that straddles two calendar months is split by
day count — so a week with 4 days in April and 3 days in May
contributes 4/7 of its value to April and 3/7 to May. This avoids
the snap-to-Thursday distortion where week-CW18 (Apr 27-May 3) was
previously counted entirely in April even though 3 of its 7 days
were in May.
"""
from __future__ import annotations

from calendar import monthrange
from datetime import date, timedelta


def iso_week_first_monday(year: int, week: int) -> date:
    """Return the Monday that starts ISO week (year, week)."""
    jan4 = date(year, 1, 4)
    iso_mon1 = jan4 - timedelta(days=jan4.isoweekday() - 1)
    return iso_mon1 + timedelta(weeks=week - 1)


def split_iso_week_across_months(year: int, week: int) -> list[tuple[int, int, float]]:
    """Split one ISO week into one or two calendar-month buckets by
    day count.

    Returns a list of (cal_year, cal_month, fraction) tuples whose
    fractions sum to 1.0. Most weeks return a single tuple; only
    weeks that straddle a month boundary return two.

    Example: CW18 2026 (Apr 27-May 3)
        → [(2026, 4, 4/7), (2026, 5, 3/7)]
    """
    mon = iso_week_first_monday(year, week)
    buckets: dict[tuple[int, int], int] = {}
    for i in range(7):
        d = mon + timedelta(days=i)
        key = (d.year, d.month)
        buckets[key] = buckets.get(key, 0) + 1
    return [(y, m, n / 7) for (y, m), n in buckets.items()]


def days_in_calendar_month(year: int, month: int) -> int:
    """Number of days in (year, month). Wraps `calendar.monthrange`."""
    return monthrange(year, month)[1]
