from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from pathlib import Path
from datetime import datetime
from zoneinfo import ZoneInfo
import random
import shutil

import swisseph as swe

from kerykeion import (
    AstrologicalSubjectFactory,
    CompositeSubjectFactory,
    ChartDataFactory,
    ChartDrawer,
)

from chart_utils import extract_composite_wheel


# =====================================================
# Router / Paths
# =====================================================

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

BASE_DIR = Path(__file__).resolve().parent
CHART_DIR = BASE_DIR / "charts"
COMPOSITE_CHART_DIR = CHART_DIR / "composite-chart"
COMPOSITE_CHART_DIR.mkdir(parents=True, exist_ok=True)

COMPOSITE_CHART_RESULTS = {}

EPHE_PATH = "/usr/share/ephe"
if Path(EPHE_PATH).exists():
    swe.set_ephe_path(EPHE_PATH)


# =====================================================
# Request Models
# =====================================================

class PersonBirthData(BaseModel):
    name: str
    year: int
    month: int
    day: int
    hour: int
    minute: int = 0
    latitude: float
    longitude: float
    timezone: str = "Asia/Kolkata"


class CompositeChartRequest(BaseModel):
    person1: PersonBirthData
    person2: PersonBirthData


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

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

SIGN_ABBREV_TO_FULL = {
    "Ari": "Aries",
    "Tau": "Taurus",
    "Gem": "Gemini",
    "Can": "Cancer",
    "Leo": "Leo",
    "Vir": "Virgo",
    "Lib": "Libra",
    "Sco": "Scorpio",
    "Sag": "Sagittarius",
    "Cap": "Capricorn",
    "Aqu": "Aquarius",
    "Pis": "Pisces",
}

PLANET_FULL_NAMES = {
    "sun": "Sun",
    "moon": "Moon",
    "mercury": "Mercury",
    "venus": "Venus",
    "mars": "Mars",
    "jupiter": "Jupiter",
    "saturn": "Saturn",
    "uranus": "Uranus",
    "neptune": "Neptune",
    "pluto": "Pluto",
    "north_node": "North Node",
    "south_node": "South Node",
    "black_moon_lilith": "Black Moon Lilith",
    "chiron": "Chiron",
    "ceres": "Ceres",
    "juno": "Juno",
    "pallas": "Pallas",
    "vesta": "Vesta",
    "white_moon_selena": "White Moon Selena",
    "pholus": "Pholus",
    "ascendant": "Ascendant",
    "descendant": "Descendant",
    "midheaven": "Midheaven",
    "imum_coeli": "Imum Coeli",
    "vertex": "Vertex",
    "part_of_fortune": "Part of Fortune",
}

BODY_IDS = {
    "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,
    "chiron": 2060,
    "ceres": swe.CERES,
    "juno": swe.JUNO,
    "pallas": swe.PALLAS,
    "vesta": swe.VESTA,
    "pholus": 5145,
}

OUTPUT_ORDER = [
    "sun",
    "moon",
    "mercury",
    "venus",
    "mars",
    "jupiter",
    "saturn",
    "uranus",
    "neptune",
    "pluto",
    "north_node",
    "south_node",
    "black_moon_lilith",
    "ceres",
    "juno",
    "pallas",
    "vesta",
    "chiron",
    "pholus",
    "white_moon_selena",
    "ascendant",
    "descendant",
    "midheaven",
    "imum_coeli",
    "vertex",
    "part_of_fortune",
]

ASPECT_DEFINITIONS = {
    # Major Aspects
    "conjunction": {
        "angle": 0,
        "orb": 10,
        "symbol": "☌",
        "type": "Major",
        "effect": "Powerful",
    },
    "opposition": {
        "angle": 180,
        "orb": 10,
        "symbol": "☍",
        "type": "Major",
        "effect": "Challenging",
    },
    "trine": {
        "angle": 120,
        "orb": 8,
        "symbol": "△",
        "type": "Major",
        "effect": "Harmonious",
    },
    "square": {
        "angle": 90,
        "orb": 8,
        "symbol": "□",
        "type": "Major",
        "effect": "Tension",
    },
    "sextile": {
        "angle": 60,
        "orb": 6,
        "symbol": "⚹",
        "type": "Major",
        "effect": "Supportive",
    },

    # Minor Aspects
    "semisextile": {
        "angle": 30,
        "orb": 3,
        "symbol": "⚺",
        "type": "Minor",
        "effect": "Subtle",
    },
    "semisquare": {
        "angle": 45,
        "orb": 3,
        "symbol": "∠",
        "type": "Minor",
        "effect": "Minor Tension",
    },
    "quintile": {
        "angle": 72,
        "orb": 2,
        "symbol": "Q",
        "type": "Minor",
        "effect": "Creative",
    },
    "sesquiquadrate": {
        "angle": 135,
        "orb": 3,
        "symbol": "⚼",
        "type": "Minor",
        "effect": "Adjustment",
    },
    "biquintile": {
        "angle": 144,
        "orb": 2,
        "symbol": "bQ",
        "type": "Minor",
        "effect": "Creative",
    },
    "quincunx": {
        "angle": 150,
        "orb": 4,
        "symbol": "⚻",
        "type": "Minor",
        "effect": "Adjustment",
    },
}

