"""
opensky.py — Zone position data from OpenSky Network.
Responsibilities:
- OAuth token management (auto-refresh)
- Bounding box zone search returning StateVector list
- Global callsign search for tracked flight (free)
- Flight trail fetch by icao24 hex (for farthest flights)
- Unit conversions (m/s → knots, metres → feet)
- Filters out ground traffic outside the configured altitude range
"""

import requests
import time
from requests.exceptions import ConnectionError
from urllib3.exceptions import NewConnectionError, MaxRetryError

try:
    from config import OPENSKY_CLIENT_ID, OPENSKY_CLIENT_SECRET
except (ImportError, ModuleNotFoundError, NameError):
    OPENSKY_CLIENT_ID = ""
    OPENSKY_CLIENT_SECRET = ""

try:
    from config import MIN_ALTITUDE, MAX_ALTITUDE
except (ImportError, ModuleNotFoundError, NameError):
    MIN_ALTITUDE = 0
    MAX_ALTITUDE = 100000

try:
    from config import ZONE_HOME
    ZONE_DEFAULT = ZONE_HOME
except (ImportError, ModuleNotFoundError, NameError):
    ZONE_DEFAULT = {"tl_y": 41.904318, "tl_x": -87.647367,
                    "br_y": 41.851654, "br_x": -87.573027}

TOKEN_URL = (
    "https://auth.opensky-network.org/auth/realms/opensky-network"
    "/protocol/openid-connect/token"
)
BASE_URL = "https://opensky-network.org"


def _bounding_box_params():
    try:
        import config
        config.reload()
        zone = config.ZONE_HOME
    except Exception:
        zone = ZONE_DEFAULT
    north = max(float(zone["tl_y"]), float(zone["br_y"]))
    south = min(float(zone["tl_y"]), float(zone["br_y"]))
    west = min(float(zone["tl_x"]), float(zone["br_x"]))
    east = max(float(zone["tl_x"]), float(zone["br_x"]))
    return {
        "lamin": south,
        "lamax": north,
        "lomin": west,
        "lomax": east,
    }


def _parse_state(s):
    """Parse a raw OpenSky state vector into a standardised dict."""
    if len(s) < 17:
        return None
    icao24       = s[0] or ""
    callsign     = (s[1] or "").strip()
    on_ground    = s[8] or False
    latitude     = s[6]
    longitude    = s[5]
    baro_alt_m   = s[7]
    velocity_ms  = s[9]
    true_track   = s[10]
    vert_rate_ms = s[11]
    time_position = s[3]
    last_contact = s[4]

    if on_ground or latitude is None or longitude is None:
        return None
    # last_contact can be fresh while the coordinates are stale. The
    # bounding-box display must use the age of the actual position.
    if not time_position or time.time() - time_position > 30:
        return None

    alt_ft = (baro_alt_m or 0) * 3.28084

    return {
        "icao24":         icao24,
        "callsign":       callsign,
        "latitude":       latitude,
        "longitude":      longitude,
        "altitude":       int(alt_ft),
        "ground_speed":   int((velocity_ms or 0) * 1.94384),
        "heading":        true_track or 0,
        "vertical_speed": int((vert_rate_ms or 0) * 196.85),
        "on_ground":      on_ground,
        "position_age_seconds": max(0, time.time() - time_position),
        "contact_age_seconds": max(0, time.time() - last_contact) if last_contact else None,
        "source": "OpenSky",
    }


