import os
from datetime import datetime, timedelta
from typing import Optional, List, Dict, Any

import pytz
import swisseph as swe
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from kerykeion import AstrologicalSubject, KerykeionChartSVG

from chart_utils import extract_progressed_wheel


router = APIRouter(prefix="/calculators", tags=["Progressed Chart"])

BASE_DIR = os.path.dirname(os.path.abspath(__file__))
CHARTS_DIR = os.path.join(BASE_DIR, "charts")
PROGRESSED_DIR = os.path.join(CHARTS_DIR, "progressed")
EPHE_PATH = os.path.join(BASE_DIR, "ephe")

os.makedirs(PROGRESSED_DIR, exist_ok=True)
swe.set_ephe_path(EPHE_PATH)


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


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


PLANETS = {
    "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,
}


EXTRA_BODIES = {
    "Chiron": 2060,
    "Pholus": 5145,
}


MAJOR_ASPECTS = {
    "Conjunction": {"angle": 0, "orb": 8, "symbol": "☌"},
    "Sextile": {"angle": 60, "orb": 6, "symbol": "✶"},
    "Square": {"angle": 90, "orb": 8, "symbol": "□"},
    "Trine": {"angle": 120, "orb": 8, "symbol": "△"},
    "Opposition": {"angle": 180, "orb": 8, "symbol": "☍"},
}


MINOR_ASPECTS = {
    "Semisextile": {"angle": 30, "orb": 3, "symbol": "⚺"},
    "Semisquare": {"angle": 45, "orb": 3, "symbol": "∠"},
    "Quintile": {"angle": 72, "orb": 3, "symbol": "⬠"},
    "Sesquiquadrate": {"angle": 135, "orb": 3, "symbol": "⚼"},
    "Biquintile": {"angle": 144, "orb": 3, "symbol": "±"},
    "Quincunx": {"angle": 150, "orb": 4, "symbol": "⚻"},
}


class ProgressedChartRequest(BaseModel):
    name: str = "User"

    birth_date: str
    birth_time: str

    birth_latitude: float
    birth_longitude: float
    birth_timezone: str = "Asia/Kolkata"
    birth_place: Optional[str] = "Birth Place"
    birth_nation: Optional[str] = "IN"

    current_date: Optional[str] = None
    house_system: Optional[str] = "Placidus"


def parse_local_datetime(date_str: str, time_str: str, timezone_str: str) -> datetime:
    try:
        tz = pytz.timezone(timezone_str)
        time_str = time_str.strip()

        if "AM" in time_str.upper() or "PM" in time_str.upper():
            naive = datetime.strptime(f"{date_str} {time_str}", "%Y-%m-%d %I:%M %p")
        else:
            naive = datetime.strptime(f"{date_str} {time_str}", "%Y-%m-%d %H:%M")

        return tz.localize(naive)

    except Exception as e:
        raise HTTPException(status_code=400, detail=f"Invalid date/time/timezone: {e}")


def calculate_age_years(birth_dt: datetime, current_dt: datetime) -> float:
    total_days = (current_dt - birth_dt).total_seconds() / 86400
    return total_days / 365.2425


def calculate_progressed_datetime(birth_dt: datetime, current_dt: datetime) -> datetime:
    """
    Secondary Progression:
    1 year of life = 1 day after birth.
    """
    age_years = calculate_age_years(birth_dt, current_dt)
    return birth_dt + timedelta(days=age_years)


def datetime_to_jd(dt: datetime) -> float:
    utc_dt = dt.astimezone(pytz.utc)
    hour_decimal = utc_dt.hour + utc_dt.minute / 60 + utc_dt.second / 3600

    return swe.julday(
        utc_dt.year,
        utc_dt.month,
        utc_dt.day,
        hour_decimal,
    )


