# -*- coding: utf-8 -*-
"""
transit_engine.py - Transit- und Lebenszyklus-Berechnungen via Swiss Ephemeris.
Baustein fuer Band II (Lebens-Timeline) und Band III (Business), generalisiert
fuer beliebige Klienten.

Alles wird deterministisch mit pyswisseph berechnet - nie aus Bildern/PDFs.
Die KI bekommt ausschliesslich diese fertigen, verifizierten Werte und deutet sie.

Liefert (compute_transits):
  - zeitfenster: (jahr_start, jahr_end)  [aktuelles Jahr -1 bis +9]
  - saturn_haus_wechsel: Liste der Saturn-Zeichen/Hauswechsel im Fenster
  - saturn_return: Liste der exakten Saturn-Return-Kontakte (0..n)
  - jupiter_return: Liste der exakten Jupiter-Return-Kontakte
  - saturn_ueber_natal: Saturn-Ueberquerungen natale Planeten (exakte Tage)
  - langsame: Uranus/Neptun/Pluto - aktuelles Haus, Zeichen, ungefaehre Verweildauer
  - jupiter_stationen: Jupiter-Zeichen/Hauswechsel im Fenster (Jahresrhythmus)
  - phasen: aus den Saturn-Hausdurchgaengen abgeleitete Lebensphasen (1..n)
  - meilensteine: runde Geburtstage + bekannte Zyklus-Marker im Fenster

WICHTIG: Diese Engine korrigiert Fehler, die in aelteren manuell erstellten
PDFs steckten (z.B. falsch datierter Saturn-Return). Die Engine-Werte sind
massgeblich.
"""
import swisseph as swe
import os as _os_ee; swe.set_ephe_path(_os_ee.path.join(_os_ee.path.dirname(_os_ee.path.abspath(__file__)), "ephe"))
import datetime

FLAGS = swe.FLG_SWIEPH | swe.FLG_SPEED

ZEICHEN = ['Widder', 'Stier', 'Zwillinge', 'Krebs', 'Löwe', 'Jungfrau',
           'Waage', 'Skorpion', 'Schütze', 'Steinbock', 'Wassermann', 'Fische']
MONATE = ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli',
          'August', 'September', 'Oktober', 'November', 'Dezember']

# ---- Grundbausteine ---------------------------------------------------------

def _lon(jd, planet):
    return swe.calc_ut(jd, planet, FLAGS)[0][0]

def _speed(jd, planet):
    return swe.calc_ut(jd, planet, FLAGS)[0][3]

def _jd(y, m, d, h=12.0):
    return swe.julday(y, m, d, h)

def _ang(a, b):
    """kleinste Winkeldifferenz a-b in [-180, 180)."""
    return (a - b + 180.0) % 360.0 - 180.0

def jd_to_date(jd):
    """Julianisches Datum -> datetime.date (auf den Tag gerundet)."""
    y, m, d, h = swe.revjul(jd)
    # Stunde >= 12 ggf. naechster Tag? Wir runden auf den Kalendertag des UT-JD.
    return datetime.date(int(y), int(m), int(d))

def date_lang(dat):
    """date -> 'TT. Monat JJJJ'."""
    return '%d. %s %d' % (dat.day, MONATE[dat.month - 1], dat.year)

def date_kurz(dat):
    """date -> 'TT.MM.JJJJ'."""
    return '%02d.%02d.%04d' % (dat.day, dat.month, dat.year)

