import json
import os
from datetime import datetime

BASE_DIR      = os.path.dirname(os.path.dirname(__file__))
# Module-level cache so it persists across grab cycles
_route_cache = {}  # callsign -> result dict
CACHE_TTL = 3600   # 1 hour

# Import each client — they self-disable if no key is configured
from utilities.airlabs     import get_flight_details as _airlabs_details, get_tracked_flight as _airlabs_tracked, is_available as _airlabs_ok
from utilities.flightaware import get_flight_details as _fa_details,      get_tracked_flight as _fa_tracked,      is_available as _fa_ok
from utilities.fr24_unofficial import is_available as _fr24u_ok
from utilities import route_db

# FR24 is optional — only imported if credentials exist
try:
    from config import OPENSKY_CLIENT_ID, OPENSKY_CLIENT_SECRET
    _has_opensky = bool(OPENSKY_CLIENT_ID and OPENSKY_CLIENT_SECRET)
except Exception:
    _has_opensky = False

try:
    from utilities.flightradar import FR24Client as _FR24Client
    _fr24_client = _FR24Client()
    _has_fr24 = True
except Exception:
    _fr24_client = None
    _has_fr24 = False

try:
    from utilities.fr24_client import FR24Client as _FR24UnofficialClient
    _fr24u_client = _FR24UnofficialClient() if _fr24u_ok() else None
except Exception as e:
    print(f"[FR24_API] unavailable: {e}")
    _fr24u_client = None


USAGE_FILE = os.path.join(BASE_DIR, "api_usage.json")
API_LOG    = os.path.join(BASE_DIR, "api_calls.log")


def _source_label(source):
    labels = {
        "RouteDB(exact)": "DB_EXACT",
        "RouteDB(schedule)": "DB_SCHEDULE",
        "RouteDB(recent)": "DB_RECENT",
        "FR24Unofficial": "API_FR24_UNOFF",
        "FR24Unofficial(tracked)": "API_FR24_UNOFF",
        "FR24": "API_FR24_PAID",
        "FR24(tracked)": "API_FR24_PAID",
        "AirLabs": "API_AIRLABS",
        "FlightAware": "API_FLIGHTAWARE",
        "NONE": "NO_ROUTE",
    }
    return labels.get(source, source)


def _load_usage():
    try:
        with open(USAGE_FILE, "r") as f:
            data = json.load(f)
        # Reset if month changed
        if data.get("month") != datetime.now().strftime("%Y-%m"):
            return {"month": datetime.now().strftime("%Y-%m"), "AirLabs": 0, "FlightAware": 0.0, "FR24": 0}
        return data
    except (FileNotFoundError, json.JSONDecodeError):
        return {"month": datetime.now().strftime("%Y-%m"), "AirLabs": 0, "FlightAware": 0.0, "FR24": 0}


def _save_usage(data):
    try:
        with open(USAGE_FILE, "w") as f:
            json.dump(data, f)
    except Exception:
        pass


def _log_usage(source, callsign, origin, dest, detail=""):
    """Increment usage counter, overwrite usage file, and append to call log."""
    label = _source_label(source)
    data = _load_usage()
    if "AirLabs" in source:
        data["AirLabs"] = data.get("AirLabs", 0) + 1
    elif "FlightAware" in source:
        data["FlightAware"] = round(data.get("FlightAware", 0.0) + 0.005, 3)
    elif "FR24" in source:
        data["FR24"] = data.get("FR24", 0) + 1
    _save_usage(data)

    # Append to call log — local time so it matches your timezone
    try:
        ts      = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        route   = f"{origin or '?'}→{dest or '?'}"
        al      = data.get("AirLabs", 0)
        fa      = data.get("FlightAware", 0.0)
        fr      = data.get("FR24", 0)
        suffix = f"  {detail}" if detail else ""
        line = (
            f"{ts}  {label:20s}  {callsign:10s}  {route:12s}"
            f"  [AL:{al} FA:${fa:.3f} FR:{fr}]{suffix}\n"
        )
        with open(API_LOG, "a", encoding="utf-8") as f:
            f.write(line)
    except Exception:
        pass


