from fastapi import APIRouter, Query
from datetime import datetime, timedelta, timezone
from typing import List, Dict, Any, Tuple
import swisseph as swe
import os

router = APIRouter(prefix="/calendar", tags=["planetary-events"])

# ---------------- EPHEMERIS SETUP ----------------
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
EPHE_PATH = os.path.join(BASE_DIR, "ephe")

# Folder must exist and should contain .se1 files
swe.set_ephe_path(EPHE_PATH)

# ---------------- DATA ----------------
ZODIAC = [
    ("Aries", "♈"),
    ("Taurus", "♉"),
    ("Gemini", "♊"),
    ("Cancer", "♋"),
    ("Leo", "♌"),
    ("Virgo", "♍"),
    ("Libra", "♎"),
    ("Scorpio", "♏"),
    ("Sagittarius", "♐"),
    ("Capricorn", "♑"),
    ("Aquarius", "♒"),
    ("Pisces", "♓"),
]

BODIES = {
    "Sun": swe.SUN,
    "Moon": swe.MOON,
    "Mercury": swe.MERCURY,
    "Venus": swe.VENUS,
    "Mars": swe.MARS,
    "Jupiter": swe.JUPITER,
    "Saturn": swe.SATURN,
    "Uranus": swe.URANUS,
    "Neptune": swe.NEPTUNE,
    "Pluto": swe.PLUTO,
}

STATION_BODIES = {
    "Mercury": swe.MERCURY,
    "Venus": swe.VENUS,
    "Mars": swe.MARS,
    "Jupiter": swe.JUPITER,
    "Saturn": swe.SATURN,
    "Uranus": swe.URANUS,
    "Neptune": swe.NEPTUNE,
    "Pluto": swe.PLUTO,
}

SYMBOLS = {
    "Sun": "☉",
    "Moon": "☽",
    "Mercury": "☿",
    "Venus": "♀",
    "Mars": "♂",
    "Jupiter": "♃",
    "Saturn": "♄",
    "Uranus": "♅",
    "Neptune": "♆",
    "Pluto": "♇",
}

FREQUENCY = {
    "Moon": "VERY COMMON",
    "Sun": "COMMON",
    "Mercury": "COMMON",
    "Venus": "COMMON",
    "Mars": "COMMON",
    "Jupiter": "RARE",
    "Saturn": "RARE",
    "Uranus": "VERY RARE",
    "Neptune": "VERY RARE",
    "Pluto": "VERY RARE",
    "retrograde": "RARE",
    "direct": "RARE",
    "New Moon": "COMMON",
    "First Quarter Moon": "COMMON",
    "Full Moon": "COMMON",
    "Last Quarter Moon": "COMMON",
}


# ---------------- HELPERS ----------------
def last_day_of_month(year: int, month: int) -> int:
    if month == 12:
        return 31
    return (datetime(year, month + 1, 1) - timedelta(days=1)).day


def decimal_timezone_to_label(tz_offset: float) -> str:
    sign = "+" if tz_offset >= 0 else "-"
    abs_tz = abs(tz_offset)
    hours = int(abs_tz)
    minutes = int(round((abs_tz - hours) * 60))
    return f"GMT{sign}{hours}:{minutes:02d}"


def jd_to_local_datetime(jd: float, tz_offset: float) -> datetime:
    unix_seconds = (jd - 2440587.5) * 86400
    utc_dt = datetime.fromtimestamp(unix_seconds, tz=timezone.utc)
    return utc_dt + timedelta(hours=tz_offset)


def local_datetime_to_jd(local_dt: datetime, tz_offset: float) -> float:
    utc_dt = local_dt - timedelta(hours=tz_offset)
    hour = utc_dt.hour + utc_dt.minute / 60 + utc_dt.second / 3600
    return swe.julday(utc_dt.year, utc_dt.month, utc_dt.day, hour)


