from itertools import combinations

PATTERN_ASPECTS = {
    "conjunction": {"degree": 0, "orb": 8},
    "sextile": {"degree": 60, "orb": 5},
    "square": {"degree": 90, "orb": 8},
    "trine": {"degree": 120, "orb": 6},
    "quincunx": {"degree": 150, "orb": 4},
    "sesquiquadrate": {"degree": 135, "orb": 3},
    "opposition": {"degree": 180, "orb": 8},
}

PATTERN_ALLOWED_BODIES = {
    "sun", "moon", "mercury", "venus", "mars",
    "jupiter", "saturn", "uranus", "neptune", "pluto",
}


def angle_diff(lon1, lon2):
    diff = abs(float(lon1) - float(lon2)) % 360
    return min(diff, 360 - diff)


def get_aspect(lon1, lon2):
    angle = angle_diff(lon1, lon2)

    for aspect, config in PATTERN_ASPECTS.items():
        orb = abs(angle - config["degree"])
        if orb <= config["orb"]:
            return {
                "aspect": aspect,
                "angle": round(angle, 2),
                "orb": round(orb, 2),
            }

    return None


def format_planet(key, data):
    return {
        "key": key,
        "name": key.replace("_", " ").title(),
        "sign": data.get("sign"),
        "degree": data.get("degree"),
        "house": data.get("house"),
        "longitude": data.get("longitude"),
    }


def create_pattern(pattern_name, planet_keys, planets):
    return {
        "type": pattern_name,
        "count": len(planet_keys),
        "planets": [format_planet(key, planets[key]) for key in planet_keys],
    }


