# -*- coding: utf-8 -*-
"""
PLANNING TEAM - calendrier reel.

Fait le pont entre le cycle abstrait (S1 / S2) et les vraies semaines datees.
Regle d'ancrage : S1 = semaines ISO impaires, S2 = semaines paires. Elle vient du
desiderata de Gwendoline, qui travaille « les samedis des semaines impaires ».

Les jours feries sont calcules, jamais saisis : onze jours, dont trois dependent de
Paques. Ils comptent dans le decompte des conges payes (un CP pose sur un ferie ne se
decompte pas) - verifie sur ao\xfbt 2026, ou « du 10 au 15/08 » vaut 5 jours et non 6,
le samedi 15 aout etant ferie.
"""

import datetime

ANNEES = (2026, 2027)          # periode couverte au demarrage

# Le planning commence a la SEMAINE 36 de 2026, du lundi 31 aout au samedi 5 septembre.
# C'est la que le classeur bascule sur le nouveau cycle de 2 semaines : ses onglets
# passent de l'ancienne rotation C1/D2/A1/B2 a « 36(2) », « 37(1) », « 38(2) »...
# Avant cette date, le cycle de reference ne veut rien dire.
DEBUT_ANNEE = 2026
DEBUT_SEMAINE = 36

JOURS = ['lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi', 'dimanche']


# Vacances scolaires de la ZONE A (academie de Lyon, dont Morance), annee 2026-2027.
# Dates officielles relevees le 26/08/2026. Les bornes sont les jours reellement
# chomes : le lendemain du dernier jour de cours, jusqu'a la veille de la reprise.
#
# Le calendrier 2027-2028 n'est pas encore publie (la date de la rentree 2027 n'est
# pas arretee) : il n'y a donc rien apres le 3 juillet 2027, a completer plus tard.
VACANCES_ZONE_A = [
    (u'Toussaint',           '2026-10-18', '2026-11-01'),
    (u'No\xebl',             '2026-12-20', '2027-01-03'),
    (u'Hiver',               '2027-02-14', '2027-02-28'),
    (u'Printemps',           '2027-04-11', '2027-04-25'),
    (u'Pont de l’Ascension', '2027-05-06', '2027-05-09'),
    (u'\xc9t\xe9',           '2027-07-04', '2027-08-31'),
]


def vacances_scolaires():
    """Les periodes de vacances scolaires, sous forme de dates."""
    return [{'nom': nom,
             'debut': datetime.date.fromisoformat(debut),
             'fin': datetime.date.fromisoformat(fin)}
            for nom, debut, fin in VACANCES_ZONE_A]


def vacances_le(jour, periodes=None):
    """Le nom de la periode de vacances scolaires couvrant ce jour, ou None."""
    for p in (periodes or vacances_scolaires()):
        if p['debut'] <= jour <= p['fin']:
            return p['nom']
    return None


def paques(annee):
    """Dimanche de Paques (algorithme de Meeus/Jones/Butcher, calendrier gregorien)."""
    a = annee % 19
    b, c = divmod(annee, 100)
    d, e = divmod(b, 4)
    f = (b + 8) // 25
    g = (b - f + 1) // 3
    h = (19 * a + b - d - g + 15) % 30
    i, k = divmod(c, 4)
    l = (32 + 2 * e + 2 * i - h - k) % 7
    m = (a + 11 * h + 22 * l) // 451
    mois, jour = divmod(h + l - 7 * m + 114, 31)
    return datetime.date(annee, mois, jour + 1)


def feries(annee):
    """{ date : nom } des onze jours feries francais (hors Alsace-Moselle)."""
    p = paques(annee)
    jours = {
        datetime.date(annee, 1, 1):   u'Jour de l’an',
        p + datetime.timedelta(1):    u'Lundi de P\xe2ques',
        datetime.date(annee, 5, 1):   u'F\xeate du Travail',
        datetime.date(annee, 5, 8):   u'Victoire 1945',
        p + datetime.timedelta(39):   u'Ascension',
        p + datetime.timedelta(50):   u'Lundi de Pentec\xf4te',
        datetime.date(annee, 7, 14):  u'F\xeate nationale',
        datetime.date(annee, 8, 15):  u'Assomption',
        datetime.date(annee, 11, 1):  u'Toussaint',
        datetime.date(annee, 11, 11): u'Armistice 1918',
        datetime.date(annee, 12, 25): u'No\xebl',
    }
    return jours


def tous_les_feries(annees=ANNEES):
    resultat = {}
    for annee in annees:
        resultat.update(feries(annee))
    return resultat


def est_ferie(jour, cache=None):
    cache = tous_les_feries() if cache is None else cache
    return jour in cache


def semaine_cycle(annee_iso, numero_iso):
    """S1 pour les semaines ISO impaires, S2 pour les paires."""
    return 1 if numero_iso % 2 else 2


def lundi_de(annee_iso, numero_iso):
    """Le lundi de la semaine ISO donnee."""
    return datetime.date.fromisocalendar(annee_iso, numero_iso, 1)


def avant_le_debut(annee_iso, numero_iso):
    """Cette semaine precede-t-elle le demarrage du planning ?"""
    return (annee_iso, numero_iso) < (DEBUT_ANNEE, DEBUT_SEMAINE)


def semaine_par_defaut(aujourdhui=None):
    """
    La semaine a ouvrir quand on arrive sur l'outil : la semaine en cours, sans jamais
    remonter avant le demarrage du planning.
    """
    jour = aujourdhui or datetime.date.today()
    annee_iso, numero = semaine_de(jour)
    if avant_le_debut(annee_iso, numero):
        return DEBUT_ANNEE, DEBUT_SEMAINE
    return annee_iso, numero


