#!/usr/bin/env python3 -u
"""
ADSB Background Logger v3
Runs 24/7, polls aircraft.json every 30 seconds,
writes daily log files, auto-deletes oldest when over 4GB.
Enriches new aircraft with FAA registry data when available.
Also logs KSEA runway config and METAR every 10 minutes.
"""

import json
import os
import sys
import time
import threading
import urllib.request
from datetime import datetime, date

# Force unbuffered output so systemd journal captures all log lines
sys.stdout.reconfigure(line_buffering=True)

AIRCRAFT_FILE = '/run/readsb/aircraft.json'
DATA_DIR = '/home/pi/adsb-analyzer/data'
FAA_LOOKUP = '/home/pi/adsb-analyzer/faa/hex_lookup.json'
POLL_INTERVAL = 30
MAX_STORAGE_BYTES = 4 * 1024 * 1024 * 1024  # 4GB
VISIT_GAP_SECONDS = 900  # 15 minutes

# KSEA runway config logging
KSEA_LOG = '/home/pi/adsb-analyzer/data/ksea_config.json'
KSEA_INTERVAL = 600  # 10 minutes
KSEA_LAT = 47.4502
KSEA_LON = -122.3088

# KSEA Class B floor rings (radius NMI, floor altitude ft)
# Inner ring: 0-5 NMI, floor 0ft (SFC)
# Middle ring: 5-10 NMI, floor 1500ft
# Outer ring: 10-20 NMI, floor 3000ft
KSEA_CLASS_B = [
    (5,   0,    10000),   # (max_nmi, floor_ft, ceiling_ft)
    (10,  1500, 10000),
    (20,  3000, 10000),
]

# Approach track corridors (center heading, tolerance degrees)
RWY_34_TRACK = (340, 20)   # 34L/34C/34R finals
RWY_16_TRACK = (160, 20)   # 16L/16C/16R finals

# Approach speed range (kts ground speed)
APPROACH_SPD_MIN = 100
APPROACH_SPD_MAX = 200

# Descent rate range (fpm) — negative = descending
DESCENT_RATE_MIN = -3000
DESCENT_RATE_MAX = -200

os.makedirs(DATA_DIR, exist_ok=True)

last_seen = {}
faa_lookup = {}
faa_loaded_time = 0

def load_faa_lookup():
    global faa_lookup, faa_loaded_time
    if not os.path.exists(FAA_LOOKUP):
        return
    mtime = os.path.getmtime(FAA_LOOKUP)
    if mtime > faa_loaded_time:
        try:
            with open(FAA_LOOKUP, 'r') as f:
                faa_lookup = json.load(f)
            faa_loaded_time = mtime
            print(f'[logger] FAA lookup loaded: {len(faa_lookup):,} aircraft')
        except Exception as e:
            print(f'[logger] FAA lookup load error: {e}')

def enrich(hex_id, rec):
    if hex_id in faa_lookup:
        info = faa_lookup[hex_id]
        rec['n_number'] = info.get('n_number', '')
        rec['make'] = info.get('make', '')
        rec['model'] = info.get('model', '')
        rec['owner'] = info.get('owner', '')
    return rec

def today_file():
    return os.path.join(DATA_DIR, date.today().isoformat() + '.json')

def load_today():
    f = today_file()
    if os.path.exists(f):
        try:
            with open(f, 'r') as fh:
                return json.load(fh)
        except:
            pass
    return {}

def save_today(data):
    f = today_file()
    tmp = f + '.tmp'
    with open(tmp, 'w') as fh:
        json.dump(data, fh)
    os.replace(tmp, f)

def get_data_size():
    total = 0
    for fn in os.listdir(DATA_DIR):
        fp = os.path.join(DATA_DIR, fn)
        if os.path.isfile(fp):
            total += os.path.getsize(fp)
    return total

def enforce_storage_limit():
    while get_data_size() > MAX_STORAGE_BYTES:
        files = sorted([f for f in os.listdir(DATA_DIR) if f.endswith('.json')])
        if not files:
            break
        oldest = files[0]
        if oldest == date.today().isoformat() + '.json':
            break
        os.remove(os.path.join(DATA_DIR, oldest))
        print(f'[logger] Deleted old log: {oldest}')