class OpenSkyClient:
    """Thin OpenSky REST client with automatic OAuth token refresh."""

    def __init__(self):
        self._token        = None
        self._token_expiry = 0

    # ------------------------------------------------------------------
    # Token management
    # ------------------------------------------------------------------

    def _refresh_token(self):
        if not OPENSKY_CLIENT_ID or not OPENSKY_CLIENT_SECRET:
            return
        try:
            resp = requests.post(
                TOKEN_URL,
                data={
                    "grant_type":    "client_credentials",
                    "client_id":     OPENSKY_CLIENT_ID,
                    "client_secret": OPENSKY_CLIENT_SECRET,
                },
                timeout=15,
            )
            resp.raise_for_status()
            data = resp.json()
            self._token        = data["access_token"]
            self._token_expiry = time.time() + data.get("expires_in", 1800) - 60
        except Exception as e:
            print(f"OpenSky token refresh failed: {e}")
            self._token = None

    def _ensure_token(self):
        if OPENSKY_CLIENT_ID and (not self._token or time.time() >= self._token_expiry):
            self._refresh_token()

    def _auth_headers(self):
        self._ensure_token()
        if self._token:
            return {"Authorization": f"Bearer {self._token}"}
        return {}

    # ------------------------------------------------------------------
    # Zone search
    # ------------------------------------------------------------------

    def get_zone_states(self):
        """
        Fetch state vectors for the configured zone.
        Returns a list of dicts with standardised field names,
        filtered to airborne flights within altitude limits.
        """
        params = _bounding_box_params()

        try:
            resp = requests.get(
                f"{BASE_URL}/api/states/all",
                headers=self._auth_headers(),
                params=params,
                timeout=15,
            )
            resp.raise_for_status()
            data = resp.json()
        except Exception as e:
            print(f"OpenSky zone fetch failed: {e}")
            return []

        raw_states = data.get("states") or []
        results    = []

        for s in raw_states:
            state = _parse_state(s)
            if not state:
                continue
            if not state["callsign"]:
                continue
            try:
                import config
                min_altitude = config.MIN_ALTITUDE
                max_altitude = config.MAX_ALTITUDE
            except Exception:
                min_altitude = MIN_ALTITUDE
                max_altitude = MAX_ALTITUDE
            alt_ft = state["altitude"]
            if alt_ft < min_altitude or alt_ft > max_altitude:
                continue
            results.append(state)

        return results

    # ------------------------------------------------------------------
    # Global callsign search (for tracked flight)
    # ------------------------------------------------------------------

    def find_callsign(self, callsign):
        """
        Search globally for a specific callsign using free ADS-B APIs.
        Returns a state dict if found airborne, or None.
        """
        callsign_clean = callsign.strip().upper()
        sources = (
            ("ADSBLol", f"https://api.adsb.lol/v2/callsign/{callsign_clean}"),
            ("AirplanesLive", f"https://api.airplanes.live/v2/callsign/{callsign_clean}"),
            ("ADSBfi", f"https://opendata.adsb.fi/api/v2/callsign/{callsign_clean}"),
        )
        try:
            for source, url in sources:
                try:
                    resp = requests.get(
                        url,
                        params={"_": int(time.time())},
                        headers={"Cache-Control": "no-cache", "Pragma": "no-cache"},
                        timeout=10,
                    )
                    if resp.status_code != 200:
                        continue
                    data = resp.json()
                    ac_list = data.get("ac", [])
                    if not ac_list:
                        continue

                    ac_list.sort(key=lambda ac: ac.get("seen_pos", ac.get("seen", 9999)) or 9999)
                    for ac in ac_list:
                        # Must match callsign exactly and be airborne
                        returned = (ac.get("flight") or ac.get("callsign") or "").strip().upper()
                        if returned != callsign_clean:
                            continue

                        gs = ac.get("gs", 0) or 0
                        on_ground = ac.get("alt_baro") == "ground" or bool(ac.get("ground", False)) or gs < 50
                        if on_ground:
                            continue

                        lat = ac.get("lat")
                        lon = ac.get("lon")
                        if lat is None or lon is None:
                            continue

                        alt_baro = ac.get("alt_baro", ac.get("alt_geom", 0))
                        alt_ft   = alt_baro if isinstance(alt_baro, (int, float)) else 0
                        vs       = ac.get("baro_rate", ac.get("geom_rate", 0)) or 0

                        print(f"[Tracked] {callsign_clean} live from {source}: "
                              f"lat={lat} lon={lon} alt={int(alt_ft)} seen={ac.get('seen_pos', ac.get('seen'))}")
                        return {
                            "icao24":        ac.get("hex", ac.get("icao", "")),
                            "callsign":      callsign_clean,
                            "latitude":      lat,
                            "longitude":     lon,
                            "altitude":      int(alt_ft),
                            "ground_speed":  int(gs),
                            "heading":       ac.get("track", ac.get("true_heading", 0)) or 0,
                            "vertical_speed": int(vs),
                            "on_ground":     False,
                            "source":        source,
                            "seen":          ac.get("seen"),
                            "seen_pos":      ac.get("seen_pos"),
                        }
                except Exception as e:
                    print(f"[{source}] Callsign search failed for {callsign_clean}: {e}")
                    continue
            return None
        except Exception as e:
            print(f"[adsb.lol] Callsign search failed for {callsign}: {e}")
            return None

    # ------------------------------------------------------------------
    # Flight trail (for farthest flights)
    # ------------------------------------------------------------------

    def get_flight_trail(self, icao24):
        """
        Fetch the actual flown trail for a live aircraft by icao24 hex code.
        Returns a list of {"lat": lat, "lon": lon} dicts or empty list.
        """
        if not icao24:
            return []
        try:
            # Use basic auth directly — more reliable than bearer token for tracks
            auth = None
            if OPENSKY_CLIENT_ID and OPENSKY_CLIENT_SECRET:
                auth = (OPENSKY_CLIENT_ID, OPENSKY_CLIENT_SECRET)

            resp = requests.get(
                f"{BASE_URL}/api/tracks/all",
                auth=auth,
                params={"icao24": icao24.lower(), "time": 0},
                timeout=15,
            )
            if resp.status_code != 200:
                return []
            data   = resp.json()
            points = data.get("path", [])
            # Each point: [time, lat, lon, baro_alt, true_track, on_ground]
            # Filter out ground points — keep airborne ones (on_ground=false)
            trail = []
            for p in points:
                if len(p) >= 6 and p[1] is not None and p[2] is not None:
                    on_ground = p[5]
                    if not on_ground:
                        trail.append([p[1], p[2]])
            return trail
        except Exception as e:
            print(f"[OpenSky] Trail fetch failed for {icao24}: {e}")
            return []