def _is_plausible(result, plane_lat, plane_lon):
    """
    Check that the plane's current position lies roughly between
    origin and destination. Rejects codeshare mismatches where the
    same flight number is used on a completely different route.
    Returns True if plausible or if coords are missing (can't check).
    """
    import math as _m

    def _coord(value):
        if value in ("", None):
            return None
        try:
            return float(value)
        except (TypeError, ValueError):
            return None

    o_lat = _coord(result.get("origin_latitude") or result.get("origin_lat"))
    o_lon = _coord(result.get("origin_longitude") or result.get("origin_lon"))
    d_lat = _coord(result.get("destination_latitude") or result.get("dest_lat"))
    d_lon = _coord(result.get("destination_longitude") or result.get("dest_lon"))
    plane_lat = _coord(plane_lat)
    plane_lon = _coord(plane_lon)

    if any(v is None for v in (o_lat, o_lon, d_lat, d_lon, plane_lat, plane_lon)):
        return True  # can't check, let it through

    def _nm(la1, lo1, la2, lo2):
        la1, lo1, la2, lo2 = map(_m.radians, (la1, lo1, la2, lo2))
        a = _m.sin((la2-la1)/2)**2 + _m.cos(la1)*_m.cos(la2)*_m.sin((lo2-lo1)/2)**2
        return 3440.07 * 2 * _m.atan2(_m.sqrt(a), _m.sqrt(1-a))

    total = _nm(o_lat, o_lon, d_lat, d_lon)
    to_o  = _nm(plane_lat, plane_lon, o_lat, o_lon)
    to_d  = _nm(plane_lat, plane_lon, d_lat, d_lon)

    return (to_o + to_d) <= total * 1.25


def _is_current_flight_instance(result, now=None):
    """Reject a cached route belonging to an old reuse of the callsign."""
    if not result:
        return False
    if now is None:
        import time
        now = time.time()

    def _timestamp(name):
        value = result.get(name)
        try:
            return float(value) if value not in (None, "") else None
        except (TypeError, ValueError):
            return None

    departure = (
        _timestamp("time_real_departure")
        or _timestamp("time_scheduled_departure")
    )
    arrival = (
        _timestamp("time_estimated_arrival")
        or _timestamp("time_scheduled_arrival")
    )

    if departure is None and arrival is None:
        return False
    if arrival is not None and now > arrival + (6 * 3600):
        return False
    if departure is not None and departure > now + (36 * 3600):
        return False
    if departure is not None and arrival is None and now > departure + (24 * 3600):
        return False
    return True


def _historical_route_for_now(result, plane_lat, plane_lon, now=None):
    """Shift a recurring cached schedule to today and validate route progress."""
    import math as _m
    from datetime import datetime, timedelta, timezone

    if now is None:
        import time
        now = time.time()

    def _coord(value):
        try:
            return float(value) if value not in (None, "") else None
        except (TypeError, ValueError):
            return None

    def _stamp(name):
        try:
            value = result.get(name)
            return float(value) if value not in (None, "") else None
        except (TypeError, ValueError):
            return None

    def _first(*names):
        for name in names:
            value = result.get(name)
            if value not in (None, ""):
                return value
        return None

    origin_lat = _coord(_first("origin_latitude", "origin_lat"))
    origin_lon = _coord(_first("origin_longitude", "origin_lon"))
    dest_lat = _coord(_first("destination_latitude", "dest_lat"))
    dest_lon = _coord(_first("destination_longitude", "dest_lon"))
    plane_lat = _coord(plane_lat)
    plane_lon = _coord(plane_lon)
    departure = _stamp("time_real_departure") or _stamp("time_scheduled_departure")
    arrival = _stamp("time_estimated_arrival") or _stamp("time_scheduled_arrival")

    if any(value is None for value in (
        origin_lat, origin_lon, dest_lat, dest_lon,
        plane_lat, plane_lon, departure, arrival,
    )):
        return None

    duration = arrival - departure
    if duration <= 0:
        duration += 86400
    if duration < 20 * 60 or duration > 20 * 3600:
        return None

    old_departure = datetime.fromtimestamp(departure, timezone.utc)
    now_dt = datetime.fromtimestamp(now, timezone.utc)
    candidates = []
    for day_offset in (-1, 0, 1):
        day = now_dt.date() + timedelta(days=day_offset)
        projected = datetime(
            day.year, day.month, day.day,
            old_departure.hour, old_departure.minute, old_departure.second,
            tzinfo=timezone.utc,
        ).timestamp()
        candidates.append(projected)

    projected_departure = min(
        candidates,
        key=lambda value: abs(now - (value + duration / 2)),
    )
    projected_arrival = projected_departure + duration
    if now < projected_departure - 2 * 3600 or now > projected_arrival + 2 * 3600:
        return None

    def _nm(la1, lo1, la2, lo2):
        la1, lo1, la2, lo2 = map(_m.radians, (la1, lo1, la2, lo2))
        value = (
            _m.sin((la2-la1)/2) ** 2
            + _m.cos(la1) * _m.cos(la2) * _m.sin((lo2-lo1)/2) ** 2
        )
        value = min(1.0, max(0.0, value))
        return 3440.07 * 2 * _m.atan2(_m.sqrt(value), _m.sqrt(1-value))

    to_origin = _nm(plane_lat, plane_lon, origin_lat, origin_lon)
    to_destination = _nm(plane_lat, plane_lon, dest_lat, dest_lon)
    actual_progress = (
        to_origin / (to_origin + to_destination)
        if to_origin + to_destination > 0 else 0
    )
    expected_progress = min(
        1.0,
        max(0.0, (now - projected_departure) / duration),
    )
    if abs(actual_progress - expected_progress) > 0.20:
        return None

    promoted = dict(result)
    shift = projected_departure - departure
    for field in (
        "time_scheduled_departure",
        "time_scheduled_arrival",
        "time_real_departure",
        "time_estimated_arrival",
    ):
        value = _stamp(field)
        promoted[field] = value + shift if value is not None else None
    promoted["trail"] = []
    return {
        "data": promoted,
        "progress_error": abs(actual_progress - expected_progress),
        "expected_progress": expected_progress,
        "actual_progress": actual_progress,
    }


