"""Cadences et périodes des synchros récurrentes (spec Imports §1.11, T8).

Deux fonctions pures, testables sans base, dont dépend toute la garantie « un tick raté se
rattrape au tick suivant, jamais de doublon » :

- `period_key` nomme **la période** d'un rendez-vous. C'est ce nom qui entre dans la
  `dedup_key` du job : deux tentatives d'enfiler la synchro hebdomadaire de la même semaine
  pour la même cible portent la même clé, donc n'en produisent qu'une.
- `advance` fait avancer `next_run_at` **jusqu'au premier rendez-vous futur**, par pas d'une
  cadence. Un worker arrêté trois semaines ne produit donc pas trois imports d'un coup : il
  en produit **un** (celui de la période due, enfilé par le sweep) et repointe l'aiguille sur
  la prochaine échéance réelle. C'est exactement « rattrapé au tick suivant, jamais de
  doublon ».

Le mensuel avance de **mois calendaire**, pas de 30 jours : un rendez-vous posé le 31 janvier
tombe au 28 février (le dernier jour du mois), puis repart du 28 — le décalage est assumé et
préférable à une dérive lente de la date, et surtout il ne saute jamais un mois.
"""

import calendar
from datetime import UTC, datetime, timedelta

WEEK = timedelta(days=7)


def period_key(sync_mode: str, moment: datetime) -> str:
    """Le nom de la période à laquelle appartient `moment`, pour la `dedup_key`.

    Semaine ISO (`2026-W36`) et mois calendaire (`2026-09`) : deux rendez-vous de la même
    semaine — le tick prévu et son rattrapage — portent le même nom, donc la même clé.
    """
    instant = moment.astimezone(UTC)
    if sync_mode == "weekly":
        year, week, _ = instant.isocalendar()
        return f"{year}-W{week:02d}"
    if sync_mode == "monthly":
        return f"{instant.year}-{instant.month:02d}"
    #  `manual` : une seule clé, sans période — le bouton « Importer maintenant » ne doit
    #  jamais lancer un second import pendant que le premier tourne (dedup_scope 'active').
    return "manual"


def _add_month(moment: datetime) -> datetime:
    year, month = divmod(moment.month, 12)
    year, month = moment.year + year, month + 1
    day = min(moment.day, calendar.monthrange(year, month)[1])
    return moment.replace(year=year, month=month, day=day)


def next_occurrence(sync_mode: str, previous: datetime) -> datetime | None:
    """Le rendez-vous suivant `previous` pour cette cadence. `None` en mode manuel."""
    if sync_mode == "weekly":
        return previous + WEEK
    if sync_mode == "monthly":
        return _add_month(previous)
    return None


def advance(sync_mode: str, previous: datetime, now: datetime) -> datetime | None:
    """Le premier rendez-vous **strictement futur**, en partant de `previous`.

    Boucle bornée par construction (chaque pas avance d'au moins 28 jours) : un `previous`
    très ancien converge en quelques dizaines de tours, pas en boucle infinie.
    """
    moment = previous
    while (moment := next_occurrence(sync_mode, moment)) is not None and moment <= now:
        continue
    return moment


def first_run_at(sync_mode: str, now: datetime) -> datetime | None:
    """L'échéance posée quand l'utilisateur choisit une cadence.

    **Une cadence pleine plus tard, jamais tout de suite.** La clé est celle du client et
    chaque fetch consomme son quota : basculer un sélecteur ne doit pas lui coûter un import
    qu'il n'a pas demandé. Pour importer maintenant, il y a le bouton « Importer maintenant »,
    juste à côté — et l'écran affiche la prochaine échéance, donc rien n'est caché.
    """
    return next_occurrence(sync_mode, now)
