"""Astronomy + Jyotisa engine for the Nietzsche rectification study.
Swiss Ephemeris (Moshier), Lahiri ayanamsa, sidereal.  All times LMT at Röcken
unless noted.  LMT = UT + 12.11611/15 h.
"""
import swisseph as swe, math, json, datetime as dt
from functools import lru_cache

LAT, LON = 51.24083, 12.11611
LMT_OFF = LON/15.0           # hours; LMT = UT + LMT_OFF
swe.set_sid_mode(swe.SIDM_LAHIRI)
FLAGS = swe.FLG_MOSEPH | swe.FLG_SIDEREAL | swe.FLG_SPEED
TROP  = swe.FLG_MOSEPH | swe.FLG_SPEED

SIGNS = ['Meṣa','Vṛṣabha','Mithuna','Karkaṭa','Siṁha','Kanyā','Tulā','Vṛścika','Dhanus','Makara','Kumbha','Mīna']
SIGNS_EN = ['Aries','Taurus','Gemini','Cancer','Leo','Virgo','Libra','Scorpio','Sagittarius','Capricorn','Aquarius','Pisces']
NAKS = ['Aśvinī','Bharaṇī','Kṛttikā','Rohiṇī','Mṛgaśīrṣa','Ārdrā','Punarvasu','Puṣya','Āśleṣā','Maghā','Pūrva Phalgunī','Uttara Phalgunī','Hasta','Citrā','Svātī','Viśākhā','Anurādhā','Jyeṣṭhā','Mūla','Pūrvāṣāḍhā','Uttarāṣāḍhā','Śravaṇa','Dhaniṣṭhā','Śatabhiṣaj','Pūrva Bhādrapadā','Uttara Bhādrapadā','Revatī']
NAK_LORDS = ['Ke','Śu','Sū','Ca','Ma','Rā','Gu','Śa','Bu']*3
DASA_YEARS = {'Ke':7,'Śu':20,'Sū':6,'Ca':10,'Ma':7,'Rā':18,'Gu':16,'Śa':19,'Bu':17}
DASA_ORDER = ['Ke','Śu','Sū','Ca','Ma','Rā','Gu','Śa','Bu']
PLANETS = [('Sū',swe.SUN),('Ca',swe.MOON),('Ma',swe.MARS),('Bu',swe.MERCURY),('Gu',swe.JUPITER),('Śu',swe.VENUS),('Śa',swe.SATURN),('Rā',swe.TRUE_NODE)]
SIGN_LORD = ['Ma','Śu','Bu','Ca','Sū','Bu','Śu','Ma','Gu','Śa','Śa','Gu']

def jd_lmt(y,m,d,hh,mm=0,ss=0.0):
    """Julian day (UT) for a LMT clock time at Röcken."""
    h = hh + mm/60 + ss/3600 - LMT_OFF
    return swe.julday(y,m,d,h,swe.GREG_CAL)

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

def ayan(jd): return swe.get_ayanamsa_ut(jd)

def pos(jd, body, sidereal=True):
    f = FLAGS if sidereal else TROP
    x = swe.calc_ut(jd, body, f)[0]
    return x[0]%360, x[3]

def asc(jd, sidereal=True):
    """Sidereal (or tropical) ascendant + MC."""
    cusps, ascmc = swe.houses_ex(jd, LAT, LON, b'P', swe.FLG_SIDEREAL if sidereal else 0)
    return ascmc[0]%360, ascmc[1]%360