def _normalise(result, callsign, plane_lat, plane_lon, registration=""):
    """
    Convert any source's result into the standard entry dict
    that overhead.py / display scenes expect.
    """
    if not result:
        return None

    # Support both naming conventions
    origin_lat = result.get("origin_latitude") or result.get("origin_lat")
    origin_lon = result.get("origin_longitude") or result.get("origin_lon")
    dest_lat   = result.get("destination_latitude") or result.get("dest_lat")
    dest_lon   = result.get("destination_longitude") or result.get("dest_lon")

    # owner_icao = operating carrier ICAO for logo lookup
    owner_icao = result.get("airline_icao", "")
    if not owner_icao and len(callsign) >= 3 and callsign[:3].isalpha():
        owner_icao = callsign[:3]
    owner_icao = owner_icao or ""

    return {
        "airline":               result.get("airline_name", ""),
        "plane":                 result.get("plane_type", "") or result.get("aircraft_type", ""),
        "origin":                result.get("origin_iata", "?") or "?",
        "origin_latitude":       origin_lat,
        "origin_longitude":      origin_lon,
        "destination":           result.get("dest_iata", "?") or "?",
        "destination_latitude":  dest_lat,
        "destination_longitude": dest_lon,
        "owner_iata":            result.get("airline_iata", "N/A") or "N/A",
        "owner_icao":            owner_icao,
        "time_scheduled_departure": result.get("time_scheduled_departure"),
        "time_scheduled_arrival":   result.get("time_scheduled_arrival"),
        "time_real_departure":      result.get("time_real_departure"),
        "time_estimated_arrival":   result.get("time_estimated_arrival"),
        "trail":                 result.get("trail", []),
    }


