"""Semaines ISO 8601 (lundi → dimanche), fuseau Europe/Paris, dates au format français."""
from __future__ import annotations

from datetime import date, datetime, timedelta
from zoneinfo import ZoneInfo

PARIS = ZoneInfo("Europe/Paris")

JOURS = ["lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi", "dimanche"]
MOIS = [
    "janvier", "février", "mars", "avril", "mai", "juin",
    "juillet", "août", "septembre", "octobre", "novembre", "décembre",
]


def weeks_in_year(iso_year: int) -> int:
    """52 ou 53 : le 28 décembre est toujours dans la dernière semaine ISO."""
    return date(iso_year, 12, 28).isocalendar().week


def validate_week(iso_year: int, iso_week: int) -> None:
    if not 2000 <= iso_year <= 2100:
        raise ValueError("Année hors plage (2000–2100).")
    if not 1 <= iso_week <= weeks_in_year(iso_year):
        raise ValueError(f"La semaine {iso_week} n'existe pas en {iso_year}.")


def week_bounds(iso_year: int, iso_week: int) -> tuple[date, date]:
    validate_week(iso_year, iso_week)
    start = date.fromisocalendar(iso_year, iso_week, 1)
    return start, start + timedelta(days=6)


def week_of(d: date) -> tuple[int, int]:
    cal = d.isocalendar()
    return cal.year, cal.week


def week_thursday(iso_year: int, iso_week: int) -> date:
    return date.fromisocalendar(iso_year, iso_week, 4)


def now_paris() -> datetime:
    return datetime.now(PARIS)


def current_week(now: datetime | None = None) -> tuple[int, int]:
    return week_of((now or now_paris()).astimezone(PARIS).date())


def shift_week(iso_year: int, iso_week: int, delta: int) -> tuple[int, int]:
    return week_of(date.fromisocalendar(iso_year, iso_week, 1) + timedelta(weeks=delta))


def fr_date(d: date) -> str:
    return d.strftime("%d/%m/%Y")


def fr_long_date(d: date) -> str:
    return f"{JOURS[d.weekday()]} {d.day} {MOIS[d.month - 1]} {d.year}"


def fr_datetime(dt: datetime) -> str:
    local = dt.astimezone(PARIS)
    return f"{local:%d/%m/%Y} à {local:%H:%M}"


def week_label(iso_year: int, iso_week: int) -> str:
    start, end = week_bounds(iso_year, iso_week)
    return f"Semaine {iso_week} — du {fr_date(start)} au {fr_date(end)}"


def month_bounds(year: int, month: int) -> tuple[date, date]:
    start = date(year, month, 1)
    nxt = date(year + (month == 12), month % 12 + 1, 1)
    return start, nxt - timedelta(days=1)


def days_in_range(start: date, end: date, lo: date, hi: date) -> int:
    """Nombre de jours de [start, end] compris dans [lo, hi] (bornes incluses)."""
    a, b = max(start, lo), min(end, hi)
    return max(0, (b - a).days + 1)


def weeks_touching(lo: date, hi: date) -> list[tuple[int, int]]:
    """Toutes les semaines ISO ayant au moins un jour dans [lo, hi]."""
    out: list[tuple[int, int]] = []
    cur = lo - timedelta(days=lo.weekday())
    while cur <= hi:
        out.append(week_of(cur))
        cur += timedelta(weeks=1)
    return out
