import os
import glob
from datetime import datetime, timedelta

import swisseph as swe
from kerykeion import AstrologicalSubject, KerykeionChartSVG

from chart_utils import extract_lunar_return_wheel

swe.set_ephe_path(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'ephe'))


# =====================================================
# Chart Output Folder
# =====================================================

BASE_DIR = os.path.dirname(os.path.abspath(__file__))
CHARTS_DIR = os.path.join(BASE_DIR, "charts")
LUNAR_RETURN_CHART_DIR = os.path.join(CHARTS_DIR, "lunar-return")
os.makedirs(LUNAR_RETURN_CHART_DIR, exist_ok=True)


# =====================================================
# Constants
# =====================================================

SIGNS = [
    "Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo",
    "Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces"
]

PLANET_SYMBOLS = {
    "Sun": "☉",
    "Moon": "☽",
    "Mercury": "☿",
    "Venus": "♀",
    "Mars": "♂",
    "Jupiter": "♃",
    "Saturn": "♄",
    "Uranus": "♅",
    "Neptune": "♆",
    "Pluto": "♇",
    "North Node": "☊",
    "South Node": "☋",
    "Black Moon Lilith": "⚸",
    "Ceres": "⚳",
    "Pallas": "⚴",
    "Juno": "⚵",
    "Vesta": "⚶",
    "Chiron": "⚷",
    "Pholus": "φ",
    "White Moon Selena": "W",
    "Ascendant": "AS",
    "Descendant": "DS",
    "Midheaven": "MC",
    "Imum Coeli": "IC",
    "Vertex": "Vx",
    "Part of Fortune": "X",
}

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,
    "North Node": swe.TRUE_NODE,
    "Black Moon Lilith": swe.MEAN_APOG,
    "Ceres": swe.CERES,
    "Pallas": swe.PALLAS,
    "Juno": swe.JUNO,
    "Vesta": swe.VESTA,
}

ASPECTS = {
    "conjunction": {"angle": 0, "orb": 10, "symbol": "☌", "type": "Major", "effect": "Powerful"},
    "sextile": {"angle": 60, "orb": 6, "symbol": "⚹", "type": "Major", "effect": "Positive"},
    "square": {"angle": 90, "orb": 8, "symbol": "□", "type": "Major", "effect": "Challenging"},
    "trine": {"angle": 120, "orb": 8, "symbol": "△", "type": "Major", "effect": "Positive"},
    "opposition": {"angle": 180, "orb": 10, "symbol": "☍", "type": "Major", "effect": "Challenging"},
    "semisextile": {"angle": 30, "orb": 2, "symbol": "⚺", "type": "Minor", "effect": "Subtle"},
    "semisquare": {"angle": 45, "orb": 2, "symbol": "∠", "type": "Minor", "effect": "Tension"},
    "quintile": {"angle": 72, "orb": 2, "symbol": "⬠", "type": "Minor", "effect": "Creative"},
    "sesquiquadrate": {"angle": 135, "orb": 2, "symbol": "⚼", "type": "Minor", "effect": "Tension"},
    "biquintile": {"angle": 144, "orb": 2, "symbol": "⫲", "type": "Minor", "effect": "Creative"},
    "quincunx": {"angle": 150, "orb": 3, "symbol": "⚻", "type": "Minor", "effect": "Adjustment"},
}


# =====================================================
# Basic Helpers
# =====================================================

def normalize(deg):
    return deg % 360


def angle_diff(a, b):
    d = abs(a - b) % 360
    return min(d, 360 - d)