def semaine_de(jour):
    """(annee ISO, numero ISO) d'une date."""
    a, n, _ = jour.isocalendar()
    return a, n


def jours_de_la_semaine(annee_iso, numero_iso):
    """
    Les six jours ouvrables (lundi -> samedi) d'une semaine ISO, avec leur statut.
    Le dimanche est ignore : l'officine n'ouvre pas.
    """
    lundi = lundi_de(annee_iso, numero_iso)
    cache = tous_les_feries(range(lundi.year - 1, lundi.year + 2))
    periodes = vacances_scolaires()
    resultat = []
    for i in range(6):
        jour = lundi + datetime.timedelta(i)
        resultat.append({
            'date': jour.isoformat(),
            'jour': i + 1,
            'nom': JOURS[i],
            'ferie': cache.get(jour),
            'vacances': vacances_le(jour, periodes),
        })
    return resultat


def mois(annee, numero_mois):
    """
    Un mois pour la vue calendrier : toutes ses semaines ISO, chaque jour avec son
    statut (ferie, vacances scolaires) et le rang du cycle.
    """
    premier = datetime.date(annee, numero_mois, 1)
    if numero_mois == 12:
        dernier = datetime.date(annee, 12, 31)
    else:
        dernier = datetime.date(annee, numero_mois + 1, 1) - datetime.timedelta(1)

    cache = tous_les_feries(range(annee - 1, annee + 2))
    periodes = vacances_scolaires()

    lundi = premier - datetime.timedelta(premier.weekday())
    semaines_mois = []
    while lundi <= dernier:
        annee_iso, numero = semaine_de(lundi)
        jours = []
        for i in range(7):
            jour = lundi + datetime.timedelta(i)
            jours.append({
                'date': jour.isoformat(),
                'numero': jour.day,
                'dans_le_mois': jour.month == numero_mois,
                'dimanche': jour.weekday() == 6,
                'ferie': cache.get(jour),
                'vacances': vacances_le(jour, periodes),
            })
        semaines_mois.append({'annee': annee_iso, 'numero': numero,
                              'cycle': semaine_cycle(annee_iso, numero),
                              'jours': jours})
        lundi += datetime.timedelta(7)
    return {'annee': annee, 'mois': numero_mois, 'semaines': semaines_mois}


def semaines(annees=ANNEES):
    """Toutes les semaines ISO des annees couvertes, dans l'ordre."""
    resultat = []
    for annee in annees:
        numero = 1
        while True:
            try:
                lundi = lundi_de(annee, numero)
            except ValueError:
                break
            resultat.append({
                'annee': annee,
                'numero': numero,
                'cycle': semaine_cycle(annee, numero),
                'lundi': lundi.isoformat(),
                'samedi': (lundi + datetime.timedelta(5)).isoformat(),
            })
            numero += 1
    return resultat


def jours_ouvrables(debut, fin, cache=None):
    """
    Les jours ouvrables d'une periode, au sens du decompte des conges payes :
    du lundi au samedi, jours feries exclus. Renvoie la liste des dates.
    """
    cache = tous_les_feries(range(debut.year - 1, fin.year + 2)) if cache is None else cache
    resultat, jour = [], debut
    while jour <= fin:
        if jour.weekday() < 6 and jour not in cache:    # 6 = dimanche
            resultat.append(jour)
        jour += datetime.timedelta(1)
    return resultat


def compter_ouvrables(debut, fin):
    return len(jours_ouvrables(debut, fin))


if __name__ == '__main__':
    d = datetime.date

    # Les feries de 2026, pour verification a l'oeil.
    print(u'Jours f\xe9ri\xe9s 2026')
    for jour, nom in sorted(feries(2026).items()):
        print(u'  %s  %-10s %s' % (jour.isoformat(), JOURS[jour.weekday()], nom))

    # Le decompte des CP, confronte a la fiche navette d'aout 2026.
    print(u'\nD\xe9compte des cong\xe9s pay\xe9s, contr\xf4l\xe9 sur la fiche navette d’ao\xfbt 2026')
    controles = [
        (d(2026, 8, 3),  d(2026, 8, 8),  6, u'\xc9lodie, du 03 au 08/08'),
        (d(2026, 8, 4),  d(2026, 8, 8),  5, u'Sariaka, du 04 au 08/08'),
        (d(2026, 8, 10), d(2026, 8, 15), 5, u'\xc9lodie, du 10 au 15/08 (15 ao\xfbt f\xe9ri\xe9)'),
        (d(2026, 8, 11), d(2026, 8, 15), 4, u'Marie-C\xe9cile, du 11 au 15/08'),
        (d(2026, 8, 17), d(2026, 8, 22), 6, u'du 17 au 22/08'),
    ]
    for debut, fin, attendu, libelle in controles:
        obtenu = compter_ouvrables(debut, fin)
        print(u'  %-45s attendu %d, calcul\xe9 %d  %s'
              % (libelle, attendu, obtenu, u'ok' if obtenu == attendu else u'ECHEC'))

    # L'ancrage du cycle, verifie sur les samedis de Gwendoline.
    print(u'\nAncrage du cycle')
    for numero in (36, 37, 38, 39):
        lundi = lundi_de(2026, numero)
        print(u'  semaine %d : lundi %s -> S%d'
              % (numero, lundi.isoformat(), semaine_cycle(2026, numero)))