def detect_chart_patterns(planets):
    patterns = {
        "grand_trine": [],
        "minor_grand_trine": [],
        "yod": [],
        "t_square": [],
        "kite": [],
        "grand_cross": [],
        "mystic_rectangle": [],
        "cradle": [],
        "grand_sextile": [],
        "thors_hammer": [],
        "stellium": [],
    }

    planet_keys = [
        key.lower()
        for key, value in planets.items()
        if key.lower() in PATTERN_ALLOWED_BODIES
        and isinstance(value, dict)
        and value.get("longitude") is not None
    ]

    def has_aspect(p1, p2, aspect_name):
        result = get_aspect(planets[p1]["longitude"], planets[p2]["longitude"])
        return result is not None and result["aspect"] == aspect_name

    def aspect_counts(keys):
        pairs = list(combinations(keys, 2))
        return {
            "conjunction": sum(1 for p1, p2 in pairs if has_aspect(p1, p2, "conjunction")),
            "sextile": sum(1 for p1, p2 in pairs if has_aspect(p1, p2, "sextile")),
            "square": sum(1 for p1, p2 in pairs if has_aspect(p1, p2, "square")),
            "trine": sum(1 for p1, p2 in pairs if has_aspect(p1, p2, "trine")),
            "quincunx": sum(1 for p1, p2 in pairs if has_aspect(p1, p2, "quincunx")),
            "sesquiquadrate": sum(1 for p1, p2 in pairs if has_aspect(p1, p2, "sesquiquadrate")),
            "opposition": sum(1 for p1, p2 in pairs if has_aspect(p1, p2, "opposition")),
        }

    seen = set()

    # -------------------------
    # 3 planet patterns
    # -------------------------
    for a, b, c in combinations(planet_keys, 3):
        combo = tuple(sorted([a, b, c]))
        counts = aspect_counts([a, b, c])

        if counts["trine"] == 3:
            key = ("grand_trine", combo)
            if key not in seen:
                patterns["grand_trine"].append(
                    create_pattern("Grand Trine", [a, b, c], planets)
                )
                seen.add(key)

        if counts["trine"] == 1 and counts["sextile"] == 2:
            key = ("minor_grand_trine", combo)
            if key not in seen:
                patterns["minor_grand_trine"].append(
                    create_pattern("Minor Grand Trine", [a, b, c], planets)
                )
                seen.add(key)

        if counts["quincunx"] == 2 and counts["sextile"] == 1:
            key = ("yod", combo)
            if key not in seen:
                patterns["yod"].append(
                    create_pattern("Yod", [a, b, c], planets)
                )
                seen.add(key)

        if counts["opposition"] == 1 and counts["square"] == 2:
            key = ("t_square", combo)
            if key not in seen:
                patterns["t_square"].append(
                    create_pattern("T Square", [a, b, c], planets)
                )
                seen.add(key)

        if counts["sesquiquadrate"] == 2 and counts["square"] == 1:
            key = ("thors_hammer", combo)
            if key not in seen:
                patterns["thors_hammer"].append(
                    create_pattern("Thors Hammer", [a, b, c], planets)
                )
                seen.add(key)

    # -------------------------
    # 4 planet patterns
    # -------------------------
    for a, b, c, d in combinations(planet_keys, 4):
        combo = tuple(sorted([a, b, c, d]))
        counts = aspect_counts([a, b, c, d])

        if counts["opposition"] == 2 and counts["square"] == 4:
            key = ("grand_cross", combo)
            if key not in seen:
                patterns["grand_cross"].append(
                    create_pattern("Grand Cross", [a, b, c, d], planets)
                )
                seen.add(key)

        if counts["opposition"] == 2 and counts["trine"] == 2 and counts["sextile"] == 2:
            key = ("mystic_rectangle", combo)
            if key not in seen:
                patterns["mystic_rectangle"].append(
                    create_pattern("Mystic Rectangle", [a, b, c, d], planets)
                )
                seen.add(key)

        if counts["opposition"] == 1 and counts["trine"] == 2 and counts["sextile"] == 3:
            key = ("cradle", combo)
            if key not in seen:
                patterns["cradle"].append(
                    create_pattern("Cradle", [a, b, c, d], planets)
                )
                seen.add(key)

        # Kite = one grand trine + fourth planet makes 1 opposition + 2 sextiles
        for three in combinations([a, b, c, d], 3):
            fourth = [p for p in [a, b, c, d] if p not in three][0]
            x, y, z = three

            is_grand_trine = (
                has_aspect(x, y, "trine")
                and has_aspect(x, z, "trine")
                and has_aspect(y, z, "trine")
            )

            opposition_to_one = sum(
                1 for p in three if has_aspect(fourth, p, "opposition")
            )

            sextile_to_two = sum(
                1 for p in three if has_aspect(fourth, p, "sextile")
            )

            if is_grand_trine and opposition_to_one == 1 and sextile_to_two == 2:
                key = ("kite", combo)
                if key not in seen:
                    patterns["kite"].append(
                        create_pattern("Kite", [a, b, c, d], planets)
                    )
                    seen.add(key)

    # -------------------------
    # 6 planet pattern
    # -------------------------
    for group in combinations(planet_keys, 6):
        combo = tuple(sorted(group))
        counts = aspect_counts(group)

        if counts["sextile"] >= 6 and counts["opposition"] >= 2 and counts["trine"] >= 2:
            key = ("grand_sextile", combo)
            if key not in seen:
                patterns["grand_sextile"].append(
                    create_pattern("Grand Sextile", list(group), planets)
                )
                seen.add(key)

    # -------------------------
    # Stellium = 3+ planets in same sign or house
    # -------------------------
    sign_groups = {}
    house_groups = {}

    for key in planet_keys:
        sign = planets[key].get("sign")
        house = planets[key].get("house")

        if sign:
            sign_groups.setdefault(sign, []).append(key)

        if house:
            house_groups.setdefault(house, []).append(key)

    for sign, group in sign_groups.items():
        if len(group) >= 3:
            patterns["stellium"].append(
                create_pattern(f"Stellium in {sign}", group, planets)
            )

    for house, group in house_groups.items():
        if len(group) >= 3:
            patterns["stellium"].append(
                create_pattern(f"Stellium in {house} House", group, planets)
            )

    return patterns