ASPECT_BODY_IDS = {
    "sun",
    "moon",
    "mercury",
    "venus",
    "mars",
    "jupiter",
    "saturn",
    "uranus",
    "neptune",
    "pluto",
    "north_node",
    "south_node",
    "black_moon_lilith",
    "chiron",
    "ceres",
    "juno",
    "pallas",
    "vesta",
    "white_moon_selena",
    "pholus",
}


# =====================================================
# Kerykeion Subject
# =====================================================

def create_subject(data: PersonBirthData):
    return AstrologicalSubjectFactory.from_birth_data(
        name=data.name,
        year=data.year,
        month=data.month,
        day=data.day,
        hour=data.hour,
        minute=data.minute,
        lng=data.longitude,
        lat=data.latitude,
        tz_str=data.timezone,
        online=False,
        houses_system_identifier="P",
        zodiac_type="Tropical",
    )


# =====================================================
# Swiss Ephemeris Helpers
# =====================================================

def normalize(deg):
    return deg % 360


def midpoint_deg(a, b):
    """
    Correct circular midpoint.
    Example: midpoint of 350° and 10° = 0°, not 180°.
    """
    a = normalize(a)
    b = normalize(b)
    diff = ((b - a + 180) % 360) - 180
    return normalize(a + diff / 2)


def angle_diff(a, b):
    """
    Smallest angular distance.
    Example: 350° and 10° = 20°.
    """
    d = abs(a - b) % 360
    return min(d, 360 - d)


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


def degree_in_sign(lon):
    return normalize(lon) % 30


def decimal_to_orb_text(value):
    degree = int(value)
    minute = int(round((value - degree) * 60))

    if minute == 60:
        degree += 1
        minute = 0

    return f"{degree}° {minute:02d}'"


def local_to_jd(data: PersonBirthData):
    tz = ZoneInfo(data.timezone)

    local_dt = datetime(
        data.year,
        data.month,
        data.day,
        data.hour,
        data.minute,
        0,
        tzinfo=tz,
    )

    utc_dt = local_dt.astimezone(ZoneInfo("UTC"))

    return swe.julday(
        utc_dt.year,
        utc_dt.month,
        utc_dt.day,
        utc_dt.hour + utc_dt.minute / 60 + utc_dt.second / 3600,
    )


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_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 build_planet_item(key, longitude, cusps):
    lon = normalize(longitude)

    return {
        "id": key,
        "name": PLANET_FULL_NAMES.get(key, key.replace("_", " ").title()),
        "sign": get_sign(lon),
        "degree": round(degree_in_sign(lon), 2),
        "longitude": round(lon, 6),
        "house": get_house(lon, cusps),
        "retrograde": False,
    }


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


def calc_vertex_fallback(asc_lon):
    return normalize(asc_lon + 150)


# =====================================================
# Composite Planets
# =====================================================

