"""
astroAPI — Single-File-Helper fuer Python.

Eine schlanke Klasse, die das Boilerplate fuer urllib + JSON + Auth-Header
wegnimmt. Keine externen Abhaengigkeiten — nur stdlib.

Voraussetzungen: Python 3.9+.

Beispiel:

    from astroapi_client import AstroApiClient

    api = AstroApiClient("ak_test_xxx")
    radix = api.radix({
        "birth": {
            "datetime": "1980-05-17T14:32:00",
            "timezone": "Europe/Berlin",
            "location": {"latitude": 52.52, "longitude": 13.405},
        },
        "options": {"house_system": "koch"},
    })
    print(radix["chart"]["planets"]["sun"])

Lizenz: MIT.
"""

from __future__ import annotations

import json
import urllib.error
import urllib.parse
import urllib.request
from typing import Any


class AstroApiException(Exception):
    """Wird geworfen bei HTTP-Status != 2xx oder Netzwerkfehler."""

    def __init__(self, message: str, status: int = 0, body: dict | None = None) -> None:
        super().__init__(message)
        self.status = status
        self.body = body


class AstroApiClient:
    BASE_LIVE = "https://astroapi.services"
    BASE_TEST = "http://astroapi.test"

    def __init__(
        self,
        api_key: str = "",
        base_url: str = BASE_LIVE,
        timeout: int = 30,
    ) -> None:
        # Leerer Key ist erlaubt fuer oeffentliche Endpunkte (z.B. /v1/health,
        # /v1/planets) — der X-API-Key-Header wird dann weggelassen.
        self.api_key = api_key
        self.base_url = base_url.rstrip("/")
        self.timeout = timeout

    # ---------- Allgemeine Endpunkte --------------------------------------

    def health(self) -> dict:
        return self.request("GET", "/v1/health")

    def me(self) -> dict:
        return self.request("GET", "/v1/me")

    def planets(self) -> dict:
        return self.request("GET", "/v1/planets")

    def houses(self, **query: Any) -> dict:
        return self.request("GET", "/v1/houses", query=query)

    # ---------- Berechnungs-Endpunkte (POST) ------------------------------

    def radix(self, body: dict) -> dict:               return self.request("POST", "/v1/radix", body)
    def aspects(self, body: dict) -> dict:             return self.request("POST", "/v1/aspects", body)
    def transits(self, body: dict) -> dict:            return self.request("POST", "/v1/transits", body)
    def progressions(self, body: dict) -> dict:        return self.request("POST", "/v1/progressions", body)
    def returns(self, body: dict) -> dict:             return self.request("POST", "/v1/returns", body)
    def mercury_return(self, body: dict) -> dict:      return self.request("POST", "/v1/mercury-return", body)
    def venus_return(self, body: dict) -> dict:        return self.request("POST", "/v1/venus-return", body)
    def mars_return(self, body: dict) -> dict:         return self.request("POST", "/v1/mars-return", body)
    def jupiter_return(self, body: dict) -> dict:      return self.request("POST", "/v1/jupiter-return", body)
    def saturn_return(self, body: dict) -> dict:       return self.request("POST", "/v1/saturn-return", body)
    def ephemeris(self, body: dict) -> dict:           return self.request("POST", "/v1/ephemeris", body)
    def asteroids(self, body: dict) -> dict:           return self.request("POST", "/v1/asteroids", body)
    def fixed_stars(self, body: dict) -> dict:         return self.request("POST", "/v1/fixed-stars", body)
    def declinations(self, body: dict) -> dict:        return self.request("POST", "/v1/declinations", body)
    def antiscia(self, body: dict) -> dict:            return self.request("POST", "/v1/antiscia", body)
    def harmonics(self, body: dict) -> dict:           return self.request("POST", "/v1/harmonics", body)
    def draconic(self, body: dict) -> dict:            return self.request("POST", "/v1/draconic", body)
    def midpoints(self, body: dict) -> dict:           return self.request("POST", "/v1/midpoints", body)
    def lunar_phases(self, body: dict) -> dict:        return self.request("POST", "/v1/lunar-phases", body)
    def ingresses(self, body: dict) -> dict:           return self.request("POST", "/v1/ingresses", body)
    def stations(self, body: dict) -> dict:            return self.request("POST", "/v1/stations", body)
    def eclipses(self, body: dict) -> dict:            return self.request("POST", "/v1/eclipses", body)
    def heliacal(self, body: dict) -> dict:            return self.request("POST", "/v1/heliacal", body)
    def rise_transit_set(self, body: dict) -> dict:    return self.request("POST", "/v1/rise-transit-set", body)
    def planetary_hours(self, body: dict) -> dict:     return self.request("POST", "/v1/planetary-hours", body)
    def patterns(self, body: dict) -> dict:            return self.request("POST", "/v1/patterns", body)
    def void_of_course(self, body: dict) -> dict:      return self.request("POST", "/v1/void-of-course", body)
    def relationship(self, body: dict) -> dict:        return self.request("POST", "/v1/relationship", body)

    # Hellenistische Endpunkte
    def lots(self, body: dict) -> dict:                return self.request("POST", "/v1/lots", body)
    def dignities(self, body: dict) -> dict:           return self.request("POST", "/v1/dignities", body)
    def profections(self, body: dict) -> dict:         return self.request("POST", "/v1/profections", body)
    def firdaria(self, body: dict) -> dict:            return self.request("POST", "/v1/firdaria", body)
    def zodiacal_releasing(self, body: dict) -> dict:  return self.request("POST", "/v1/zodiacal-releasing", body)

    # ---------- Generischer Aufruf ----------------------------------------

    def request(
        self,
        method: str,
        path: str,
        body: dict | None = None,
        query: dict | None = None,
    ) -> dict:
        """
        Eigene/neue Endpunkte ohne Convenience-Methode aufrufen.

        Wirft AstroApiException bei HTTP-Status != 2xx oder Netzwerkfehler.
        """
        url = self.base_url + path
        if query:
            url += ("&" if "?" in url else "?") + urllib.parse.urlencode(query)

        headers = {
            "Accept":     "application/json",
            "User-Agent": "astroAPI-Python-Helper/1.0",
        }
        if self.api_key:
            headers["X-API-Key"] = self.api_key
        data: bytes | None = None
        if method == "POST":
            data = json.dumps(body if body is not None else {}).encode("utf-8")
            headers["Content-Type"] = "application/json"

        req = urllib.request.Request(url, data=data, headers=headers, method=method)
        try:
            with urllib.request.urlopen(req, timeout=self.timeout) as resp:
                raw = resp.read().decode("utf-8")
                status = resp.status
        except urllib.error.HTTPError as e:
            raw = e.read().decode("utf-8")
            status = e.code
        except urllib.error.URLError as e:
            raise AstroApiException(f"Network error: {e.reason}", 0) from e

        try:
            decoded = json.loads(raw)
        except json.JSONDecodeError as e:
            raise AstroApiException(f"Invalid JSON response (status {status}).", status) from e

        if not isinstance(decoded, dict):
            raise AstroApiException(f"Unexpected response shape (status {status}).", status)
        if status >= 400:
            err_code = (decoded.get("error") or {}).get("code") or "http_error"
            err_text = (decoded.get("error") or {}).get("message") or f"HTTP {status}"
            raise AstroApiException(f"{err_code}: {err_text}", status, decoded)
        return decoded
