from __future__ import annotations
from dataclasses import dataclass, asdict
from datetime import date, datetime, timedelta
import math
from pathlib import Path
import pytz
from skyfield.api import load
from skyfield import almanac

TZ = pytz.timezone("Europe/Paris")
SIGN_NAMES = [
    "Bélier", "Taureau", "Gémeaux", "Cancer", "Lion", "Vierge",
    "Balance", "Scorpion", "Sagittaire", "Capricorne", "Verseau", "Poissons",
]
SIGN_TO_TYPE = {
    "Taureau": "Racine", "Vierge": "Racine", "Capricorne": "Racine",
    "Cancer": "Feuille", "Scorpion": "Feuille", "Poissons": "Feuille",
    "Gémeaux": "Fleur", "Balance": "Fleur", "Verseau": "Fleur",
    "Bélier": "Fruit", "Lion": "Fruit", "Sagittaire": "Fruit",
}


@dataclass
class DayInfo:
    date: str
    phase: str
    illumination: float
    croissante_decroissante: str
    montante_descendante: str
    signe: str
    type_jour: str
    perigee: bool
    apogee: bool
    noeud_lunaire: bool


def zodiac_sign_from_lon(lon_deg: float) -> str:
    return SIGN_NAMES[int(lon_deg // 30) % 12]


def _local_noon(d: date) -> datetime:
    return TZ.localize(datetime(d.year, d.month, d.day, 12, 0, 0))


def _compute_perigee_apogee_days(
    ts, earth, moon, start: date, end: date
) -> tuple[set[date], set[date]]:
    sample_start = datetime.combine(start - timedelta(days=1), datetime.min.time())
    sample_end = datetime.combine(
        end + timedelta(days=1), datetime.max.time().replace(microsecond=0)
    )
    samples = []
    current = TZ.localize(sample_start)
    end_local = TZ.localize(sample_end)
    step = timedelta(hours=1)
    while current <= end_local:
        t = ts.utc(current.astimezone(pytz.utc))
        dist_km = earth.at(t).observe(moon).distance().km
        samples.append((current.date(), dist_km))
        current += step
    perigee_days, apogee_days = set(), set()
    for i in range(1, len(samples) - 1):
        day, dist = samples[i]
        if not (start <= day <= end):
            continue
        prev_dist = samples[i - 1][1]
        next_dist = samples[i + 1][1]
        if dist < prev_dist and dist < next_dist:
            perigee_days.add(day)
        if dist > prev_dist and dist > next_dist:
            apogee_days.add(day)
    return perigee_days, apogee_days


def build_calendar(start: date, end: date) -> list[DayInfo]:
    if end < start:
        raise ValueError(f"Invalid date range: {start} > {end}")
    ts = load.timescale()
    eph = load("de421.bsp")
    earth, moon, sun = eph["earth"], eph["moon"], eph["sun"]
    t0 = ts.utc(start.year, start.month, start.day)
    t1 = ts.utc(end.year, end.month, end.day + 1)
    f_phase = almanac.moon_phases(eph)
    phase_times, phase_events = almanac.find_discrete(t0, t1, f_phase)
    phase_by_day = {}
    for t, ev in zip(phase_times, phase_events):
        local_day = (
            t.utc_datetime().replace(tzinfo=pytz.utc).astimezone(TZ).date()
        )
        phase_by_day[local_day] = [
            "Nouvelle Lune", "Premier Quartier", "Pleine Lune", "Dernier Quartier"
        ][int(ev)]
    f_nodes = almanac.moon_nodes(eph)
    node_times, _ = almanac.find_discrete(t0, t1, f_nodes)
    node_days = set()
    for t in node_times:
        local_day = (
            t.utc_datetime().replace(tzinfo=pytz.utc).astimezone(TZ).date()
        )
        node_days.add(local_day)
    perigee_days, apogee_days = _compute_perigee_apogee_days(
        ts, earth, moon, start, end
    )
    result = []
    d = start
    while d <= end:
        local_noon = _local_noon(d)
        t = ts.utc(local_noon.astimezone(pytz.utc))
        e = earth.at(t)
        v_sun = e.observe(sun).apparent()
        v_moon = e.observe(moon).apparent()
        sep = v_sun.separation_from(v_moon).radians
        illum = (1 - math.cos(sep)) / 2
        d2 = d + timedelta(days=1)
        local_noon2 = _local_noon(d2)
        t2 = ts.utc(local_noon2.astimezone(pytz.utc))
        e2 = earth.at(t2)
        v_sun2 = e2.observe(sun).apparent()
        v_moon2 = e2.observe(moon).apparent()
        sep2 = v_sun2.separation_from(v_moon2).radians
        illum2 = (1 - math.cos(sep2)) / 2
        croissante = "Croissante" if illum2 >= illum else "Décroissante"
        dec = v_moon.radec()[1].degrees
        dec2 = v_moon2.radec()[1].degrees
        montante = "Montante" if dec2 >= dec else "Descendante"
        lat, lon, dist = v_moon.ecliptic_latlon()
        signe = zodiac_sign_from_lon(lon.degrees % 360.0)
        type_jour = SIGN_TO_TYPE[signe]
        result.append(
            DayInfo(
                date=d.isoformat(),
                phase=phase_by_day.get(d, ""),
                illumination=round(illum * 100, 2),
                croissante_decroissante=croissante,
                montante_descendante=montante,
                signe=signe,
                type_jour=type_jour,
                perigee=(d in perigee_days),
                apogee=(d in apogee_days),
                noeud_lunaire=(d in node_days),
            )
        )
        d += timedelta(days=1)
    return result
