import requests
import time

from utilities.opensky import OpenSkyClient

try:
    from config import MIN_ALTITUDE, MAX_ALTITUDE, ZONE_HOME
except Exception:
    MIN_ALTITUDE = 0
    MAX_ALTITUDE = 100000
    ZONE_HOME = {"tl_y": 0.0, "tl_x": 0.0, "br_y": 0.0, "br_x": 0.0}

try:
    from config import _cfg
    _POSITION_CFG = _cfg.get("position_sources", {})
except Exception:
    _POSITION_CFG = {}


def _runtime_config():
    try:
        import config
        return config
    except Exception:
        return None


def _refresh_runtime_config():
    runtime = _runtime_config()
    if runtime:
        runtime.reload()


def _zone():
    runtime = _runtime_config()
    zone = getattr(runtime, "ZONE_HOME", ZONE_HOME) if runtime else ZONE_HOME
    north = _num(zone.get("tl_y"))
    south = _num(zone.get("br_y"))
    west = _num(zone.get("tl_x"))
    east = _num(zone.get("br_x"))
    return {
        "north": max(north, south),
        "south": min(north, south),
        "west": min(west, east),
        "east": max(west, east),
    }


def _enabled():
    runtime = _runtime_config()
    if runtime:
        return getattr(runtime, "POSITION_SOURCE_ENABLED", {})
    return _POSITION_CFG.get("enabled", {})


def _order():
    runtime = _runtime_config()
    if runtime:
        order = list(getattr(runtime, "POSITION_SOURCE_ORDER", ["OpenSky"]))
    else:
        order = list(_POSITION_CFG.get("order", ["OpenSky"]))
    for fallback in ("ADSBLol", "ADSBfi"):
        if fallback not in order:
            order.append(fallback)
    return order


def _num(value, default=0):
    try:
        return float(value)
    except (TypeError, ValueError):
        return default


def _altitude_ft(value):
    if value in ("ground", None, ""):
        return 0
    return int(_num(value))


def _parse_adsb_aircraft(ac):
    callsign = (ac.get("flight") or ac.get("callsign") or "").strip()
    lat = ac.get("lat")
    lon = ac.get("lon")
    if not callsign or lat is None or lon is None:
        return None

    altitude = _altitude_ft(ac.get("alt_baro", ac.get("alt_geom", 0)))
    runtime = _runtime_config()
    min_altitude = getattr(runtime, "MIN_ALTITUDE", MIN_ALTITUDE) if runtime else MIN_ALTITUDE
    max_altitude = getattr(runtime, "MAX_ALTITUDE", MAX_ALTITUDE) if runtime else MAX_ALTITUDE
    if altitude < min_altitude or altitude > max_altitude:
        return None

    if ac.get("alt_baro") == "ground" or bool(ac.get("ground", False)):
        return None

    position_age = ac.get("seen_pos", ac.get("seen"))
    if position_age not in (None, "") and _num(position_age, 9999) > 30:
        return None

    return {
        "icao24": (ac.get("hex") or ac.get("icao") or "").strip(),
        "callsign": callsign,
        "latitude": _num(lat),
        "longitude": _num(lon),
        "altitude": altitude,
        "ground_speed": int(_num(ac.get("gs", ac.get("speed", 0)))),
        "heading": _num(ac.get("track", ac.get("true_heading", 0))),
        "vertical_speed": int(_num(ac.get("baro_rate", ac.get("geom_rate", 0)))),
        "on_ground": False,
        "position_age_seconds": _num(position_age) if position_age not in (None, "") else None,
    }


def _within_zone(state):
    lat = state.get("latitude")
    lon = state.get("longitude")
    zone = _zone()
    return (
        lat is not None
        and lon is not None
        and zone["south"] <= lat <= zone["north"]
        and zone["west"] <= lon <= zone["east"]
    )


def _dedupe(states):
    seen = set()
    result = []
    for state in states:
        key = state.get("icao24") or state.get("callsign")
        if not key or key in seen:
            continue
        seen.add(key)
        result.append(state)
    return result


def _zone_center_radius_nm():
    zone = _zone()
    north = zone["north"]
    south = zone["south"]
    west = zone["west"]
    east = zone["east"]
    center_lat = (north + south) / 2
    center_lon = (west + east) / 2

    # Radius endpoints use nautical miles. Query a generous circle, then
    # apply the exact configured bounding box locally.
    import math
    lat_nm = abs(north - south) * 60
    lon_nm = abs(east - west) * 60 * math.cos(math.radians(center_lat))
    radius_nm = max(60, int(math.ceil(math.hypot(lat_nm, lon_nm) / 2 + 5)))
    return center_lat, center_lon, radius_nm


class PositionClient:
    def __init__(self):
        self._opensky = OpenSkyClient()
        self.last_source = None

    def get_zone_states(self):
        _refresh_runtime_config()
        zone = _zone()
        print(
            "[Position] active box "
            f"N={zone['north']:.6f} S={zone['south']:.6f} "
            f"W={zone['west']:.6f} E={zone['east']:.6f}"
        )
        enabled = _enabled()
        for source in _order():
            if enabled.get(source, True) is False:
                continue

            if source == "OpenSky":
                states = self._from_opensky()
            elif source == "ADSBLol":
                states = self._from_adsblol()
            elif source == "AirplanesLive":
                states = self._from_airplanes_live()
            elif source == "ADSBfi":
                states = self._from_adsbfi()
            else:
                continue

            if states:
                self.last_source = source
                print(f"[Position] {source} {len(states)} aircraft")
                return states

            print(f"[Position] {source} no usable aircraft")

        self.last_source = None
        return []

    def find_callsign(self, callsign):
        return self._opensky.find_callsign(callsign)

    def get_flight_trail(self, icao24):
        return self._opensky.get_flight_trail(icao24)

    def _from_opensky(self):
        return [
            state for state in self._opensky.get_zone_states()
            if _within_zone(state)
        ]

    def _from_airplanes_live(self):
        print("[Position] AirplanesLive skipped: public v2 bbox endpoint unavailable")
        return []

    def _from_adsblol(self):
        lat, lon, radius_nm = _zone_center_radius_nm()
        url = f"https://api.adsb.lol/v2/lat/{lat:.6f}/lon/{lon:.6f}/dist/{radius_nm}"
        return self._from_adsb_style_url("ADSBLol", url)

    def _from_adsbfi(self):
        lat, lon, radius_nm = _zone_center_radius_nm()
        url = f"https://opendata.adsb.fi/api/v2/lat/{lat:.6f}/lon/{lon:.6f}/dist/{radius_nm}"
        return self._from_adsb_style_url("ADSBfi", url)

    def _from_adsb_style_url(self, source, url):
        try:
            resp = requests.get(
                url,
                params={"_": int(time.time())},
                headers={"Cache-Control": "no-cache", "Pragma": "no-cache"},
                timeout=5,
            )
            resp.raise_for_status()
            data = resp.json()
        except Exception as e:
            print(f"[Position] {source} failed: {e}")
            return []

        states = []
        aircraft = data.get("ac") or data.get("aircraft") or []
        for ac in aircraft:
            state = _parse_adsb_aircraft(ac)
            if state and _within_zone(state):
                states.append(state)
        return _dedupe(states)