def zeichen_von(lon):
    return ZEICHEN[int(lon // 30) % 12]


def _crossings(planet, target_lon, jd_start, jd_end, step=2.0):
    """Alle Zeitpunkte, zu denen 'planet' die ekliptische Laenge target_lon
    (mod 360) erreicht - inkl. retrograder Mehrfachkontakte.
    Gibt Liste von (date, richtung) zurueck. richtung in {'direkt','rückläufig'}."""
    hits = []
    jd = jd_start
    prev = _ang(_lon(jd, planet), target_lon)
    while jd < jd_end:
        jd2 = jd + step
        cur = _ang(_lon(jd2, planet), target_lon)
        crossed = (prev == 0.0) or ((prev < 0) != (cur < 0))
        if crossed and abs(prev - cur) < 180.0:  # echten 360-Sprung ausschliessen
            lo, hi = jd, jd2
            for _ in range(60):
                mid = (lo + hi) / 2.0
                if (_ang(_lon(lo, planet), target_lon) < 0) != (_ang(_lon(mid, planet), target_lon) < 0):
                    hi = mid
                else:
                    lo = mid
            jmid = (lo + hi) / 2.0
            richtung = 'direkt' if _speed(jmid, planet) >= 0 else 'rückläufig'
            hits.append((jd_to_date(jmid), richtung))
        prev = cur
        jd = jd2
    return hits


def _sign_changes(planet, jd_start, jd_end, step=3.0):
    """Findet alle Zeichenwechsel (Vielfache von 30 Grad) eines Planeten im Fenster.
    Gibt Liste von (date, neues_zeichen_index) zurueck - chronologisch, nur 'echte'
    Vorwaerts-/Rueckwaerts-Eintritte in ein neues Zeichen (Grenze ueberschritten)."""
    out = []
    jd = jd_start
    prev_sign = int(_lon(jd, planet) // 30) % 12
    while jd < jd_end:
        jd2 = jd + step
        s = int(_lon(jd2, planet) // 30) % 12
        if s != prev_sign:
            # exakten Grenzuebertritt einkreisen
            boundary = ((s if (s - prev_sign) % 12 == 1 else prev_sign) * 30.0) % 360.0
            # robust: suche die 30-Grad-Grenze zwischen den beiden Zeichen
            # (bei Vorwaertsbewegung = neues Zeichen * 30; bei Rueckwaerts = altes * 30)
            fwd = ((prev_sign + 1) % 12) == s
            grenze = (((prev_sign + 1) % 12) * 30.0) if fwd else (prev_sign * 30.0)
            lo, hi = jd, jd2
            for _ in range(60):
                mid = (lo + hi) / 2.0
                ml = _lon(mid, planet)
                if (_ang(_lon(lo, planet), grenze) < 0) != (_ang(ml, grenze) < 0):
                    hi = mid
                else:
                    lo = mid
            out.append((jd_to_date((lo + hi) / 2.0), s))
            prev_sign = s
        jd = jd2
    return out


def _haus_index(lon, cusps):
    """Hausnummer 1..12 fuer eine Laenge anhand der 12 Haeuserspitzen."""
    for i in range(12):
        a = cusps[i]
        b = cusps[(i + 1) % 12]
        if a <= b:
            if a <= lon < b:
                return i + 1
        else:
            if lon >= a or lon < b:
                return i + 1
    return 12


# ---- Hauptfunktion ----------------------------------------------------------

def compute_transits(radix, jahr_start=None, jahr_end=None, heute=None):
    """Berechnet alle Transit-/Zyklus-Daten fuer den Prognosezeitraum.

    radix : Ergebnis aus radix_engine.compute_radix (enthaelt 'planeten',
            'haeuser' (cusps mit 'laenge'), 'ac','mc','lat','lon').
    Standard-Fenster: aktuelles Jahr -1 bis aktuelles Jahr +9 (10 Jahre,
    ein Kontrolljahr am Anfang).
    """
    heute = heute or datetime.date.today()
    if jahr_start is None:
        jahr_start = heute.year - 1
    if jahr_end is None:
        jahr_end = jahr_start + 10  # 10-Jahres-Spanne (start..start+10)

    cusps = [h['laenge'] for h in radix['haeuser']]
    natal = {p['name']: p for p in radix['planeten']}
    geburtsjahr = None  # wird vom Aufrufer via radix nicht direkt geliefert; optional

    jds = _jd(jahr_start, 1, 1)
    jde = _jd(jahr_end, 12, 31)

    # 1) Saturn-Zeichen/Hauswechsel
    saturn_haus = []
    for dat, sidx in _sign_changes(swe.SATURN, jds, jde):
        zlon = sidx * 30.0 + 0.001
        haus = _haus_index(zlon, cusps)
        saturn_haus.append({
            'datum': dat, 'datum_kurz': date_kurz(dat), 'datum_lang': date_lang(dat),
            'zeichen': ZEICHEN[sidx], 'haus': haus,
        })

    # 2) Jupiter-Zeichen/Hauswechsel
    jupiter_stationen = []
    for dat, sidx in _sign_changes(swe.JUPITER, jds, jde):
        zlon = sidx * 30.0 + 0.001
        haus = _haus_index(zlon, cusps)
        jupiter_stationen.append({
            'datum': dat, 'datum_kurz': date_kurz(dat), 'datum_lang': date_lang(dat),
            'zeichen': ZEICHEN[sidx], 'haus': haus,
        })

    # 3) Saturn-Return (Saturn auf natale Saturn-Laenge)
    natal_sat = natal['Saturn']['laenge']
    saturn_return = [{'datum': d, 'datum_kurz': date_kurz(d), 'datum_lang': date_lang(d),
                      'richtung': r} for d, r in _crossings(swe.SATURN, natal_sat, jds, jde)]

    # 4) Jupiter-Return (Jupiter auf natale Jupiter-Laenge)
    natal_jup = natal['Jupiter']['laenge']
    jupiter_return = [{'datum': d, 'datum_kurz': date_kurz(d), 'datum_lang': date_lang(d),
                       'richtung': r} for d, r in _crossings(swe.JUPITER, natal_jup, jds, jde)]

    # 5) Saturn ueber natale Planeten (exakte Tage) - die "schweren" Transit-Tage
    saturn_ueber = []
    ZIELE = ['Sonne', 'Mond', 'Merkur', 'Venus', 'Mars', 'Jupiter',
             'Saturn', 'Chiron']
    for pname in ZIELE:
        if pname not in natal:
            continue
        tl = natal[pname]['laenge']
        kontakte = _crossings(swe.SATURN, tl, jds, jde)
        for d, r in kontakte:
            saturn_ueber.append({
                'planet': pname, 'datum': d, 'datum_kurz': date_kurz(d),
                'datum_lang': date_lang(d), 'richtung': r,
                'natal_zeichen': natal[pname]['zeichen'],
                'natal_grad': natal[pname]['grad'], 'natal_minute': natal[pname]['minute'],
            })
    saturn_ueber.sort(key=lambda x: x['datum'])

    # 6) Langsame Planeten - aktuelles Haus/Zeichen am Fensteranfang + Eintritte
    langsame = {}
    for pname, pid in [('Uranus', swe.URANUS), ('Neptun', swe.NEPTUNE), ('Pluto', swe.PLUTO)]:
        jl = _jd(jahr_start, 1, 1)
        l0 = _lon(jl, pid)
        eintritte = []
        for dat, sidx in _sign_changes(pid, jds, jde):
            zlon = sidx * 30.0 + 0.001
            eintritte.append({'datum': dat, 'datum_kurz': date_kurz(dat),
                              'datum_lang': date_lang(dat), 'zeichen': ZEICHEN[sidx],
                              'haus': _haus_index(zlon, cusps)})
        langsame[pname] = {
            'start_zeichen': zeichen_von(l0),
            'start_haus': _haus_index(l0, cusps),
            'eintritte': eintritte,
        }

    # 7) Lebensphasen aus den Saturn-Hausdurchgaengen ableiten
    #    Jede Phase = Zeitspanne, in der Saturn in EINEM Haus steht.
    #    Start = Fensteranfang; Grenzen = Saturn-Hauswechsel; plus Saturn-Return als
    #    eigener Mark/Phasengrenze, wenn er nicht mit einem Hauswechsel zusammenfaellt.
    phasen = _phasen_aus_saturn(radix, cusps, natal, jahr_start, jahr_end,
                                saturn_haus, saturn_return)

    # 8) Meilensteine: runde Geburtstage im Fenster (sofern Geburtsjahr bekannt)
    meilensteine = []

    return {
        'zeitfenster': (jahr_start, jahr_end),
        'heute': heute,
        'saturn_haus_wechsel': saturn_haus,
        'jupiter_stationen': jupiter_stationen,
        'saturn_return': saturn_return,
        'jupiter_return': jupiter_return,
        'saturn_ueber_natal': saturn_ueber,
        'langsame': langsame,
        'phasen': phasen,
        'meilensteine': meilensteine,
    }


def _saturn_haus_am(jd, cusps):
    l = _lon(jd, swe.SATURN)
    return _haus_index(l, cusps)


def _phasen_aus_saturn(radix, cusps, natal, jahr_start, jahr_end,
                       saturn_haus, saturn_return):
    """Leitet 4-6 Lebensphasen aus den Saturn-Hausdurchgaengen im Fenster ab.
    Jede Phase traegt: nr, von_date, bis_date, haus, zeichen, ist_return."""
    HAUS_THEMA = {
        1: ('Neuanfang & Selbstbild', 'Identität, Körper, neuer Lebensabschnitt'),
        2: ('Werte & Finanzen', 'Geld, Selbstwert, materielle Sicherheit'),
        3: ('Lernen & Austausch', 'Kommunikation, Umfeld, Wissen'),
        4: ('Wurzeln & Heim', 'Familie, Zuhause, innere Basis'),
        5: ('Kreativität & Ausdruck', 'Schöpferkraft, Kinder, Spiel, Sichtbarkeit'),
        6: ('Arbeit & Alltag', 'Routinen, Gesundheit, Dienst, Handwerk'),
        7: ('Beziehung & Partnerschaft', 'Partner, Verträge, das Gegenüber'),
        8: ('Tiefe & Wandlung', 'Bindung, gemeinsame Ressourcen, Transformation'),
        9: ('Sinn & Weite', 'Philosophie, Reisen, Lehre, höhere Bildung'),
        10: ('Karriere & Lebenswerk', 'Beruf, Status, öffentliche Rolle'),
        11: ('Vision & Gemeinschaft', 'Netzwerk, Freundschaft, Zukunft, Gruppen'),
        12: ('Rückzug & Spiritualität', 'innere Welt, Auflösung, Vorbereitung des Neuen'),
    }
    # Saturn-Haus am Fensteranfang
    jd_start = _jd(jahr_start, 1, 1)
    start_haus = _saturn_haus_am(jd_start, cusps)
    start_zeichen = zeichen_von(_lon(jd_start, swe.SATURN))

    # Grenzpunkte sammeln (Datum, neues_haus, zeichen)
    roh = [{'datum': datetime.date(jahr_start, 1, 1), 'haus': start_haus,
            'zeichen': start_zeichen}]
    for w in saturn_haus:
        roh.append({'datum': w['datum'], 'haus': w['haus'], 'zeichen': w['zeichen']})

    # --- Glaetten ---------------------------------------------------------
    # Saturn pendelt an Zeichengrenzen durch Rueckläufigkeit oft mehrfach hin und
    # her. Das erzeugt Mini-Intervalle von wenigen Wochen. Wir verschmelzen
    # aufeinanderfolgende Eintraege mit GLEICHEM Haus und behalten als
    # Phasenbeginn den ERSTEN (Vorbote) bzw. - falls vorhanden - den ersten
    # DIREKTEN Eintritt. So entsteht aus dem Pendeln eine einzige, durchgehende
    # Phase mit dem definitiven Eintrittsdatum.
    intervals = []  # (von_date, haus, zeichen, alle_eintritte_dieser_phase)
    for g in roh:
        if intervals and intervals[-1]['haus'] == g['haus']:
            # gleiches Haus -> Phase laeuft weiter (Pendelbewegung), nichts Neues
            intervals[-1]['eintritte'].append(g['datum'])
            continue
        intervals.append({'von': g['datum'], 'haus': g['haus'],
                          'zeichen': g['zeichen'], 'eintritte': [g['datum']]})

    # bis-Datum jeder Phase = Beginn der naechsten (sonst Fensterende)
    for i, iv in enumerate(intervals):
        iv['bis'] = intervals[i + 1]['von'] if i + 1 < len(intervals) else datetime.date(jahr_end, 12, 31)

    # --- Pendel-Kurzphasen generell einklappen ---------------------------
    # Eine echte Saturn-Hausphase dauert ~2-2,5 Jahre. Jede Phase, die kuerzer
    # als MIN_TAGE ist, ist ein Artefakt der retrograden Pendelbewegung an einer
    # Hausgrenze. Solche Kurzphasen werden mit dem laengeren Nachbarn verschmolzen
    # (Haus des laengeren Nachbarn gewinnt). Wir iterieren, bis nichts Kurzes mehr
    # uebrig ist. Der erste Eintritt einer Kurzphase bleibt als 'Vorbote' am
    # Nachfolger erhalten.
    MIN_TAGE = 200
    def _dauer(iv):
        return (iv['bis'] - iv['von']).days

    changed = True
    while changed and len(intervals) > 1:
        changed = False
        # kuerzeste Phase finden
        idx = min(range(len(intervals)), key=lambda k: _dauer(intervals[k]))
        if _dauer(intervals[idx]) >= MIN_TAGE:
            break
        kurz = intervals[idx]
        # mit welchem Nachbarn verschmelzen? -> mit dem laengeren
        left = intervals[idx - 1] if idx > 0 else None
        right = intervals[idx + 1] if idx + 1 < len(intervals) else None
        if left and right:
            ziel = left if _dauer(left) >= _dauer(right) else right
        else:
            ziel = left or right
        # Eintritte/Datum konsolidieren
        if ziel is left:
            # Kurzphase faellt nach links: linke Phase laeuft bis kurz.bis
            left['bis'] = kurz['bis']
            left['eintritte'].extend(kurz['eintritte'])
        else:
            # Kurzphase faellt nach rechts: rechte Phase beginnt frueher (kurz.von),
            # ihr definitiver Eintritt bleibt aber; kurz.von wird Vorbote
            right['vorbote_von'] = kurz['von']
            right['eintritte'].extend(kurz['eintritte'])
            right['von'] = kurz['von']
        intervals.pop(idx)
        changed = True

    # Definitiven Eintritt je Phase bestimmen: das spaeteste Eintrittsdatum
    # innerhalb der ersten ~10 Monate gilt als 'definitiv' (nach dem Pendeln);
    # falls nur ein Eintritt existiert, ist dieser definitiv.
    HAUS_THEMA_LOCAL = HAUS_THEMA
    phasen = []
    for i, iv in enumerate(intervals):
        eintr = sorted(iv['eintritte'])
        # definitiver Eintritt = letzter Eintritt, der zum HAUS dieser Phase
        # gehoert und nahe am Phasenbeginn liegt
        definitiv = eintr[0]
        for e in eintr:
            if (e - eintr[0]).days <= 300:
                definitiv = e
        titel, thema = HAUS_THEMA_LOCAL.get(iv['haus'], ('Lebensphase', ''))
        ret_in = [r for r in saturn_return if iv['von'] <= r['datum'] <= iv['bis']]
        phasen.append({
            'nr': i + 1,
            'von': iv['von'], 'bis': iv['bis'],
            'von_lang': date_lang(iv['von']), 'bis_lang': date_lang(iv['bis']),
            'definitiv': definitiv, 'definitiv_lang': date_lang(definitiv),
            'vorbote_von': iv.get('vorbote_von'),
            'haus': iv['haus'], 'zeichen': iv['zeichen'],
            'titel': titel, 'thema': thema,
            'saturn_return': ret_in,
            'ist_return_phase': bool(ret_in),
        })
    return phasen


# ---- Standalone-Verifikation ------------------------------------------------

if __name__ == '__main__':
    import os
    swe.set_ephe_path(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'ephe'))
    from radix_engine import compute_radix

    radix = compute_radix(1976, 3, 31, 15, 0, 53.55, 10.0,
                          tz='Europe/Berlin', hsys=b'K', name='Dirk Zessin')
    T = compute_transits(radix, jahr_start=2025, jahr_end=2035)

    print('FENSTER:', T['zeitfenster'])
    print('\nSATURN-HAUSWECHSEL:')
    for w in T['saturn_haus_wechsel']:
        print('  %-12s Saturn -> %-10s %d. Haus' % (w['datum_kurz'], w['zeichen'], w['haus']))
    print('\nSATURN-RETURN:')
    for r in T['saturn_return']:
        print('  %-12s (%s)' % (r['datum_kurz'], r['richtung']))
    print('\nJUPITER-RETURN:')
    for r in T['jupiter_return']:
        print('  %-12s (%s)' % (r['datum_kurz'], r['richtung']))
    print('\nSATURN UEBER NATALE PLANETEN:')
    for s in T['saturn_ueber_natal']:
        print('  %-12s Saturn -> %-7s (%s)' % (s['datum_kurz'], s['planet'], s['richtung']))
    print('\nLANGSAME PLANETEN (Stand %d):' % T['zeitfenster'][0])
    for n, d in T['langsame'].items():
        print('  %-7s start: %s %d. Haus' % (n, d['start_zeichen'], d['start_haus']))
        for e in d['eintritte']:
            print('           -> %s %s (%d. Haus)' % (e['datum_kurz'], e['zeichen'], e['haus']))
    print('\nPHASEN (aus Saturn-Hausdurchgaengen):')
    for p in T['phasen']:
        rmark = '  <-- SATURN-RETURN' if p['ist_return_phase'] else ''
        print('  Phase %d: %s - %s | %d. Haus (%s) - %s%s' % (
            p['nr'], p['von_lang'], p['bis_lang'], p['haus'], p['zeichen'], p['titel'], rmark))