def calculate_composite_planets(
    person1_data: PersonBirthData,
    person2_data: PersonBirthData,
):
    """
    Composite planet calculation using Swiss Ephemeris.

    Logic:
    1. Calculate both persons' planetary longitudes.
    2. Composite longitude = circular midpoint.
    3. Composite houses = midpoint of both natal house cusps.
    4. Missing points like South Node, Selena, Vertex, Fortune are derived.
    """

    jd1 = local_to_jd(person1_data)
    jd2 = local_to_jd(person2_data)

    houses1 = calc_houses(
        jd1,
        person1_data.latitude,
        person1_data.longitude,
    )

    houses2 = calc_houses(
        jd2,
        person2_data.latitude,
        person2_data.longitude,
    )

    composite_cusps = [
        midpoint_deg(houses1["cusps"][i], houses2["cusps"][i])
        for i in range(12)
    ]

    raw = {}

    for key, body_id in BODY_IDS.items():
        lon1 = calc_body_longitude(jd1, body_id)
        lon2 = calc_body_longitude(jd2, body_id)

        if lon1 is None or lon2 is None:
            continue

        raw[key] = midpoint_deg(lon1, lon2)

    if "north_node" in raw:
        raw["south_node"] = normalize(raw["north_node"] + 180)

    if "black_moon_lilith" in raw:
        raw["white_moon_selena"] = normalize(raw["black_moon_lilith"] + 180)

    composite_asc = midpoint_deg(
        houses1["ascendant"],
        houses2["ascendant"],
    )

    composite_mc = midpoint_deg(
        houses1["midheaven"],
        houses2["midheaven"],
    )

    raw["ascendant"] = composite_asc
    raw["descendant"] = normalize(composite_asc + 180)
    raw["midheaven"] = composite_mc
    raw["imum_coeli"] = normalize(composite_mc + 180)
    raw["vertex"] = calc_vertex_fallback(composite_asc)

    if "sun" in raw and "moon" in raw:
        raw["part_of_fortune"] = calc_part_of_fortune(
            raw["sun"],
            raw["moon"],
            composite_asc,
        )

    planets = []

    for key in OUTPUT_ORDER:
        if key in raw:
            planets.append(build_planet_item(key, raw[key], composite_cusps))

    return planets


# =====================================================
# Composite Aspects
# =====================================================

def calculate_composite_aspects(planets):
    """
    Calculate major and minor aspects from Swiss-based composite planets.
    """

    aspect_planets = [
        p for p in planets
        if p.get("id") in ASPECT_BODY_IDS and p.get("longitude") is not None
    ]

    aspects = []

    for i in range(len(aspect_planets)):
        for j in range(i + 1, len(aspect_planets)):
            p1 = aspect_planets[i]
            p2 = aspect_planets[j]

            diff = angle_diff(
                float(p1["longitude"]),
                float(p2["longitude"]),
            )

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

                if orb_value <= orb_limit:
                    aspects.append({
                        "planet1": p1["name"],
                        "planet1_id": p1["id"],

                        "planet2": p2["name"],
                        "planet2_id": p2["id"],

                        "aspect": aspect_name,
                        "aspect_name": aspect_name.title(),
                        "aspect_symbol": aspect_data["symbol"],
                        "aspect_type": aspect_data["type"],

                        "angle": exact_angle,
                        "actual_angle": round(diff, 4),

                        "orb": decimal_to_orb_text(orb_value),
                        "orb_value": round(orb_value, 4),

                        "effect": aspect_data["effect"],
                    })

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

    major_aspects = [
        a for a in aspects
        if a["aspect_type"] == "Major"
    ]

    minor_aspects = [
        a for a in aspects
        if a["aspect_type"] == "Minor"
    ]

    return {
        "aspects": aspects,
        "major_aspects": major_aspects,
        "minor_aspects": minor_aspects,
    }


# =====================================================
# Natal Signs - Using Swiss Ephemeris for accuracy
# =====================================================