def poll():
    now = time.time()
    now_iso = datetime.utcnow().isoformat() + 'Z'

    try:
        with open(AIRCRAFT_FILE, 'r') as f:
            data = json.load(f)
    except Exception as e:
        print(f'[logger] Could not read aircraft.json: {e}')
        return

    today = load_today()

    for ac in data.get('aircraft', []):
        hex_id = ac.get('hex')
        flight = (ac.get('flight') or '').strip()
        if not hex_id or not flight:
            continue

        entry = {
            'flight':    flight,
            'hex':       hex_id,
            'category':  ac.get('category', '?'),
            'alt_baro':  ac.get('alt_baro'),
            'gs':        ac.get('gs'),
            'track':     ac.get('track'),
            'r_dst':     ac.get('r_dst'),
            'r_dir':     ac.get('r_dir'),
            'baro_rate': ac.get('baro_rate'),
            'rssi':      ac.get('rssi'),
            'squawk':    ac.get('squawk'),
            'emergency': ac.get('emergency'),
        }
        entry = {k: v for k, v in entry.items() if v is not None}

        prev = last_seen.get(hex_id)
        is_new = hex_id not in today
        is_return = prev and (now - prev) > VISIT_GAP_SECONDS

        if is_new:
            entry = enrich(hex_id, entry)
            today[hex_id] = {
                **entry,
                'first_seen': now_iso,
                'last_seen':  now_iso,
                'visits':     1,
                'total_sightings': 1,
            }
        else:
            today[hex_id]['last_seen'] = now_iso
            today[hex_id]['total_sightings'] = today[hex_id].get('total_sightings', 0) + 1
            for k, v in entry.items():
                today[hex_id][k] = v
            # Add FAA data if not already present
            if 'make' not in today[hex_id]:
                today[hex_id] = enrich(hex_id, today[hex_id])
            if is_return:
                today[hex_id]['visits'] = today[hex_id].get('visits', 1) + 1

        last_seen[hex_id] = now

    save_today(today)
    enforce_storage_limit()

# ── KSEA RUNWAY CONFIG LOGGING ────────────────────────────────────

def haversine_nmi(lat1, lon1, lat2, lon2):
    """Distance in NMI between two lat/lon points."""
    import math
    R = 3440.065  # Earth radius in NMI
    p = math.pi / 180
    a = (math.sin((lat2-lat1)*p/2)**2 +
         math.cos(lat1*p) * math.cos(lat2*p) *
         math.sin((lon2-lon1)*p/2)**2)
    return 2 * R * math.asin(math.sqrt(a))

def fetch_metar():
    """Fetch current KSEA METAR from aviationweather.gov."""
    url = 'https://aviationweather.gov/api/data/metar?ids=KSEA&format=json'
    try:
        req = urllib.request.Request(url, headers={'User-Agent': 'adsb-analyzer/1.0'})
        with urllib.request.urlopen(req, timeout=10) as r:
            data = json.loads(r.read())
        if not data:
            return None
        m = data[0]
        return {
            'raw':        m.get('rawOb', ''),
            'wind_dir':   m.get('wdir'),
            'wind_spd':   m.get('wspd'),
            'wind_gust':  m.get('wgst'),
            'visibility': m.get('visib'),
            'ceiling':    m.get('ceiling'),
            'altimeter':  m.get('altim'),
            'temp':       m.get('temp'),
            'dewpoint':   m.get('dewp'),
            'wx':         m.get('wxString', ''),
        }
    except Exception as e:
        print(f'[ksea] METAR fetch error: {e}')
        return None

def in_class_b(dist_nmi, alt):
    """Return True if position is inside KSEA Class B at this altitude."""
    for max_nmi, floor_ft, ceiling_ft in KSEA_CLASS_B:
        if dist_nmi <= max_nmi:
            return floor_ft <= alt <= ceiling_ft
    return False

def track_matches(track, center, tolerance):
    """Return True if track is within tolerance degrees of center heading."""
    diff = abs((track - center + 180) % 360 - 180)
    return diff <= tolerance

def detect_ksea_config():
    """
    Sample aircraft inside KSEA Class B airspace that are:
    - Descending (vertical rate -200 to -3000 fpm)
    - On approach track (340°±20° or 160°±20°)
    - Ground speed consistent with approach (100-200kt)
    Returns (config, arrivals_34, arrivals_16, total_in_classb)
    """
    try:
        with open(AIRCRAFT_FILE, 'r') as f:
            data = json.load(f)
    except:
        return None, 0, 0, 0

    arr_34 = 0
    arr_16 = 0
    total_classb = 0

    for ac in data.get('aircraft', []):
        lat = ac.get('lat')
        lon = ac.get('lon')
        alt = ac.get('alt_baro')
        track = ac.get('track')
        gs = ac.get('gs')           # ground speed in kts
        vrate = ac.get('baro_rate') # vertical rate in fpm

        if lat is None or lon is None or alt is None:
            continue
        if isinstance(alt, str):
            continue

        dist = haversine_nmi(KSEA_LAT, KSEA_LON, lat, lon)

        if not in_class_b(dist, alt):
            continue

        total_classb += 1

        # Must have track, speed, and descent rate to qualify as arrival
        if track is None or gs is None or vrate is None:
            continue

        # Speed filter — approach speed range
        if not (APPROACH_SPD_MIN <= gs <= APPROACH_SPD_MAX):
            continue

        # Descent filter — must be descending
        if not (DESCENT_RATE_MIN <= vrate <= DESCENT_RATE_MAX):
            continue

        # Track filter — must match one of the two runway orientations
        if track_matches(track, RWY_34_TRACK[0], RWY_34_TRACK[1]):
            arr_34 += 1
        elif track_matches(track, RWY_16_TRACK[0], RWY_16_TRACK[1]):
            arr_16 += 1

    total_arrivals = arr_34 + arr_16
    if total_arrivals < 2:
        return None, arr_34, arr_16, total_classb

    if arr_34 > arr_16:
        return '34s', arr_34, arr_16, total_classb
    elif arr_16 > arr_34:
        return '16s', arr_34, arr_16, total_classb
    return None, arr_34, arr_16, total_classb

