import requests

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 _enabled():
    return _POSITION_CFG.get("enabled", {})


def _order():
    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)))
    if altitude < MIN_ALTITUDE or altitude > MAX_ALTITUDE:
        return None

    if ac.get("alt_baro") == "ground" or bool(ac.get("ground", False)):
        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,
    }


def _within_zone(state):
    lat = state.get("latitude")
    lon = state.get("longitude")
    return (
        lat is not None
        and lon is not None
        and ZONE_HOME["br_y"] <= lat <= ZONE_HOME["tl_y"]
        and ZONE_HOME["tl_x"] <= lon <= ZONE_HOME["br_x"]
    )


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():
    north = _num(ZONE_HOME.get("tl_y"))
    south = _num(ZONE_HOME.get("br_y"))
    west = _num(ZONE_HOME.get("tl_x"))
    east = _num(ZONE_HOME.get("br_x"))
    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):
        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 self._opensky.get_zone_states()

    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, timeout=15)
            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)