class RouteClient:
    """
    Unified route lookup client.
    Tries available APIs in config-defined order.
    """

    def __init__(self):
        try:
            from config import API_SOURCE_ORDER as _order, API_SOURCE_ENABLED as _enabled
        except ImportError:
            _order   = ["AirLabs", "FlightAware", "FR24Unofficial", "FR24"]
            _enabled = {}
            
        active = [s for s in _order if _enabled.get(s, True)]
        if active:
            print(f"[RouteClient] Active sources: {' -> '.join(_source_label(source) for source in active)}")
        else:
            print("[RouteClient] WARNING: No API keys configured — flights will show callsign only")

    @property
    def ok(self):
        return True

    def get_flight_details(self, callsign, plane_lat, plane_lon,
                           plane_type="", registration="", distance=0.0):
        """Try each source in configuration order, return first successful result."""
        from time import time
        import re as _re

        # Check module-level cache first
        cached = _route_cache.get(callsign)
        if cached is not None and (time() - cached["ts"]) < CACHE_TTL:
            cached_data = cached["data"]
            if (
                not cached_data
                or (
                    _is_current_flight_instance(cached_data)
                    and _is_plausible(cached_data, plane_lat, plane_lon)
                )
            ):
                return cached_data  # may be {} for a cached miss
            print(f"[RouteCache] discarded old or mismatched flight instance for {callsign}")
            _route_cache.pop(callsign, None)

        def _cache_and_return(data):
            _route_cache[callsign] = {"data": data, "ts": time()}
            return data

        # Check callsign structure eligibility (matches private implementation logic)
        _is_icao = bool(_re.match(r"^[A-Z]{3}[A-Z0-9]+$", callsign))
        if not _is_icao:
            # Fallback early to default values if it's a tail registration number (e.g. N487CB)
            return _cache_and_return({
                "airline": "Private", "plane": plane_type,
                "origin": "?", "origin_latitude": None, "origin_longitude": None,
                "destination": "?", "destination_latitude": None, "destination_longitude": None,
                "owner_iata": "N/A", "owner_icao": callsign[:3] if len(callsign) >= 3 else "",
                "time_scheduled_departure": None, "time_scheduled_arrival": None,
                "time_real_departure": None, "time_estimated_arrival": None,
                "distance_origin": 0, "distance_destination": 0, "trail": [],
            })

        db_records = route_db.get_routes(callsign)
        db_candidates = []
        plausible_count = 0
        for db_record in db_records:
            db_cached = db_record["data"]
            current_instance = _is_current_flight_instance(db_cached)
            plausible = _is_plausible(db_cached, plane_lat, plane_lon)
            plausible_count += int(plausible)
            historical_match = (
                _historical_route_for_now(
                    db_cached,
                    plane_lat,
                    plane_lon,
                )
                if plausible and not current_instance
                else None
            )

            if plausible and current_instance:
                db_candidates.append({
                    "score": 1000,
                    "match_type": "exact",
                    "source_label": "RouteDB(exact)",
                    "record": db_record,
                    "data": db_cached,
                })
                continue

            if plausible and historical_match:
                db_candidates.append({
                    "score": 800 - int(historical_match["progress_error"] * 500),
                    "match_type": "schedule",
                    "source_label": "RouteDB(schedule)",
                    "record": db_record,
                    "data": historical_match["data"],
                    "progress_error": historical_match["progress_error"],
                })
                continue

            if plausible:
                try:
                    route_day = datetime.strptime(
                        db_record["flight_date"],
                        "%Y-%m-%d",
                    ).date()
                    age_days = (datetime.utcnow().date() - route_day).days
                except (TypeError, ValueError):
                    age_days = 9999
                if 0 <= age_days <= 7:
                    recent_data = dict(db_cached)
                    for field in (
                        "time_scheduled_departure",
                        "time_scheduled_arrival",
                        "time_real_departure",
                        "time_estimated_arrival",
                    ):
                        recent_data[field] = None
                    recent_data["trail"] = []
                    db_candidates.append({
                        "score": 400 - age_days,
                        "match_type": "recent",
                        "source_label": "RouteDB(recent)",
                        "record": db_record,
                        "data": recent_data,
                    })

        if db_candidates:
            selected = max(
                db_candidates,
                key=lambda candidate: (
                    candidate["score"],
                    candidate["record"]["flight_date"],
                ),
            )
            record = selected["record"]
            selected_data = selected["data"]
            original_source = record.get("source") or "unknown"
            decision = selected["match_type"]
            print(
                f"[RouteResolve] {callsign} decision=DB_{decision.upper()} "
                f"route={selected_data.get('origin', '?')}-"
                f"{selected_data.get('destination', '?')} "
                f"cache_date={record['flight_date']} "
                f"original_source={original_source} "
                f"candidates={len(db_records)}"
            )
            _log_usage(
                selected["source_label"],
                callsign,
                selected_data.get("origin"),
                selected_data.get("destination"),
                (
                    f"cache_date={record['flight_date']} "
                    f"original_source={original_source}"
                ),
            )
            if decision in ("schedule", "recent"):
                route_db.save_route(
                    callsign,
                    selected_data,
                    original_source,
                    created_by=f"{decision}_match",
                    parent_flight_date=record["flight_date"],
                )
            route_db.record_hit(
                callsign,
                record["flight_date"],
                decision,
            )
            return _cache_and_return(selected_data)

        if db_records:
            print(
                f"[RouteResolve] {callsign} decision=DB_MISS "
                f"candidates={len(db_records)} plausible={plausible_count} "
                "reason=no-safe-match; trying configured APIs"
            )
        else:
            print(
                f"[RouteResolve] {callsign} decision=DB_MISS "
                "candidates=0; trying configured APIs"
            )

        # Load API execution configs dynamically
        try:
            from config import API_SOURCE_ORDER as _order, API_SOURCE_ENABLED as _enabled
        except ImportError:
            _order   = ["AirLabs", "FlightAware", "FR24Unofficial", "FR24"]
            _enabled = {}

        # Dictionary routing setup mapping config strings to actual functional methods and checks
        _SOURCE_FNS = {
            "AirLabs":     (lambda cs: _airlabs_details(cs), _airlabs_ok),
            "FlightAware": (lambda cs: _fa_details(cs),      _fa_ok),
            "FR24Unofficial": (lambda cs: _fr24u_client.get_flight_details(cs, plane_lat, plane_lon, plane_type, registration, distance)
                               if _fr24u_client else None, lambda: bool(_fr24u_client)),
            "FR24":        (lambda cs: _fr24_client.get_flight_details(cs, plane_lat, plane_lon, plane_type, registration, distance) 
                            if _fr24_client else None, lambda: bool(_has_fr24 and _fr24_client))
        }

        # Dynamically loop through order configuration
        for source in _order:
            if not _enabled.get(source, True):
                continue
            if source not in _SOURCE_FNS:
                continue
                
            fn, check = _SOURCE_FNS[source]
            if not check():
                continue

            result = fn(callsign)
            _log_usage(
                source,
                callsign,
                result.get("origin_iata") if result else None,
                result.get("dest_iata") if result else None,
                "database_checked_first=true",
            )
            
            if result and result.get("origin_iata") not in ("?", "", None):
                if not _is_plausible(result, plane_lat, plane_lon):
                    print(f"[RouteClient] {_source_label(source)} {result.get('origin_iata')}-{result.get('dest_iata')} "
                          f"rejected for {callsign} — trying next source")
                else:
                    normalised = _normalise(result, callsign, plane_lat, plane_lon, registration)
                    if normalised:
                        route_db.save_route(
                            callsign,
                            normalised,
                            source,
                            created_by="api",
                        )
                        print(
                            f"[RouteResolve] {callsign} decision=API "
                            f"provider={source} "
                            f"route={normalised.get('origin', '?')}-"
                            f"{normalised.get('destination', '?')} "
                            f"saved_date={route_db.flight_date(normalised)}"
                        )
                        return _cache_and_return(normalised)

        # All configuration steps exhausted failed — cache the miss 
        _route_cache[callsign] = {"data": {}, "ts": time()}
        _log_usage("NONE", callsign, None, None)
        print(
            f"[RouteResolve] {callsign} decision=NO_ROUTE "
            "database_and_apis_exhausted=true"
        )
        return {
            "airline": "Private", "plane": plane_type,
            "origin": "?", "origin_latitude": None, "origin_longitude": None,
            "destination": "?", "destination_latitude": None, "destination_longitude": None,
            "owner_iata": "N/A", "owner_icao": callsign[:3] if len(callsign) >= 3 else "",
            "time_scheduled_departure": None, "time_scheduled_arrival": None,
            "time_real_departure": None, "time_estimated_arrival": None,
            "distance_origin": 0, "distance_destination": 0, "trail": [],
        }

    def get_tracked_flight(self, callsign):
        """Search for a tracked flight globally across available sources."""
        try:
            from config import API_SOURCE_ORDER as _order, API_SOURCE_ENABLED as _enabled
        except ImportError:
            _order   = ["AirLabs", "FlightAware", "FR24Unofficial", "FR24"]
            _enabled = {}

        _TRACK_FNS = {
            "AirLabs":     (_airlabs_tracked, _airlabs_ok),
            "FlightAware": (_fa_tracked,      _fa_ok),
            "FR24Unofficial": (lambda cs: _fr24u_client.get_tracked_flight(cs) if _fr24u_client else None,
                               lambda: bool(_fr24u_client)),
            "FR24":        (lambda cs: _fr24_client.get_tracked_flight(cs) if _fr24_client else None, 
                            lambda: bool(_has_fr24 and _fr24_client))
        }

        for source in _order:
            if not _enabled.get(source, True):
                continue
            if source not in _TRACK_FNS:
                continue

            fn, check = _TRACK_FNS[source]
            if not check():
                continue

            result = fn(callsign)
            if result:
                # Check signature compatibility differences between APIs for live validation
                if source in ("FR24", "FR24Unofficial") or result.get("is_live"):
                    _log_usage(f"{source}(tracked)", callsign, result.get("origin"), result.get("destination"))
                    return result

        return None