def get_sign(longitude: float) -> str:
    return SIGNS[int(longitude // 30) % 12]


def degree_in_sign(longitude: float) -> float:
    return longitude % 30


def format_degree(value: float) -> str:
    deg = int(value)
    minutes = round((value - deg) * 60)

    if minutes == 60:
        deg += 1
        minutes = 0

    if deg >= 30:
        deg = 0

    return f"{deg}° {minutes}'"


def format_orb(value: float) -> str:
    deg = int(value)
    minutes = round((value - deg) * 60)

    if minutes == 60:
        deg += 1
        minutes = 0

    return f"{deg}° {minutes}'"


def angle_distance(a: float, b: float) -> float:
    diff = abs(a - b) % 360
    return min(diff, 360 - diff)


def calculate_houses(jd: float, lat: float, lon: float):
    try:
        cusps, ascmc = swe.houses(jd, lat, lon, b"P")
        return list(cusps), list(ascmc)
    except Exception as e:
        print("House calculation error:", e)
        return [], []


def get_house(longitude: float, cusps: List[float]) -> Optional[int]:
    if not cusps:
        return None

    lon = longitude % 360

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

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

    return 12


def make_body_item(
    id_key: str,
    name: str,
    longitude: float,
    cusps: List[float],
    retrograde: bool = False,
) -> Dict[str, Any]:
    longitude = longitude % 360

    return {
        "id": id_key,
        "planet": name,
        "sign": get_sign(longitude),
        "degree": format_degree(degree_in_sign(longitude)),
        "longitude": round(longitude, 6),
        "house": get_house(longitude, cusps),
        "retrograde": retrograde,
    }


def calculate_one_planet(
    planet_name: str,
    planet_id: int,
    jd: float,
    cusps: List[float],
) -> Optional[Dict[str, Any]]:
    try:
        result = swe.calc_ut(jd, planet_id, swe.FLG_SWIEPH | swe.FLG_SPEED)
        longitude = result[0][0] % 360
        speed = result[0][3]

        return make_body_item(
            id_key=planet_name.lower().replace(" ", "_"),
            name=planet_name,
            longitude=longitude,
            cusps=cusps,
            retrograde=speed < 0,
        )

    except Exception as e:
        print(f"Skipped planet {planet_name}:", e)
        return None


def calculate_white_moon_selena(jd: float, cusps: List[float]) -> Optional[Dict[str, Any]]:
    """
    White Moon Selena is not always directly available in pyswisseph.
    This is only a fallback symbolic point.
    Exact match with another website/API may not happen for Selena.
    """
    try:
        sun = swe.calc_ut(jd, swe.SUN, swe.FLG_SWIEPH)[0][0] % 360
        moon = swe.calc_ut(jd, swe.MOON, swe.FLG_SWIEPH)[0][0] % 360

        selena_lon = ((sun + moon) / 2) % 360

        return make_body_item(
            id_key="white_moon_selena",
            name="White Moon Selena",
            longitude=selena_lon,
            cusps=cusps,
            retrograde=False,
        )

    except Exception as e:
        print("White Moon Selena calculation error:", e)
        return None


def calculate_part_of_fortune(
    sun_lon: float,
    moon_lon: float,
    asc_lon: float,
    sun_house: Optional[int],
    cusps: List[float],
) -> Dict[str, Any]:
    """
    Part of Fortune:
    Day chart: ASC + Moon - Sun
    Night chart: ASC + Sun - Moon

    This uses house-based day/night check.
    """
    is_day_chart = sun_house in [7, 8, 9, 10, 11, 12]

    if is_day_chart:
        pof_lon = asc_lon + moon_lon - sun_lon
    else:
        pof_lon = asc_lon + sun_lon - moon_lon

    pof_lon = pof_lon % 360

    return make_body_item(
        id_key="part_of_fortune",
        name="Part of Fortune",
        longitude=pof_lon,
        cusps=cusps,
        retrograde=False,
    )


def sort_bodies(items: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
    order_map = {name: index for index, name in enumerate(ALL_BODIES_ORDER)}
    return sorted(items, key=lambda x: order_map.get(x.get("planet"), 999))


def get_body_order(name: str) -> int:
    try:
        return ALL_BODIES_ORDER.index(name)
    except ValueError:
        return 999


def calculate_all_planets(dt: datetime, lat: float, lon: float) -> List[Dict[str, Any]]:
    jd = datetime_to_jd(dt)
    cusps, ascmc = calculate_houses(jd, lat, lon)

    planets = []

    for name, planet_id in PLANETS.items():
        item = calculate_one_planet(name, planet_id, jd, cusps)
        if item:
            planets.append(item)

    for name, planet_id in EXTRA_BODIES.items():
        item = calculate_one_planet(name, planet_id, jd, cusps)
        if item:
            planets.append(item)

    north = next((p for p in planets if p["planet"] == "North Node"), None)
    if north:
        south_lon = (north["longitude"] + 180) % 360
        planets.append(
            make_body_item(
                id_key="south_node",
                name="South Node",
                longitude=south_lon,
                cusps=cusps,
                retrograde=north.get("retrograde", False),
            )
        )

    if ascmc:
        asc = ascmc[0] % 360
        mc = ascmc[1] % 360
        desc = (asc + 180) % 360
        ic = (mc + 180) % 360

        planets.append(make_body_item("ascendant", "Ascendant", asc, cusps))
        planets.append(make_body_item("descendant", "Descendant", desc, cusps))
        planets.append(make_body_item("midheaven", "Midheaven", mc, cusps))
        planets.append(make_body_item("imum_coeli", "Imum Coeli", ic, cusps))

        if len(ascmc) > 3:
            vertex = ascmc[3] % 360
            planets.append(make_body_item("vertex", "Vertex", vertex, cusps))

        sun = next((p for p in planets if p["planet"] == "Sun"), None)
        moon = next((p for p in planets if p["planet"] == "Moon"), None)

        if sun and moon:
            pof = calculate_part_of_fortune(
                sun_lon=sun["longitude"],
                moon_lon=moon["longitude"],
                asc_lon=asc,
                sun_house=sun.get("house"),
                cusps=cusps,
            )
            planets.append(pof)

    selena = calculate_white_moon_selena(jd, cusps)
    if selena:
        planets.append(selena)

    return sort_bodies(planets)


def get_aspect_between(
    lon1: float,
    lon2: float,
    aspect_defs: Dict[str, Dict[str, Any]],
) -> Optional[Dict[str, Any]]:
    diff = angle_distance(lon1, lon2)

    for aspect_name, aspect_data in aspect_defs.items():
        exact = aspect_data["angle"]
        orb_limit = aspect_data["orb"]
        orb = abs(diff - exact)

        if orb <= orb_limit:
            return {
                "aspect": aspect_name,
                "symbol": aspect_data["symbol"],
                "orb": format_orb(orb),
                "orb_value": round(orb, 4),
            }

    return None


def find_aspects_flat(
    progressed_planets: List[Dict[str, Any]],
    natal_planets: List[Dict[str, Any]],
    aspect_defs: Dict[str, Dict[str, Any]],
    aspect_type: str,
) -> List[Dict[str, Any]]:
    aspects = []

    for natal in natal_planets:
        natal_name = natal["planet"]
        natal_lon = natal["longitude"]

        for progressed in progressed_planets:
            progressed_name = progressed["planet"]
            progressed_lon = progressed["longitude"]

            aspect = get_aspect_between(
                progressed_lon,
                natal_lon,
                aspect_defs,
            )

            if aspect:
                aspect_name = aspect["aspect"]
                aspect_name_lower = aspect_name.lower()

                aspects.append({
                    "natal_planet": natal_name,
                    "progressed_planet": progressed_name,
                    "aspect": aspect_name,
                    "aspect_lower": aspect_name_lower,
                    "symbol": aspect["symbol"],
                    "orb": aspect["orb"],
                    "orb_value": aspect["orb_value"],
                    "type": aspect_type,

                    # For old UI
                    "text": f"Natal {natal_name} {aspect_name} Progressed {progressed_name}",

                    # For your desired UI:
                    # △
                    # Sun trine Mars orb: 0° 55'
                    "display_text": f"{natal_name} {aspect_name_lower} {progressed_name} orb: {aspect['orb']}",
                })

    # Match actual style: first planet order, then second planet order.
    # Not orb-wise sorting.
    aspects.sort(
        key=lambda x: (
            get_body_order(x["natal_planet"]),
            get_body_order(x["progressed_planet"]),
        )
    )

    return aspects


def group_aspects_by_natal_planet(
    progressed_planets: List[Dict[str, Any]],
    natal_planets: List[Dict[str, Any]],
    aspect_defs: Dict[str, Dict[str, Any]],
    aspect_type: str,
) -> List[Dict[str, Any]]:
    grouped = []

    natal_map = {p["planet"]: p for p in natal_planets}
    progressed_map = {p["planet"]: p for p in progressed_planets}

    for natal_name in ALL_BODIES_ORDER:
        natal = natal_map.get(natal_name)

        if not natal:
            continue

        items = []

        for progressed_name in ALL_BODIES_ORDER:
            progressed = progressed_map.get(progressed_name)

            if not progressed:
                continue

            aspect = get_aspect_between(
                progressed["longitude"],
                natal["longitude"],
                aspect_defs,
            )

            if aspect:
                aspect_name = aspect["aspect"]
                aspect_name_lower = aspect_name.lower()

                items.append({
                    "natal_planet": natal_name,
                    "progressed_planet": progressed_name,
                    "aspect": aspect_name,
                    "aspect_lower": aspect_name_lower,
                    "symbol": aspect["symbol"],
                    "orb": aspect["orb"],
                    "orb_value": aspect["orb_value"],
                    "type": aspect_type,
                    "text": f"Natal {natal_name} {aspect_name} Progressed {progressed_name}",
                    "display_text": f"{natal_name} {aspect_name_lower} {progressed_name} orb: {aspect['orb']}",
                })

        items.sort(key=lambda x: get_body_order(x["progressed_planet"]))

        grouped.append({
            "natal_planet": natal_name,
            "natal_sign": natal.get("sign"),
            "natal_degree": natal.get("degree"),
            "items": items,
        })

    return grouped


def debug_specific_aspect(
    natal_planets: List[Dict[str, Any]],
    progressed_planets: List[Dict[str, Any]],
    natal_name: str = "Sun",
    progressed_name: str = "Mars",
    aspect_angle: float = 120,
):
    natal_map = {p["planet"]: p for p in natal_planets}
    progressed_map = {p["planet"]: p for p in progressed_planets}

    if natal_name not in natal_map or progressed_name not in progressed_map:
        return

    lon1 = natal_map[natal_name]["longitude"]
    lon2 = progressed_map[progressed_name]["longitude"]
    diff = angle_distance(lon1, lon2)
    orb = abs(diff - aspect_angle)

    print(f"DEBUG Natal {natal_name} longitude:", lon1)
    print(f"DEBUG Progressed {progressed_name} longitude:", lon2)
    print(f"DEBUG distance:", diff)
    print(f"DEBUG orb:", format_orb(orb))


def safe_filename(value: str) -> str:
    return (
        value.replace(" ", "_")
        .replace("/", "_")
        .replace("\\", "_")
        .replace(":", "_")
    )


def generate_progressed_dual_chart(
    name: str,
    birth_dt: datetime,
    progressed_dt: datetime,
    lat: float,
    lon: float,
    timezone_str: str,
    city: str,
    nation: str,
) -> Optional[str]:
    try:
        before_files = set(os.listdir(PROGRESSED_DIR))

        natal_subject = AstrologicalSubject(
            name=f"{name} Natal",
            year=birth_dt.year,
            month=birth_dt.month,
            day=birth_dt.day,
            hour=birth_dt.hour,
            minute=birth_dt.minute,
            city=city or "Birth Place",
            nation=nation or "IN",
            lng=lon,
            lat=lat,
            tz_str=timezone_str,
        )

        progressed_subject = AstrologicalSubject(
            name=f"{name} Progressed",
            year=progressed_dt.year,
            month=progressed_dt.month,
            day=progressed_dt.day,
            hour=progressed_dt.hour,
            minute=progressed_dt.minute,
            city=city or "Birth Place",
            nation=nation or "IN",
            lng=lon,
            lat=lat,
            tz_str=timezone_str,
        )

        chart = KerykeionChartSVG(
            natal_subject,
            chart_type="Transit",
            second_obj=progressed_subject,
            new_output_directory=PROGRESSED_DIR,
        )

        chart.makeSVG()

        after_files = set(os.listdir(PROGRESSED_DIR))
        new_files = list(after_files - before_files)

        svg_files = [f for f in new_files if f.lower().endswith(".svg")]

        if not svg_files:
            svg_files = [
                f for f in os.listdir(PROGRESSED_DIR)
                if f.lower().endswith(".svg")
            ]

        if not svg_files:
            return None

        svg_files.sort(
            key=lambda f: os.path.getmtime(os.path.join(PROGRESSED_DIR, f)),
            reverse=True,
        )

        latest_svg = svg_files[0]
        latest_path = os.path.join(PROGRESSED_DIR, latest_svg)

        final_name = safe_filename(
            f"{name}_progressed_wheel_{datetime.now().strftime('%Y%m%d%H%M%S')}.svg"
        )
        final_path = os.path.join(PROGRESSED_DIR, final_name)

        extract_progressed_wheel(latest_path, final_path)

        return f"/charts/progressed/{final_name}"

    except Exception as e:
        print("Progressed chart generation error:", e)
        return None


@router.post("/progressed-chart")
def progressed_chart(req: ProgressedChartRequest):
    birth_dt = parse_local_datetime(
        req.birth_date,
        req.birth_time,
        req.birth_timezone,
    )

    if req.current_date:
        current_dt = parse_local_datetime(
            req.current_date,
            "12:00",
            req.birth_timezone,
        )
    else:
        current_dt = datetime.now(pytz.timezone(req.birth_timezone))

    if current_dt <= birth_dt:
        raise HTTPException(
            status_code=400,
            detail="Current date must be after birth date.",
        )

    age_years = calculate_age_years(birth_dt, current_dt)
    progressed_dt = calculate_progressed_datetime(birth_dt, current_dt)

    natal_planets = calculate_all_planets(
        birth_dt,
        req.birth_latitude,
        req.birth_longitude,
    )

    progressed_planets = calculate_all_planets(
        progressed_dt,
        req.birth_latitude,
        req.birth_longitude,
    )

    # Debug for checking exact mismatch with actual result
    debug_specific_aspect(
        natal_planets=natal_planets,
        progressed_planets=progressed_planets,
        natal_name="Sun",
        progressed_name="Mars",
        aspect_angle=120,
    )

    major_aspects = find_aspects_flat(
        progressed_planets=progressed_planets,
        natal_planets=natal_planets,
        aspect_defs=MAJOR_ASPECTS,
        aspect_type="major",
    )

    minor_aspects = find_aspects_flat(
        progressed_planets=progressed_planets,
        natal_planets=natal_planets,
        aspect_defs=MINOR_ASPECTS,
        aspect_type="minor",
    )

    major_aspects_by_natal = group_aspects_by_natal_planet(
        progressed_planets=progressed_planets,
        natal_planets=natal_planets,
        aspect_defs=MAJOR_ASPECTS,
        aspect_type="major",
    )

    minor_aspects_by_natal = group_aspects_by_natal_planet(
        progressed_planets=progressed_planets,
        natal_planets=natal_planets,
        aspect_defs=MINOR_ASPECTS,
        aspect_type="minor",
    )

    chart_path = generate_progressed_dual_chart(
        name=req.name,
        birth_dt=birth_dt,
        progressed_dt=progressed_dt,
        lat=req.birth_latitude,
        lon=req.birth_longitude,
        timezone_str=req.birth_timezone,
        city=req.birth_place or "Birth Place",
        nation=req.birth_nation or "IN",
    )

    print("Age:", round(age_years, 2))
    print("Progressed Date:", progressed_dt.strftime("%Y-%m-%d %H:%M"))
    print("Chart Path:", chart_path)

    return {
        "name": req.name,

        "birth_date": birth_dt.strftime("%Y-%m-%d"),
        "birth_time": birth_dt.strftime("%H:%M"),
        "birth_location": req.birth_place or "",
        "birth_latitude": req.birth_latitude,
        "birth_longitude": req.birth_longitude,
        "birth_timezone": req.birth_timezone,

        "current_date": current_dt.strftime("%Y-%m-%d"),
        "age": round(age_years, 2),

        "progressed_date": progressed_dt.strftime("%Y-%m-%d"),
        "progressed_time": progressed_dt.strftime("%H:%M"),

        "house_system": req.house_system or "Placidus",

        "chart": chart_path,

        "natal_planets": natal_planets,
        "progressed_planets": progressed_planets,

        "major_aspects": major_aspects,
        "minor_aspects": minor_aspects,

        "progressed_planet_aspects": {
            "major": major_aspects,
            "minor": minor_aspects,
            "major_by_natal": major_aspects_by_natal,
            "minor_by_natal": minor_aspects_by_natal,
        },

        "details": {
            "place": req.birth_place or "",
            "latitude": req.birth_latitude,
            "longitude": req.birth_longitude,
        },
    }