def fmt(lon):
    s = int(lon//30); d = lon-30*s; dd=int(d); mm=int((d-dd)*60); ss=int(round(((d-dd)*60-mm)*60))
    if ss==60: ss=0; mm+=1
    return f"{dd:02d}°{mm:02d}′{ss:02d}″ {SIGNS[s]}"

def fmt_short(lon):
    s = int(lon//30); d = lon-30*s; dd=int(d); mm=int(round((d-dd)*60))
    if mm==60: mm=0; dd+=1
    return f"{dd:02d}°{mm:02d}′ {SIGNS[s]}"

def nak(lon):
    i = int(lon//(360/27)); p = int((lon%(360/27))//(360/108))+1
    return NAKS[i], p, i

def varga(lon, n):
    """Return sign index (0-11) of the D-n division (Parasara, standard)."""
    s = int(lon//30); d = lon%30; part = int(d/(30/n))
    if n==9:
        start = {0:0,1:9,2:6,3:3}[s%4]  # movable→Aries, fixed→Capricorn, dual→Libra ... via s%4? no:
        # Standard: Aries/Leo/Sag start Aries; Taurus/Virgo/Cap start Capricorn; Gemini/Libra/Aqu start Libra; Cancer/Sco/Pis start Cancer
        start = [0,9,6,3][s%4]
        return (start+part)%12
    if n==10:
        return ((s+part)%12) if s%2==0 else ((s+8+part)%12)
    if n==12:
        return (s+part)%12
    if n==3:
        return (s+4*part)%12
    if n==7:
        return (s+part)%12 if s%2==0 else (s+6+part)%12
    if n==60:
        return (s+part)%12
    if n==2:  # hora
        return 3 if ((s%2==0 and part==0) or (s%2==1 and part==1)) else 4
    if n==4:
        return (s+3*part)%12
    if n==24:
        return (4+part)%12 if s%2==0 else (3+part)%12
    if n==27:
        return ([0,3,6,9][s%4]+part)%12
    if n==30:
        # trimsamsa: odd: Mars5 Sat5 Jup8 Mer7 Ven5 ; even reversed
        if s%2==0:
            if d<5: return 0
            if d<10: return 10
            if d<18: return 8
            if d<25: return 2
            return 6
        else:
            if d<5: return 1
            if d<12: return 5
            if d<20: return 11
            if d<25: return 9
            return 7
    if n==16:
        return ([0,4,8][s%3]+part)%12
    if n==20:
        return ([0,8,4][s%3]+part)%12
    if n==40:
        return ([0,6][s%2]+part)%12
    if n==45:
        return ([0,4,8][s%3]+part)%12
    raise ValueError(n)

def sunrise(y,m,d):
    """Sunrise at Röcken (UT jd) on Gregorian date; returns jd and LMT hours."""
    jd0 = swe.julday(y,m,d,0.0 - LMT_OFF,swe.GREG_CAL)
    res = swe.rise_trans(jd0, swe.SUN, swe.CALC_RISE|swe.BIT_DISC_CENTER, (LON,LAT,120), 1013.25, 10.0, swe.FLG_MOSEPH)
    jr = res[1][0]
    return jr
def sunset(y,m,d):
    jd0 = swe.julday(y,m,d,0.0 - LMT_OFF,swe.GREG_CAL)
    res = swe.rise_trans(jd0, swe.SUN, swe.CALC_SET|swe.BIT_DISC_CENTER, (LON,LAT,120), 1013.25, 10.0, swe.FLG_MOSEPH)
    return res[1][0]

def jd_to_lmt_hours(jd):
    y,m,d,h = swe.revjul(jd, swe.GREG_CAL)
    return h + LMT_OFF

def hms(h):
    hh=int(h); mm=int((h-hh)*60); ss=int(round(((h-hh)*60-mm)*60))
    if ss==60: ss=0; mm+=1
    return f"{hh:02d}:{mm:02d}:{ss:02d}"

def chart(jd):
    out = {}
    for name, body in PLANETS:
        lon, spd = pos(jd, body)
        out[name] = {'lon':lon,'spd':spd,'sign':int(lon//30),'nak':nak(lon)}
    out['Ke'] = {'lon':(out['Rā']['lon']+180)%360,'spd':out['Rā']['spd'],'sign':int(((out['Rā']['lon']+180)%360)//30)}
    out['Ke']['nak']=nak(out['Ke']['lon'])
    a, mc = asc(jd)
    out['La'] = {'lon':a,'sign':int(a//30),'nak':nak(a)}
    out['MC'] = {'lon':mc,'sign':int(mc//30)}
    return out

# ---------- Vimsottari ----------
YEAR = 365.25
def dasa_balance(moon_lon):
    i = int(moon_lon//(360/27)); lord = NAK_LORDS[i]
    frac_done = (moon_lon%(360/27))/(360/27)
    return lord, DASA_YEARS[lord]*(1-frac_done)

def dasa_periods(jd_birth, moon_lon, levels=3):
    """Return list of (start_jd, end_jd, [lords...]) for maha/antara/pratyantara."""
    lord, bal = dasa_balance(moon_lon)
    k = DASA_ORDER.index(lord)
    # maha dasa start (virtual) = birth - elapsed
    elapsed = DASA_YEARS[lord]-bal
    t = jd_birth - elapsed*YEAR
    periods=[]
    def sub(start, length_years, chain, level):
        # antara within a period: order starts with the period's own lord
        if level>levels: return
        first = chain[-1]; i0 = DASA_ORDER.index(first)
        t0=start
        for j in range(9):
            l = DASA_ORDER[(i0+j)%9]
            ln = length_years*DASA_YEARS[l]/120
            periods.append((t0, t0+ln*YEAR, chain+[l]))
            sub(t0, ln, chain+[l], level+1)
            t0 += ln*YEAR
    for j in range(9):
        l = DASA_ORDER[(k+j)%9]
        ln = DASA_YEARS[l]
        periods.append((t, t+ln*YEAR, [l]))
        sub(t, ln, [l], 2)
        t += ln*YEAR
    return periods

def running(periods, jd, level):
    for s,e,ch in periods:
        if len(ch)==level and s<=jd<e: return ch, s, e
    return None

def jd_to_date(jd):
    y,m,d,h = swe.revjul(jd, swe.GREG_CAL)
    return dt.date(y,m,d)