def get_sign(deg):
    return SIGNS[int(normalize(deg) // 30)]


def decimal_to_dms(deg):
    deg = normalize(deg)
    sign_deg = deg % 30

    d = int(sign_deg)
    m = int(round((sign_deg - d) * 60))

    if m == 60:
        d += 1
        m = 0

    return f"{d}° {m:02d}’"


def format_position(deg):
    return f"{decimal_to_dms(deg)} {get_sign(deg)}"


def local_to_ut(year, month, day, hour, minute, second, timezone):
    local_dt = datetime(year, month, day, hour, minute, second)
    return local_dt - timedelta(hours=timezone)


def datetime_to_jd(dt):
    return swe.julday(
        dt.year,
        dt.month,
        dt.day,
        dt.hour + dt.minute / 60 + dt.second / 3600,
    )


def jd_to_local_datetime(jd, timezone):
    y, m, d, h = swe.revjul(jd)

    hour = int(h)
    minute_float = (h - hour) * 60
    minute = int(minute_float)
    second = int(round((minute_float - minute) * 60))

    if second >= 60:
        second = 0
        minute += 1

    if minute >= 60:
        minute = 0
        hour += 1

    ut_dt = datetime(y, m, d, hour, minute, second)
    return ut_dt + timedelta(hours=timezone)


def calc_body_longitude(jd, body_id):
    try:
        result = swe.calc_ut(jd, body_id)[0]
        return normalize(result[0])
    except Exception:
        return None


def calc_body_retrograde(jd, body_id):
    try:
        result = swe.calc_ut(jd, body_id)[0]
        speed = result[3]
        return speed < 0
    except Exception:
        return False


# =====================================================
# Houses / Planets
# =====================================================

def calc_houses(jd, latitude, longitude):
    cusps, ascmc = swe.houses(jd, latitude, longitude, b"P")

    return {
        "cusps": [normalize(c) for c in cusps],
        "ascendant": normalize(ascmc[0]),
        "midheaven": normalize(ascmc[1]),
    }


def get_house(longitude, cusps):
    longitude = normalize(longitude)

    for i in range(12):
        start = normalize(cusps[i])
        end = normalize(cusps[(i + 1) % 12])

        if start <= end:
            if start <= longitude < end:
                return i + 1
        else:
            if longitude >= start or longitude < end:
                return i + 1

    return 12


def calc_part_of_fortune(sun_lon, moon_lon, asc_lon):
    return normalize(asc_lon + moon_lon - sun_lon)


def calc_vertex_proper(jd, latitude, longitude):
    try:
        cusps, ascmc = swe.houses(jd, latitude, longitude, b"P")
        asc = normalize(ascmc[0])
        desc = normalize(asc + 180)
        vertex = normalize((asc + desc) / 2 + 90)
        return vertex
    except Exception:
        return normalize(asc_lon + 150) if 'asc_lon' in dir() else normalize(150)


def calc_planets_with_houses(jd, latitude, longitude):
    houses_data = calc_houses(jd, latitude, longitude)
    cusps = houses_data["cusps"]

    planets = []
    raw_lons = {}

    for name, body_id in BODIES.items():
        lon = calc_body_longitude(jd, body_id)

        if lon is None:
            continue

        raw_lons[name] = lon

        planets.append({
            "name": name,
            "symbol": PLANET_SYMBOLS.get(name, ""),
            "longitude": round(lon, 6),
            "sign": get_sign(lon),
            "degree": decimal_to_dms(lon),
            "position": format_position(lon),
            "house": get_house(lon, cusps),
            "retrograde": calc_body_retrograde(jd, body_id),
        })

    if "North Node" in raw_lons:
        south_node = normalize(raw_lons["North Node"] + 180)

        planets.append({
            "name": "South Node",
            "symbol": PLANET_SYMBOLS["South Node"],
            "longitude": round(south_node, 6),
            "sign": get_sign(south_node),
            "degree": decimal_to_dms(south_node),
            "position": format_position(south_node),
            "house": get_house(south_node, cusps),
            "retrograde": True,
        })

    if "Black Moon Lilith" in raw_lons:
        selena = normalize(raw_lons["Black Moon Lilith"] + 180)

        planets.append({
            "name": "White Moon Selena",
            "symbol": PLANET_SYMBOLS["White Moon Selena"],
            "longitude": round(selena, 6),
            "sign": get_sign(selena),
            "degree": decimal_to_dms(selena),
            "position": format_position(selena),
            "house": get_house(selena, cusps),
            "retrograde": False,
        })

    asc = houses_data["ascendant"]
    mc = houses_data["midheaven"]
    desc = normalize(asc + 180)
    ic = normalize(mc + 180)

    sun_lon = raw_lons.get("Sun", 0)
    moon_lon = raw_lons.get("Moon", 0)

    vertex = calc_vertex_proper(jd, latitude, longitude)
    fortune = calc_part_of_fortune(sun_lon, moon_lon, asc)

    points = [
        ("Ascendant", asc),
        ("Descendant", desc),
        ("Midheaven", mc),
        ("Imum Coeli", ic),
        ("Vertex", vertex),
        ("Part of Fortune", fortune),
    ]

    for name, lon in points:
        planets.append({
            "name": name,
            "symbol": PLANET_SYMBOLS.get(name, ""),
            "longitude": round(lon, 6),
            "sign": get_sign(lon),
            "degree": decimal_to_dms(lon),
            "position": format_position(lon),
            "house": get_house(lon, cusps),
            "retrograde": False,
        })

    return planets


# =====================================================
# Lunar Return Calculation
# =====================================================

def find_lunar_return_jd(natal_moon_lon, return_year, return_month):
    start_dt = datetime(return_year, return_month, 1, 0, 0, 0)

    if return_month == 12:
        end_dt = datetime(return_year + 1, 1, 1, 0, 0, 0)
    else:
        end_dt = datetime(return_year, return_month + 1, 1, 0, 0, 0)

    start_jd = datetime_to_jd(start_dt)
    end_jd = datetime_to_jd(end_dt)

    step = 1 / 1440

    prev_jd = start_jd
    prev_moon = calc_body_longitude(prev_jd, swe.MOON)
    prev_delta = normalize(prev_moon - natal_moon_lon)

    if prev_delta > 180:
        prev_delta -= 360

    jd = start_jd + step

    while jd <= end_jd:
        current_moon = calc_body_longitude(jd, swe.MOON)
        current_delta = normalize(current_moon - natal_moon_lon)

        if current_delta > 180:
            current_delta -= 360

        if abs(current_delta) < 0.001:
            return jd

        if current_delta * prev_delta < 0:
            low = prev_jd
            high = jd

            for _ in range(100):
                mid = (low + high) / 2
                mid_moon = calc_body_longitude(mid, swe.MOON)
                mid_delta = normalize(mid_moon - natal_moon_lon)

                if mid_delta > 180:
                    mid_delta -= 360

                if mid_delta * (normalize(prev_moon - natal_moon_lon) if normalize(prev_moon - natal_moon_lon) <= 180 else normalize(prev_moon - natal_moon_lon) - 360) < 0:
                    high = mid
                else:
                    low = mid

            return (low + high) / 2

        prev_jd = jd
        prev_delta = current_delta
        jd += step

    raise ValueError("No Lunar Return found in selected month.")


# =====================================================
# Aspect Calculation
# =====================================================

def get_impact(orb_value):
    if orb_value <= 0.5:
        return "Highest"
    if orb_value <= 1:
        return "Very High"
    if orb_value <= 2:
        return "High"
    if orb_value <= 4:
        return "Medium"
    return "Low"


def calculate_overlay_aspects(return_planets, natal_planets):
    allowed_names = {
        "Sun", "Moon", "Mercury", "Venus", "Mars", "Jupiter", "Saturn",
        "Uranus", "Neptune", "Pluto",
        "North Node", "South Node",
        "Black Moon Lilith", "White Moon Selena",
        "Ceres", "Pallas", "Juno", "Vesta",
        "Midheaven", "Imum Coeli", "Ascendant", "Descendant",
        "Vertex", "Part of Fortune",
    }

    planet_order = ["Sun", "Moon", "Mercury", "Venus", "Mars", "Jupiter", "Saturn",
                    "Uranus", "Neptune", "Pluto", "North Node", "South Node",
                    "Black Moon Lilith", "White Moon Selena", "Ceres", "Pallas",
                    "Juno", "Vesta", "Midheaven", "Imum Coeli",
                    "Ascendant", "Descendant", "Vertex", "Part of Fortune"]

    aspects = []

    natal_lookup = {p["name"]: p for p in natal_planets}
    return_lookup = {p["name"]: p for p in return_planets}

    for np_name in planet_order:
        if np_name not in allowed_names or np_name not in natal_lookup:
            continue

        np_data = natal_lookup[np_name]
        np_order = planet_order.index(np_name)

        for rp_name in planet_order:
            if rp_name not in allowed_names or rp_name not in return_lookup:
                continue

            rp_data = return_lookup[rp_name]
            diff = angle_diff(rp_data["longitude"], np_data["longitude"])

            for aspect_name, asp in ASPECTS.items():
                orb_value = abs(diff - asp["angle"])

                if orb_value <= asp["orb"]:
                    aspects.append({
                        "return_planet": rp_name,
                        "return_symbol": rp_data["symbol"],
                        "return_house": rp_data["house"],
                        "natal_planet": np_name,
                        "natal_symbol": np_data["symbol"],
                        "natal_house": np_data["house"],
                        "aspect": aspect_name,
                        "aspect_symbol": asp["symbol"],
                        "aspect_type": asp["type"],
                        "orb": decimal_to_dms(orb_value),
                        "orb_value": round(orb_value, 4),
                        "effect": asp["effect"],
                        "impact": get_impact(orb_value),
                        "sort_order": np_order,
                    })

    aspects.sort(key=lambda x: (x["sort_order"], x["orb_value"]))
    return aspects


# =====================================================
# Kerykeion SVG Chart Generation
# =====================================================

def timezone_to_tz_str(timezone_value):
    """
    Kerykeion needs IANA timezone string.
    Your frontend sends 5.5, so we use Asia/Kolkata.
    """
    try:
        tz = float(timezone_value)
    except Exception:
        return "Asia/Kolkata"

    if tz == 5.5:
        return "Asia/Kolkata"

    return "Asia/Kolkata"


def safe_filename_name(name):
    clean = "".join(
        c for c in str(name)
        if c.isalnum() or c in (" ", "-", "_")
    ).strip()

    return clean or "User"


def generate_lunar_return_svg(payload, return_local_dt):
    """
    Generates Lunar Return dual wheel.

    IMPORTANT:
    first_obj  = Natal chart / inner wheel
    second_obj = Lunar Return chart / outer wheel

    If you reverse these, Natal and Lunar Return dates get swapped.
    """

    safe_name = safe_filename_name(payload.name)

    birth_tz_str = timezone_to_tz_str(payload.timezone)
    return_tz_str = timezone_to_tz_str(payload.return_timezone)

    natal_subject = AstrologicalSubject(
        name=f"{safe_name}_Natal",
        year=payload.year,
        month=payload.month,
        day=payload.day,
        hour=payload.hour,
        minute=payload.minute,
        city=payload.birth_place or "Birth Place",
        nation="IN",
        lat=payload.latitude,
        lng=payload.longitude,
        tz_str=birth_tz_str,
    )

    lunar_subject = AstrologicalSubject(
        name=f"{safe_name}_Lunar_Return_{payload.return_year}_{payload.return_month:02d}",
        year=return_local_dt.year,
        month=return_local_dt.month,
        day=return_local_dt.day,
        hour=return_local_dt.hour,
        minute=return_local_dt.minute,
        city=payload.return_place or payload.birth_place or "Return Place",
        nation="IN",
        lat=payload.return_latitude,
        lng=payload.return_longitude,
        tz_str=return_tz_str,
    )

    before_files = set(glob.glob(os.path.join(LUNAR_RETURN_CHART_DIR, "*.svg")))

    chart = KerykeionChartSVG(
        first_obj=natal_subject,
        chart_type="Transit",
        second_obj=lunar_subject,
        new_output_directory=LUNAR_RETURN_CHART_DIR,
    )

    chart.makeSVG()

    after_files = set(glob.glob(os.path.join(LUNAR_RETURN_CHART_DIR, "*.svg")))
    new_files = list(after_files - before_files)

    if new_files:
        latest_file = max(new_files, key=os.path.getmtime)
    else:
        svg_files = glob.glob(os.path.join(LUNAR_RETURN_CHART_DIR, "*.svg"))

        if not svg_files:
            raise RuntimeError("Lunar Return SVG chart was not generated.")

        latest_file = max(svg_files, key=os.path.getmtime)

    base_name = os.path.splitext(os.path.basename(latest_file))[0]
    wheel_filename = f"{base_name}_wheel.svg"
    wheel_path = os.path.join(LUNAR_RETURN_CHART_DIR, wheel_filename)

    ok = extract_lunar_return_wheel(latest_file, wheel_path)

    if ok:
        return f"/charts/lunar-return/{wheel_filename}"

    filename = os.path.basename(latest_file)
    return f"/charts/lunar-return/{filename}"


# =====================================================
# Main API Calculation Function
# =====================================================

def calculate_lunar_return(payload):
    birth_ut = local_to_ut(
        payload.year,
        payload.month,
        payload.day,
        payload.hour,
        payload.minute,
        payload.second,
        payload.timezone,
    )

    birth_jd = datetime_to_jd(birth_ut)

    natal_planets = calc_planets_with_houses(
        birth_jd,
        payload.latitude,
        payload.longitude,
    )

    natal_moon = next(p for p in natal_planets if p["name"] == "Moon")
    natal_moon_lon = natal_moon["longitude"]

    lunar_return_jd = find_lunar_return_jd(
        natal_moon_lon,
        payload.return_year,
        payload.return_month,
    )

    return_local_dt = jd_to_local_datetime(
        lunar_return_jd,
        payload.return_timezone,
    )

    lunar_return_planets = calc_planets_with_houses(
        lunar_return_jd,
        payload.return_latitude,
        payload.return_longitude,
    )

    overlay_aspects = calculate_overlay_aspects(
        lunar_return_planets,
        natal_planets,
    )

    major_overlay_aspects = [
        a for a in overlay_aspects if a["aspect_type"] == "Major"
    ]

    minor_overlay_aspects = [
        a for a in overlay_aspects if a["aspect_type"] == "Minor"
    ]

    chart_url = generate_lunar_return_svg(payload, return_local_dt)

    return {
        "chart_type": "lunar_return",
        "chart_url": chart_url,

        "details": {
            "name": payload.name,

            "birth_date": f"{payload.year}-{payload.month:02d}-{payload.day:02d}",
            "birth_time": f"{payload.hour:02d}:{payload.minute:02d}",
            "birth_place": payload.birth_place,
            "birth_latitude": payload.latitude,
            "birth_longitude": payload.longitude,
            "birth_timezone": payload.timezone,

            "house_system": "Placidus",

            "return_date": return_local_dt.strftime("%Y-%m-%d"),
            "return_time": return_local_dt.strftime("%H:%M"),
            "return_place": payload.return_place or payload.birth_place,
            "return_latitude": payload.return_latitude,
            "return_longitude": payload.return_longitude,
            "return_timezone": payload.return_timezone,
        },

        "section_1_details": {
            "title": "Details of Your Lunar Return",
            "birth_details": {
                "birth_date": f"{payload.year}-{payload.month:02d}-{payload.day:02d}",
                "birth_time": f"{payload.hour:02d}:{payload.minute:02d}",
                "location": payload.birth_place,
                "planet_color": "INDIGO",
            },
            "chart_details": {
                "house_system": "Placidus",
            },
            "lunar_return_details": {
                "return_date": return_local_dt.strftime("%Y-%m-%d"),
                "return_time": return_local_dt.strftime("%H:%M"),
                "location": payload.return_place or payload.birth_place,
                "planet_color": "PINK",
            },
        },

        "section_2_planet_positions": {
            "title": "Planet Positions",
            "natal_planets": natal_planets,
            "lunar_return_planets": lunar_return_planets,
        },

        "section_3_overlay_aspects": {
            "title": "Lunar Return Overlay Aspects",
            "all": overlay_aspects,
            "major": major_overlay_aspects,
            "minor": minor_overlay_aspects,
        },

        "natal_planets": natal_planets,
        "lunar_return_planets": lunar_return_planets,
        "overlay_aspects": overlay_aspects,
    }