def get_natal_signs_from_ephemeris(data: PersonBirthData):
    """
    Calculate Sun, Moon, and Ascendant signs using Swiss Ephemeris.
    This ensures consistency with planet positions.
    """
    tz = ZoneInfo(data.timezone)
    local_dt = datetime(
        data.year, data.month, data.day,
        data.hour, data.minute, 0, tzinfo=tz
    )
    utc_dt = local_dt.astimezone(ZoneInfo("UTC"))
    jd = swe.julday(
        utc_dt.year, utc_dt.month, utc_dt.day,
        utc_dt.hour + utc_dt.minute / 60 + utc_dt.second / 3600
    )

    # Calculate Sun
    sun_result = swe.calc_ut(jd, swe.SUN)[0]
    sun_lon = sun_result[0] % 360
    sun_sign = SIGNS[int(sun_lon // 30)]
    sun_degree = round(sun_lon % 30, 2)

    # Calculate Moon
    moon_result = swe.calc_ut(jd, swe.MOON)[0]
    moon_lon = moon_result[0] % 360
    moon_sign = SIGNS[int(moon_lon // 30)]
    moon_degree = round(moon_lon % 30, 2)

    # Calculate Ascendant
    cusps, ascmc = swe.houses(jd, data.latitude, data.longitude, b"P")
    asc_lon = ascmc[0] % 360
    asc_sign = SIGNS[int(asc_lon // 30)]
    asc_degree = round(asc_lon % 30, 2)

    return {
        "sun_sign": sun_sign,
        "sun_degree": sun_degree,
        "moon_sign": moon_sign,
        "moon_degree": moon_degree,
        "rising_sign": asc_sign,
        "rising_degree": asc_degree,
    }


# =====================================================
# Test API
# =====================================================

@router.get("/composite-chart/test")
def composite_chart_test():
    return {
        "success": True,
        "message": "Composite chart router is working",
    }


# =====================================================
# Main Composite Chart API
# =====================================================

@router.post("/composite-chart")
def generate_composite_chart(payload: CompositeChartRequest):
    try:
        person1 = create_subject(payload.person1)
        person2 = create_subject(payload.person2)

        composite_factory = CompositeSubjectFactory(person1, person2)
        composite_subject = composite_factory.get_midpoint_composite_subject_model()

        chart_data = ChartDataFactory.create_composite_chart_data(
            composite_subject
        )

        chart_drawer = ChartDrawer(
            chart_data=chart_data,
            theme="classic",
            chart_language="EN",
        )

        result_id = str(random.randint(1000000, 9999999))
        filename_without_ext = f"composite_chart_{result_id}"

        temp_dir = COMPOSITE_CHART_DIR / f"temp_{result_id}"
        temp_dir.mkdir(parents=True, exist_ok=True)

        chart_drawer.save_svg(
            output_path=temp_dir,
            filename=filename_without_ext,
        )

        full_svg_list = list(temp_dir.glob(f"{filename_without_ext}*"))

        if not full_svg_list:
            raise Exception("SVG file was not created in temp folder")

        full_svg = full_svg_list[0]
        final_svg = COMPOSITE_CHART_DIR / f"{filename_without_ext}.svg"

        extracted = extract_composite_wheel(str(full_svg), str(final_svg))

        if not extracted:
            shutil.copy(str(full_svg), str(final_svg))

        shutil.rmtree(temp_dir, ignore_errors=True)

        # Swiss Ephemeris full composite planet list
        planets = calculate_composite_planets(payload.person1, payload.person2)

        # Swiss Ephemeris composite aspects
        aspect_data = calculate_composite_aspects(planets)

        chart_data_dict = chart_data.model_dump()

        person1_natal = get_natal_signs_from_ephemeris(payload.person1)
        person2_natal = get_natal_signs_from_ephemeris(payload.person2)

        result_data = {
            "success": True,
            "id": result_id,
            "chart_type": "Composite Chart",

            "person1": payload.person1.name,
            "person2": payload.person2.name,

            "person1_details": {
                "year": payload.person1.year,
                "month": payload.person1.month,
                "day": payload.person1.day,
                "hour": payload.person1.hour,
                "minute": payload.person1.minute,
                "latitude": payload.person1.latitude,
                "longitude": payload.person1.longitude,
                "timezone": payload.person1.timezone,
            },

            "person2_details": {
                "year": payload.person2.year,
                "month": payload.person2.month,
                "day": payload.person2.day,
                "hour": payload.person2.hour,
                "minute": payload.person2.minute,
                "latitude": payload.person2.latitude,
                "longitude": payload.person2.longitude,
                "timezone": payload.person2.timezone,
            },

            "person1_natal": person1_natal,
            "person2_natal": person2_natal,

            "chart_url": f"/charts/composite-chart/{filename_without_ext}.svg",

            "composite_planets": planets,

            "aspects": aspect_data["aspects"],
            "major_aspects": aspect_data["major_aspects"],
            "minor_aspects": aspect_data["minor_aspects"],

            # Optional debug/source comparison
            "kerykeion_aspects": chart_data_dict.get("aspects", []),
        }

        COMPOSITE_CHART_RESULTS[result_id] = result_data

        return result_data

    except Exception as e:
        raise HTTPException(
            status_code=500,
            detail=f"Composite chart generation failed: {str(e)}",
        )


# =====================================================
# Get Stored Result
# =====================================================

@router.get("/composite-chart/{result_id}")
def get_composite_chart(result_id: str):
    result = COMPOSITE_CHART_RESULTS.get(result_id)

    if not result:
        raise HTTPException(
            status_code=404,
            detail="Composite chart result not found.",
        )

    return result