def load_ksea_log():
    if os.path.exists(KSEA_LOG):
        try:
            with open(KSEA_LOG, 'r') as f:
                return json.load(f)
        except:
            pass
    return []

def save_ksea_log(entries):
    tmp = KSEA_LOG + '.tmp'
    with open(tmp, 'w') as f:
        json.dump(entries, f, indent=2)
    os.replace(tmp, KSEA_LOG)

def ksea_loop():
    """Runs every 10 minutes in a background thread."""
    print('[ksea] Runway config logger started')
    last_confirmed_config = None
    pending_config = None  # candidate config waiting for confirmation

    while True:
        try:
            metar = fetch_metar()
            config, arr_34, arr_16, total_classb = detect_ksea_config()

            wind_dir = metar.get('wind_dir') if metar else None
            wind_spd = metar.get('wind_spd') if metar else None

            # If we can't detect from traffic, infer from wind
            if config is None and wind_dir is not None:
                if 150 <= wind_dir <= 210 and (wind_spd or 0) >= 5:
                    config = '16s'
                elif (wind_dir >= 300 or wind_dir <= 60) and (wind_spd or 0) >= 5:
                    config = '34s'

            # Two-sample confirmation
            config_changed = False
            if config is not None:
                if config != last_confirmed_config:
                    if pending_config == config:
                        config_changed = (last_confirmed_config is not None)
                        last_confirmed_config = config
                        pending_config = None
                    else:
                        pending_config = config
                else:
                    pending_config = None

            entry = {
                'timestamp':        datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ'),
                'config':           last_confirmed_config,
                'config_changed':   config_changed,
                'arr_34':           arr_34,
                'arr_16':           arr_16,
                'total_classb':     total_classb,
                'wind_dir':         wind_dir,
                'wind_spd':         wind_spd,
                'wind_gust':        metar.get('wind_gust') if metar else None,
                'visibility':       metar.get('visibility') if metar else None,
                'ceiling':          metar.get('ceiling') if metar else None,
                'altimeter':        metar.get('altimeter') if metar else None,
                'temp':             metar.get('temp') if metar else None,
                'dewpoint':         metar.get('dewpoint') if metar else None,
                'wx':               metar.get('wx') if metar else None,
                'metar_raw':        metar.get('raw') if metar else None,
            }
            entry = {k: v for k, v in entry.items() if v is not None}

            log = load_ksea_log()
            log.append(entry)
            save_ksea_log(log)

            if config_changed:
                print(f'[ksea] CONFIRMED flip → {last_confirmed_config} | wind {wind_dir}°@{wind_spd}kt')
            elif pending_config:
                print(f'[ksea] Pending: {pending_config} (awaiting confirmation)')

            print(f'[ksea] {entry["timestamp"]} config={last_confirmed_config} 34s={arr_34} 16s={arr_16} classb={total_classb} wind={wind_dir}@{wind_spd}')

        except Exception as e:
            print(f'[ksea] Loop error: {e}')

        time.sleep(KSEA_INTERVAL)


def main():
    print(f'[logger] ADSB Logger v3 started. Poll interval: {POLL_INTERVAL}s')
    load_faa_lookup()

    # Start KSEA runway config thread
    ksea_thread = threading.Thread(target=ksea_loop, daemon=True)
    ksea_thread.start()

    poll_count = 0
    while True:
        try:
            poll()
            poll_count += 1
            # Reload FAA lookup every hour in case it was updated
            if poll_count % 120 == 0:
                load_faa_lookup()
        except Exception as e:
            print(f'[logger] Poll error: {e}')
        time.sleep(POLL_INTERVAL)

if __name__ == '__main__':
    main()