def get_sign(lon: float) -> Tuple[str, str]:
    idx = int((lon % 360) // 30) % 12
    return ZODIAC[idx]


def normalize_angle(angle: float) -> float:
    return angle % 360


def body_pos(jd: float, pid: int) -> Tuple[float, float]:
    """
    Correct pyswisseph output:
    swe.calc_ut(...) returns:
    (
      (longitude, latitude, distance, speed_longitude, speed_latitude, speed_distance),
      flag
    )
    """
    data, _flag = swe.calc_ut(jd, pid, swe.FLG_SWIEPH | swe.FLG_SPEED)
    longitude = data[0] % 360
    speed = data[3]
    return longitude, speed


def make_event_time(local_dt: datetime, tz_offset: float) -> str:
    return f"{local_dt.strftime('%H:%M')} {decimal_timezone_to_label(tz_offset)}"


def event_display_date(local_dt: datetime) -> str:
    return local_dt.strftime("%B %d, %Y")


def ingress_meaning(body: str, sign: str) -> str:
    meanings = {
        "Aries": "new action, courage, and fresh starts.",
        "Taurus": "stability, money, comfort, and practical growth.",
        "Gemini": "communication, learning, curiosity, and movement.",
        "Cancer": "home, family, emotions, and care.",
        "Leo": "confidence, creativity, romance, and self-expression.",
        "Virgo": "routine, work, health, and improvement.",
        "Libra": "relationships, balance, beauty, and harmony.",
        "Scorpio": "depth, transformation, intimacy, and emotional truth.",
        "Sagittarius": "travel, belief, learning, and expansion.",
        "Capricorn": "career, responsibility, discipline, and long-term goals.",
        "Aquarius": "innovation, friendship, groups, and future vision.",
        "Pisces": "intuition, dreams, compassion, and spirituality.",
    }
    return f"{body} enters {sign}, bringing focus to {meanings.get(sign, 'new astrological themes.')}"


def station_meaning(body: str, motion: str) -> str:
    if motion == "retrograde":
        return f"{body} turns retrograde, bringing review, reflection, delays, and inner correction."
    return f"{body} turns direct, restoring forward movement, clarity, and progress."


def moon_phase_meaning(phase: str) -> str:
    meanings = {
        "New Moon": "The New Moon supports new beginnings, intentions, and emotional reset.",
        "First Quarter Moon": "The First Quarter Moon brings action, decisions, and momentum.",
        "Full Moon": "The Full Moon brings emotional awareness, completion, release, and clarity.",
        "Last Quarter Moon": "The Last Quarter Moon supports reflection, clearing, and letting go.",
    }
    return meanings.get(phase, "Moon phase energy is active.")


# ---------------- EVENT CALCULATIONS ----------------
def find_ingress_time(
    start_jd: float,
    end_jd: float,
    body_id: int,
    target_sign_index: int,
) -> float:
    """
    Binary search exact sign ingress time.
    """
    left = start_jd
    right = end_jd

    for _ in range(40):
        mid = (left + right) / 2
        lon, _speed = body_pos(mid, body_id)
        sign_index = int(lon // 30) % 12

        if sign_index == target_sign_index:
            right = mid
        else:
            left = mid

    return (left + right) / 2


def find_station_time(
    start_jd: float,
    end_jd: float,
    body_id: int,
    start_speed: float,
) -> float:
    """
    Binary search exact retrograde/direct station time.
    """
    left = start_jd
    right = end_jd

    for _ in range(45):
        mid = (left + right) / 2
        _lon, mid_speed = body_pos(mid, body_id)

        if start_speed * mid_speed <= 0:
            right = mid
        else:
            left = mid

    return (left + right) / 2


def moon_phase_angle(jd: float) -> float:
    sun_lon, _ = body_pos(jd, swe.SUN)
    moon_lon, _ = body_pos(jd, swe.MOON)
    return normalize_angle(moon_lon - sun_lon)


def angle_crossed(prev_angle: float, cur_angle: float, target: float) -> bool:
    """
    Checks if Sun-Moon phase angle crossed target between two samples.
    Works across 0/360 wrap.
    """
    prev = prev_angle
    cur = cur_angle

    if cur < prev:
        cur += 360

    t = target
    if t < prev:
        t += 360

    return prev <= t <= cur


def find_moon_phase_time(start_jd: float, end_jd: float, target_angle: float) -> float:
    """
    Binary search exact Moon phase time.
    """
    left = start_jd
    right = end_jd
    start_angle = moon_phase_angle(start_jd)

    for _ in range(45):
        mid = (left + right) / 2
        mid_angle = moon_phase_angle(mid)

        a1 = start_angle
        a2 = mid_angle

        if a2 < a1:
            a2 += 360

        target = target_angle
        if target < a1:
            target += 360

        if a2 >= target:
            right = mid
        else:
            left = mid

    return (left + right) / 2


def generate_planetary_events(month: int, year: int, tz_offset: float) -> List[Dict[str, Any]]:
    events: List[Dict[str, Any]] = []
    days = last_day_of_month(year, month)

    # Local month start/end
    start_local = datetime(year, month, 1, 0, 0, 0)
    end_local = datetime(year, month, days, 23, 59, 59)

    start_jd = local_datetime_to_jd(start_local, tz_offset)
    end_jd = local_datetime_to_jd(end_local, tz_offset)

    # sample every 6 hours for better accuracy
    step = 6 / 24

    samples = []
    jd = start_jd - 1
    while jd <= end_jd + 1:
        samples.append(jd)
        jd += step

    # 1. Sign ingress events
    for body_name, body_id in BODIES.items():
        prev_jd = samples[0]
        prev_lon, prev_speed = body_pos(prev_jd, body_id)
        prev_sign_index = int(prev_lon // 30) % 12

        for cur_jd in samples[1:]:
            cur_lon, cur_speed = body_pos(cur_jd, body_id)
            cur_sign_index = int(cur_lon // 30) % 12

            if cur_sign_index != prev_sign_index:
                exact_jd = find_ingress_time(prev_jd, cur_jd, body_id, cur_sign_index)
                local_dt = jd_to_local_datetime(exact_jd, tz_offset)

                if local_dt.year == year and local_dt.month == month:
                    from_sign, from_symbol = ZODIAC[prev_sign_index]
                    to_sign, to_symbol = ZODIAC[cur_sign_index]

                    events.append({
                        "id": f"{local_dt.isoformat()}-{body_name.lower()}-ingress",
                        "date": local_dt.date().isoformat(),
                        "display_date": event_display_date(local_dt),
                        "body": body_name,
                        "body_symbol": SYMBOLS.get(body_name, "★"),
                        "time": make_event_time(local_dt, tz_offset),
                        "event_type": "ingress",
                        "motion": None,
                        "from_sign": from_sign,
                        "from_symbol": from_symbol,
                        "to_sign": to_sign,
                        "to_symbol": to_symbol,
                        "frequency": FREQUENCY.get(body_name, "COMMON"),
                        "meaning": ingress_meaning(body_name, to_sign),
                    })

            prev_jd = cur_jd
            prev_lon = cur_lon
            prev_speed = cur_speed
            prev_sign_index = cur_sign_index

    # 2. Retrograde/direct station events
    for body_name, body_id in STATION_BODIES.items():
        prev_jd = samples[0]
        _prev_lon, prev_speed = body_pos(prev_jd, body_id)

        for cur_jd in samples[1:]:
            _cur_lon, cur_speed = body_pos(cur_jd, body_id)

            if prev_speed != 0 and cur_speed != 0 and prev_speed * cur_speed < 0:
                exact_jd = find_station_time(prev_jd, cur_jd, body_id, prev_speed)
                local_dt = jd_to_local_datetime(exact_jd, tz_offset)

                if local_dt.year == year and local_dt.month == month:
                    motion = "retrograde" if prev_speed > 0 and cur_speed < 0 else "direct"
                    events.append({
                        "id": f"{local_dt.isoformat()}-{body_name.lower()}-{motion}",
                        "date": local_dt.date().isoformat(),
                        "display_date": event_display_date(local_dt),
                        "body": body_name,
                        "body_symbol": SYMBOLS.get(body_name, "★"),
                        "time": make_event_time(local_dt, tz_offset),
                        "event_type": "station",
                        "motion": motion,
                        "from_sign": None,
                        "from_symbol": None,
                        "to_sign": None,
                        "to_symbol": None,
                        "frequency": FREQUENCY.get(motion, "RARE"),
                        "meaning": station_meaning(body_name, motion),
                    })

            prev_jd = cur_jd
            prev_speed = cur_speed

    # 3. Moon phases
    moon_targets = [
        ("New Moon", "🌑", 0),
        ("First Quarter Moon", "🌓", 90),
        ("Full Moon", "🌕", 180),
        ("Last Quarter Moon", "🌗", 270),
    ]

    prev_jd = samples[0]
    prev_angle = moon_phase_angle(prev_jd)

    for cur_jd in samples[1:]:
        cur_angle = moon_phase_angle(cur_jd)

        for phase_name, phase_symbol, target_angle in moon_targets:
            if angle_crossed(prev_angle, cur_angle, target_angle):
                exact_jd = find_moon_phase_time(prev_jd, cur_jd, target_angle)
                local_dt = jd_to_local_datetime(exact_jd, tz_offset)

                if local_dt.year == year and local_dt.month == month:
                    moon_lon, _ = body_pos(exact_jd, swe.MOON)
                    sign_name, sign_symbol = get_sign(moon_lon)

                    events.append({
                        "id": f"{local_dt.isoformat()}-{phase_name.lower().replace(' ', '-')}",
                        "date": local_dt.date().isoformat(),
                        "display_date": event_display_date(local_dt),
                        "body": phase_name,
                        "body_symbol": phase_symbol,
                        "time": make_event_time(local_dt, tz_offset),
                        "event_type": "moon_phase",
                        "motion": None,
                        "from_sign": None,
                        "from_symbol": None,
                        "to_sign": sign_name,
                        "to_symbol": sign_symbol,
                        "frequency": FREQUENCY.get(phase_name, "COMMON"),
                        "meaning": f"{phase_name} in {sign_name}. {moon_phase_meaning(phase_name)}",
                    })

        prev_jd = cur_jd
        prev_angle = cur_angle

    # Remove duplicates
    unique = {}
    for event in events:
        unique[event["id"]] = event

    events = list(unique.values())

    # Sort by date + time
    events.sort(key=lambda e: (e["date"], e["time"], e["body"]))

    return events


# ---------------- API ----------------
@router.get("/planet-events")
async def get_planet_events(
    month: int = Query(..., ge=1, le=12),
    year: int = Query(..., ge=1900, le=2100),
    timezone: float = Query(5.5),
):
    try:
        events = generate_planetary_events(month, year, timezone)

        return {
            "success": True,
            "month": month,
            "year": year,
            "timezone": timezone,
            "events": events,
        }

    except Exception as e:
        return {
            "success": False,
            "month": month,
            "year": year,
            "timezone": timezone,
            "events": [],
            "error": str(e),
        }