#!/usr/bin/python3
from flask import Flask, render_template, jsonify, send_from_directory, request, Response, redirect
import json
import os
import sys
import shutil
import subprocess
import threading
from datetime import datetime, timezone

# Suppress Flask request logging
import logging
logging.getLogger('werkzeug').setLevel(logging.ERROR)

# Ensure project root is in path for utilities imports
_WEB_DIR  = os.path.dirname(__file__)
_BASE_DIR = os.path.abspath(os.path.join(_WEB_DIR, ".."))
if _BASE_DIR not in sys.path:
    sys.path.insert(0, _BASE_DIR)

try:
    from config import MASTER_TRACKER as _MASTER_TRACKER
except Exception:
    _MASTER_TRACKER = ""

WEB_DIR  = _WEB_DIR
BASE_DIR = _BASE_DIR
app = Flask(
    __name__,
    template_folder=os.path.join(WEB_DIR, "templates"),
    static_folder=os.path.join(WEB_DIR, "static")
)

CLOSEST_FILE  = os.path.join(BASE_DIR, "close.txt")
FARTHEST_FILE = os.path.join(BASE_DIR, "farthest.txt")
TRACKED_FILE  = os.path.join(BASE_DIR, "tracked_flight.json")
DISTANCE_RECORD_HOME_FILE = os.path.join(BASE_DIR, ".cache", "distance_records_home.json")


def load_json(path, default):
    try:
        with open(path, "r", encoding="utf-8") as f:
            return json.load(f)
    except Exception as e:
        print(f"Could not load {path}: {e}")
        return default


def _atomic_write_json(path, data, keep_backup=False):
    os.makedirs(os.path.dirname(path), exist_ok=True)
    tmp_path = path + ".tmp"
    backup_path = path + ".bak"
    with open(tmp_path, "w", encoding="utf-8") as f:
        json.dump(data, f, indent=2)
        f.flush()
        os.fsync(f.fileno())
    os.replace(tmp_path, path)
    if keep_backup:
        try:
            shutil.copy2(path, backup_path)
        except OSError as e:
            print(f"Could not back up {path}: {e}")


def _merge_config(existing, updates):
    merged = dict(existing) if isinstance(existing, dict) else {}
    for key, value in (updates or {}).items():
        if isinstance(value, dict) and isinstance(merged.get(key), dict):
            merged[key] = _merge_config(merged[key], value)
        else:
            merged[key] = value
    return merged


def _normalise_tracked_callsign(callsign, cached_route=None):
    callsign = (callsign or "").strip().upper()
    route = cached_route or {}
    airline_icao = str(route.get("airline_icao") or "").strip().upper()
    airline_iata = str(route.get("airline_iata") or "").strip().upper()

    if not callsign:
        return ""

    if airline_icao and callsign.startswith(airline_icao):
        suffix = callsign[len(airline_icao):]
        # Repair values created by double-converting BAW293 as BA + W293.
        if suffix.startswith(airline_icao[-1:]) and suffix[1:].isdigit():
            return airline_icao + suffix[1:]
        return callsign

    if airline_icao and airline_iata and callsign.startswith(airline_iata):
        suffix = callsign[len(airline_iata):]
        if suffix:
            return airline_icao + suffix

    return callsign


def _public_flight_number(callsign, cached_route=None):
    callsign = (callsign or "").strip().upper()
    route = cached_route or {}
    explicit = str(
        route.get("public_number")
        or route.get("display_number")
        or route.get("flight_number")
        or ""
    ).strip().upper()
    if explicit:
        return explicit

    airline_icao = str(route.get("airline_icao") or "").strip().upper()
    airline_iata = str(route.get("airline_iata") or "").strip().upper()
    if airline_icao and airline_iata and callsign.startswith(airline_icao):
        suffix = callsign[len(airline_icao):]
        if suffix:
            return airline_iata + suffix

    number = str(route.get("number") or "").strip().upper()
    if number and number != callsign:
        return number
    return callsign


def _tracked_waiting_payload(callsign, payload):
    route = payload.get("cached_route") or {}
    return {
        "callsign": callsign,
        "override_flights": bool(payload.get("override_flights", False)),
        "override_idle": bool(payload.get("override_idle", True)),
        "number": _public_flight_number(callsign, route),
        "airline_name": route.get("airline_name", "") or route.get("airline", ""),
        "origin": route.get("origin", ""),
        "destination": route.get("destination", ""),
        "is_live": False,
        "waiting_live": True,
        "last_seen_ts": None,
    }


def _home_location(cfg):
    try:
        home = cfg.get("location", {}).get("location_home", [])
        if len(home) < 2:
            return None
        return (round(float(home[0]), 6), round(float(home[1]), 6))
    except (TypeError, ValueError):
        return None


def _current_home_location():
    cfg_path = os.path.join(BASE_DIR, "config", "config.json")
    try:
        with open(cfg_path, "r", encoding="utf-8-sig") as f:
            return _home_location(json.load(f))
    except Exception:
        return None


def _reset_distance_records():
    for path in (CLOSEST_FILE, FARTHEST_FILE):
        with open(path, "w", encoding="utf-8") as f:
            json.dump([], f, indent=2)

    maps_dir = os.path.join(WEB_DIR, "static", "maps")
    for filename in ("closest.html", "farthest.html"):
        try:
            os.remove(os.path.join(maps_dir, filename))
        except FileNotFoundError:
            pass


def _distance_records_home():
    try:
        with open(DISTANCE_RECORD_HOME_FILE, "r", encoding="utf-8") as f:
            return tuple(json.load(f).get("location_home", []))
    except Exception:
        return None


def _save_distance_records_home(home):
    os.makedirs(os.path.dirname(DISTANCE_RECORD_HOME_FILE), exist_ok=True)
    with open(DISTANCE_RECORD_HOME_FILE, "w", encoding="utf-8") as f:
        json.dump({"location_home": list(home)}, f, indent=2)


def _ensure_distance_records_current():
    home = _current_home_location()
    if home is not None and _distance_records_home() != home:
        _reset_distance_records()
        _save_distance_records_home(home)
        return True
    return False


def _run_cmd(cmd, timeout=10):
    return subprocess.run(
        cmd,
        capture_output=True,
        text=True,
        timeout=timeout,
        check=False,
    )


def _nmcli_available():
    return shutil.which("nmcli") is not None


def _split_nmcli_line(line):
    parts = []
    buf = []
    escaped = False
    for ch in line:
        if escaped:
            buf.append(ch)
            escaped = False
        elif ch == "\\":
            escaped = True
        elif ch == ":":
            parts.append("".join(buf))
            buf = []
        else:
            buf.append(ch)
    parts.append("".join(buf))
    return parts


def _wifi_device():
    if not _nmcli_available():
        return ""
    result = _run_cmd(["nmcli", "-t", "-f", "DEVICE,TYPE", "device", "status"])
    for line in result.stdout.splitlines():
        parts = _split_nmcli_line(line)
        if len(parts) >= 2 and parts[1] == "wifi":
            return parts[0]
    return "wlan0"


def _wifi_signal_from_dbm(dbm):
    try:
        dbm = float(dbm)
    except (TypeError, ValueError):
        return 0
    return max(0, min(100, int(2 * (dbm + 100))))


def _merge_wifi_network(networks, network):
    ssid = network.get("ssid", "")
    if not ssid:
        return
    existing = networks.get(ssid)
    if not existing:
        networks[ssid] = network
        return
    existing["active"] = existing.get("active") or network.get("active")
    existing["signal"] = max(int(existing.get("signal") or 0), int(network.get("signal") or 0))
    if not existing.get("security") and network.get("security"):
        existing["security"] = network["security"]
    sources = set((existing.get("source") or "").split("+"))
    sources.update((network.get("source") or "").split("+"))
    existing["source"] = "+".join(sorted(s for s in sources if s))


def _scan_nmcli(device="", rescan=True):
    cmd = ["nmcli", "-t", "-f", "ACTIVE,SSID,SIGNAL,SECURITY", "device", "wifi", "list"]
    if device:
        cmd.extend(["ifname", device])
    if rescan:
        cmd.extend(["--rescan", "yes"])

    result = _run_cmd(cmd, timeout=25)
    networks = []
    if result.returncode != 0:
        return networks, result.stderr.strip()

    for line in result.stdout.splitlines():
        parts = _split_nmcli_line(line)
        if len(parts) < 4:
            continue
        active, ssid, signal, security = parts[:4]
        if not ssid:
            continue
        try:
            signal_value = int(signal)
        except ValueError:
            signal_value = 0
        networks.append({
            "ssid": ssid,
            "active": active == "yes",
            "signal": signal_value,
            "security": security,
            "source": "nmcli",
        })
    return networks, ""


def _scan_iw(device):
    iw_path = shutil.which("iw")
    if not device or not iw_path:
        return [], ""
    result = _run_cmd([iw_path, "dev", device, "scan"], timeout=25)
    if result.returncode != 0:
        if shutil.which("sudo"):
            result = _run_cmd(["sudo", "-n", iw_path, "dev", device, "scan"], timeout=25)
        if result.returncode != 0:
            return [], result.stderr.strip()

    networks = []
    current = {}
    for raw_line in result.stdout.splitlines():
        line = raw_line.strip()
        if line.startswith("BSS "):
            if current:
                networks.append(current)
            current = {"ssid": "", "signal": 0, "security": "", "active": False, "source": "iw"}
        elif line.startswith("SSID:"):
            current["ssid"] = line.split("SSID:", 1)[1].strip()
        elif line.startswith("signal:"):
            value = line.split("signal:", 1)[1].strip().split()[0]
            current["signal"] = _wifi_signal_from_dbm(value)
        elif "RSN:" in line or "WPA:" in line:
            current["security"] = "WPA"
        elif "Privacy:" in line and "yes" in line.lower() and not current.get("security"):
            current["security"] = "WEP"
    if current:
        networks.append(current)
    return [n for n in networks if n.get("ssid")], ""


def _scan_iwlist(device):
    iwlist_path = shutil.which("iwlist")
    if not device or not iwlist_path:
        return [], ""
    result = _run_cmd([iwlist_path, device, "scan"], timeout=25)
    if result.returncode != 0:
        if shutil.which("sudo"):
            result = _run_cmd(["sudo", "-n", iwlist_path, device, "scan"], timeout=25)
        if result.returncode != 0:
            return [], result.stderr.strip()

    networks = []
    current = {}
    for raw_line in result.stdout.splitlines():
        line = raw_line.strip()
        if "Cell " in line and "Address:" in line:
            if current:
                networks.append(current)
            current = {"ssid": "", "signal": 0, "security": "", "active": False, "source": "iwlist"}
        elif "ESSID:" in line:
            current["ssid"] = line.split("ESSID:", 1)[1].strip().strip('"')
        elif "Quality=" in line:
            quality = line.split("Quality=", 1)[1].split()[0]
            try:
                num, den = quality.split("/", 1)
                current["signal"] = int(float(num) / float(den) * 100)
            except Exception:
                pass
        elif "Encryption key:on" in line:
            current["security"] = current.get("security") or "WPA/WEP"
        elif "IE: IEEE 802.11i/WPA2" in line:
            current["security"] = "WPA2"
        elif "IE: WPA" in line:
            current["security"] = "WPA"
    if current:
        networks.append(current)
    return [n for n in networks if n.get("ssid")], ""


def _wifi_scan_methods(device):
    merged = {}
    errors = []

    for method in (
        lambda: _scan_nmcli(device=device, rescan=True),
        lambda: _scan_nmcli(device="", rescan=True),
        lambda: _scan_iw(device),
        lambda: _scan_iwlist(device),
        lambda: _scan_nmcli(device=device, rescan=False),
    ):
        found, error = method()
        for network in found:
            _merge_wifi_network(merged, network)
        if error:
            errors.append(error)

    networks = list(merged.values())
    networks.sort(key=lambda item: (item.get("active", False), int(item.get("signal") or 0)), reverse=True)
    return networks, errors


def _wifi_status():
    if not _nmcli_available():
        ssid = ""
        if shutil.which("iwgetid"):
            result = _run_cmd(["iwgetid", "-r"], timeout=5)
            ssid = result.stdout.strip() if result.returncode == 0 else ""
        return {
            "available": False,
            "tool": "iwgetid" if ssid else "",
            "ssid": ssid,
            "device": "",
            "message": "Install or enable NetworkManager/nmcli to manage Wi-Fi from the web UI.",
        }

    device = _wifi_device()
    status = {
        "available": True,
        "tool": "nmcli",
        "ssid": "",
        "device": device,
        "message": "",
    }

    result = _run_cmd(["nmcli", "-t", "-f", "ACTIVE,SSID,SIGNAL,SECURITY", "device", "wifi", "list"], timeout=10)
    if result.returncode != 0:
        status["message"] = result.stderr.strip() or "Could not read Wi-Fi status."
        return status
    for line in result.stdout.splitlines():
        parts = _split_nmcli_line(line)
        if len(parts) >= 2 and parts[0] == "yes":
            status["ssid"] = parts[1]
            status["signal"] = parts[2] if len(parts) > 2 else ""
            status["security"] = parts[3] if len(parts) > 3 else ""
            break
    try:
        from utilities.wifi_setup_mode import status as _setup_status
        status["setup"] = _setup_status()
    except Exception:
        status["setup"] = {"active": False}
    return status


def _wifi_scan():
    if not _nmcli_available():
        return {"available": False, "networks": [], "message": "NetworkManager/nmcli is not available."}

    device = _wifi_device()
    networks, errors = _wifi_scan_methods(device)
    try:
        from utilities.wifi_setup_mode import cached_scan_networks
        merged = {network.get("ssid"): network for network in networks if network.get("ssid")}
        for network in cached_scan_networks():
            _merge_wifi_network(merged, network)
        networks = list(merged.values())
        networks.sort(
            key=lambda item: (
                item.get("active", False),
                int(item.get("signal") or 0),
            ),
            reverse=True,
        )
    except Exception as e:
        errors.append(f"Cached setup scan unavailable: {e}")
    message = ""
    if errors and not networks:
        message = "Wi-Fi scan failed: " + " | ".join(errors[:2])
    elif errors:
        message = f"Found {len(networks)} Wi-Fi networks. Some scan methods failed."
    return {"available": True, "device": device, "networks": networks, "message": message, "errors": errors[:4]}


def _wifi_deep_scan():
    if not _nmcli_available():
        return {"available": False, "networks": [], "message": "NetworkManager/nmcli is not available."}

    import time
    device = _wifi_device()
    current = _wifi_status().get("ssid", "")
    errors = []
    disconnected = False

    if device:
        result = _run_cmd(["nmcli", "device", "disconnect", device], timeout=10)
        disconnected = result.returncode == 0
        if result.returncode != 0:
            errors.append(result.stderr.strip() or "Could not temporarily disconnect Wi-Fi.")
        time.sleep(2)

    networks, scan_errors = _wifi_scan_methods(device)
    errors.extend(scan_errors)

    if disconnected and device:
        if current.startswith("PlaneTracker-Setup"):
            try:
                from utilities.wifi_setup_mode import start_setup_ap
                start_setup_ap(force=True)
            except Exception as e:
                errors.append(f"Could not restore setup hotspot: {e}")
        elif current:
            reconnect = _run_cmd(["nmcli", "device", "wifi", "connect", current, "ifname", device], timeout=25)
            if reconnect.returncode != 0:
                _run_cmd(["nmcli", "device", "connect", device], timeout=15)
        else:
            _run_cmd(["nmcli", "device", "connect", device], timeout=15)

    message = f"Deep scan found {len(networks)} Wi-Fi networks"
    if current:
        message += f"; attempted to return to {current}"
    if errors and not networks:
        message = "Deep scan failed: " + " | ".join(errors[:2])
    elif errors:
        message += ". Some scan methods failed."

    return {
        "available": True,
        "device": device,
        "networks": networks,
        "message": message,
        "deep": True,
        "errors": errors[:4],
    }


def load_flight_database():
    """Merge route-cache rows with flight counter seen totals for the DB page."""
    import sqlite3
    from collections import Counter

    seen = Counter()
    first_seen = {}
    last_seen = {}

    try:
        from utilities.flight_history import callsign_summary
        for item in callsign_summary():
            callsign = (item.get("callsign") or "").strip().upper()
            if not callsign:
                continue
            seen[callsign] = int(item.get("seen_count") or 0)
            first_seen[callsign] = item.get("first_seen") or ""
            last_seen[callsign] = item.get("last_seen") or ""
    except Exception as e:
        print(f"[DB] flight history read failed: {e}")

    def _sort_stamp(value):
        if not value:
            return 0
        try:
            return int(datetime.strptime(value, "%Y-%m-%d %H:%M:%S").timestamp())
        except Exception:
            try:
                return int(datetime.strptime(value, "%Y-%m-%d").timestamp())
            except Exception:
                return 0

    rows = {}
    db_path = os.path.join(BASE_DIR, ".cache", "route_cache.sqlite3")
    cache_decisions = Counter()
    original_sources = Counter()
    try:
        from utilities import route_db
        route_db.stats()
        with sqlite3.connect(db_path, timeout=10) as conn:
            conn.row_factory = sqlite3.Row
            for row in conn.execute(
                """
                SELECT callsign, flight_date, data_json, source,
                       created_by, parent_flight_date, last_match_type,
                       first_seen, last_seen, last_used, hit_count
                FROM route_cache
                ORDER BY last_seen DESC
                """
            ):
                callsign = (row["callsign"] or "").strip().upper()
                instance_date = row["flight_date"] or ""
                try:
                    data = json.loads(row["data_json"] or "{}")
                except Exception:
                    data = {}
                created_by = row["created_by"] or "api"
                last_match_type = row["last_match_type"] or ""
                original_source = row["source"] or "Unknown"
                cache_decisions[last_match_type or created_by] += 1
                original_sources[original_source] += 1
                last_used_ts = int(row["last_used"] or 0)
                rows[f"{callsign}|{instance_date}"] = {
                    "callsign": callsign,
                    "flight_date": instance_date,
                    "airline": data.get("airline") or data.get("airline_name") or "",
                    "plane": data.get("plane") or data.get("plane_type") or data.get("aircraft_type") or "",
                    "origin": data.get("origin") or data.get("origin_iata") or "?",
                    "destination": data.get("destination") or data.get("dest_iata") or "?",
                    "source": original_source,
                    "original_source": original_source,
                    "created_by": created_by,
                    "parent_flight_date": row["parent_flight_date"] or "",
                    "last_match_type": last_match_type,
                    "seen_count": seen.get(callsign, 0),
                    "cache_hits": int(row["hit_count"] or 0),
                    "first_seen": first_seen.get(callsign, ""),
                    "last_seen": last_seen.get(callsign, ""),
                    "last_seen_sort": _sort_stamp(last_seen.get(callsign, "")),
                    "cache_first_seen": int(row["first_seen"] or 0),
                    "cache_last_seen": int(row["last_seen"] or 0),
                    "last_used": (
                        datetime.fromtimestamp(last_used_ts).strftime(
                            "%Y-%m-%d %H:%M:%S"
                        )
                        if last_used_ts else ""
                    ),
                    "in_route_cache": True,
                }
    except Exception as e:
        print(f"[DB] route cache read failed: {e}")

    for callsign, count in seen.items():
        if any(row["callsign"] == callsign for row in rows.values()):
            continue
        rows[callsign] = {
            "callsign": callsign,
            "flight_date": "",
            "airline": "",
            "plane": "",
            "origin": "?",
            "destination": "?",
            "source": "",
            "original_source": "",
            "created_by": "",
            "parent_flight_date": "",
            "last_match_type": "",
            "seen_count": count,
            "cache_hits": 0,
            "first_seen": first_seen.get(callsign, ""),
            "last_seen": last_seen.get(callsign, ""),
            "last_seen_sort": _sort_stamp(last_seen.get(callsign, "")),
            "cache_first_seen": 0,
            "cache_last_seen": 0,
            "last_used": "",
            "in_route_cache": False,
        }

    result = sorted(
        rows.values(),
        key=lambda item: (item["last_seen_sort"], item["cache_last_seen"], item["seen_count"]),
        reverse=True,
    )
    return {
        "count": len(result),
        "route_cache_count": sum(1 for row in result if row["in_route_cache"]),
        "cache_hits": sum(row["cache_hits"] for row in result),
        "cache_decisions": dict(cache_decisions),
        "original_sources": dict(original_sources),
        "seen_total": sum(row["seen_count"] for row in result),
        "rows": result,
    }


def lookup_flight(callsign):
    """Look up a flight using routelookup cascade (AirLabs → FlightAware → FR24)."""
    callsign = callsign.strip().upper()
    try:
        from utilities.routelookup import RouteClient
        from utilities.opensky import OpenSkyClient
        rc = RouteClient()
        os = OpenSkyClient()

        # Check if airborne via OpenSky first
        state = os.find_callsign(callsign)
        if not state:
            return {"found": False}

        # Get route info via cascade
        details = rc.get_flight_details(
            callsign,
            state["latitude"],
            state["longitude"],
        )

        origin = details.get("origin", "???") or "???"
        dest   = details.get("destination", "???") or "???"
        airline = details.get("airline", "")

        return {
            "found":       True,
            "callsign":    callsign,
            "number":      callsign,
            "airline":     airline,
            "origin":      origin,
            "destination": dest,
            "summary":     f"{airline} {callsign} {origin}→{dest}",
        }

    except Exception as e:
        print(f"Lookup error: {e}")
        return {"found": False, "error": str(e)}


def search_route(origin, destination):
    """Search for flights on a route using FR24 flight-summary/light."""
    origin      = origin.strip().upper()
    destination = destination.strip().upper()
    try:
        from utilities.flightradar import FR24Client
        fr24 = FR24Client()
        if not fr24.ok:
            return {"found": False, "error": "FR24 not configured"}

        from datetime import datetime, timezone, timedelta
        import requests as _req
        now_utc  = datetime.now(timezone.utc)
        from_utc = (now_utc - timedelta(hours=12)).strftime("%Y-%m-%dT%H:%M:%S")
        to_utc   = (now_utc + timedelta(hours=6)).strftime("%Y-%m-%dT%H:%M:%S")

        from config import FLIGHTRADAR24_KEY as _KEY
        r = _req.get(
            "https://fr24api.flightradar24.com/api/flight-summary/light",
            headers={"Accept": "application/json", "Accept-Version": "v1",
                     "Authorization": f"Bearer {_KEY}"},
            params={"flight_datetime_from": from_utc, "flight_datetime_to": to_utc,
                    "airports": f"outbound:{origin},inbound:{destination}",
                    "limit": 20, "sort": "desc"},
            timeout=10,
        )
        if r.status_code != 200:
            return {"found": False, "error": f"FR24 HTTP {r.status_code}"}

        flights = r.json().get("data", [])
        results = []
        for f in flights:
            results.append({
                "callsign":    f.get("callsign", "").strip(),
                "number":      f.get("flight", ""),
                "airline":     f.get("painted_as", ""),
                "origin":      origin,
                "destination": destination,
                "dep_time":    f.get("datetime_takeoff", ""),
                "is_live":     not f.get("flight_ended", True),
                "status":      "En route" if not f.get("flight_ended", True) else "Landed",
            })
        return {"found": bool(results), "flights": results}
    except Exception as e:
        print(f"Route search error: {e}")
        return {"found": False, "error": str(e)}

def _fa_lookup_callsign(callsign):
    """
    Call FlightAware /flights/{ident} and return all today's flights as picker dicts.
    This is Call 1 when the flight isn't yet airborne.
    """
    from time import time as _time
    now = _time()
    results = []
    try:
        from utilities.flightaware import _get_active_key, _parse_flight, BASE_URL, _increment_usage, is_available as _fa_ok
        import requests as _req
        if not _fa_ok():
            return []
        key = _get_active_key()
        r = _req.get(
            f"{BASE_URL}/flights/{callsign}",
            headers={"x-apikey": key},
            params={"max_pages": 1},
            timeout=10,
        )
        _increment_usage(key)
        # Log the lookup call
        try:
            from utilities.routelookup import _log_usage
            _log_usage("FlightAware(lookup)", callsign, None, None)
        except Exception:
            pass
        if r.status_code != 200:
            print(f"[lookup] FlightAware HTTP {r.status_code} for {callsign}")
            return []
        flights = r.json().get("flights", [])
        print(f"[lookup] FlightAware returned {len(flights)} flights for {callsign}")
        for f in flights:
            parsed = _parse_flight(f)
            sched_dep = parsed.get("time_scheduled_departure")
            # Only within 24h window
            if sched_dep and abs(sched_dep - now) > 86400:
                continue
            origin = parsed.get("origin_iata", "")
            dest   = parsed.get("dest_iata", "")
            if not origin and not dest:
                continue
            status = f.get("status", "Scheduled")
            operator_icao = (parsed.get("airline_icao") or "").strip().upper()
            operator_iata = (parsed.get("airline_iata") or "").strip().upper()
            tracked_callsign = callsign
            if (
                operator_icao
                and operator_iata
                and callsign.startswith(operator_iata)
                and not callsign.startswith(operator_icao)
                and len(callsign) > len(operator_iata)
            ):
                tracked_callsign = operator_icao + callsign[len(operator_iata):]
            results.append({
                "flight_id":           f.get("fa_flight_id", ""),
                "callsign":            tracked_callsign,
                "number":              callsign,
                "registration":        f.get("registration", ""),
                "aircraft":            f.get("aircraft_type", ""),
                "airline":             parsed.get("airline_name", ""),
                "origin":              origin,
                "destination":         dest,
                "latitude":            0, "longitude": 0, "altitude": 0,
                "is_live":             status == "En Route",
                "status":              status,
                "scheduled_departure": sched_dep,
                "cached_route": {
                    "origin":               origin,
                    "origin_latitude":      parsed.get("origin_lat"),
                    "origin_longitude":     parsed.get("origin_lon"),
                    "destination":          dest,
                    "destination_latitude": parsed.get("dest_lat"),
                    "destination_longitude":parsed.get("dest_lon"),
                    "time_scheduled_departure": sched_dep,
                    "time_scheduled_arrival":   parsed.get("time_scheduled_arrival"),
                    "time_real_departure":      parsed.get("time_real_departure"),
                    "time_estimated_arrival":   parsed.get("time_estimated_arrival"),
                    "airline_icao":              operator_icao,
                    "airline_iata":              operator_iata,
                    "number":                    _public_flight_number(tracked_callsign, {
                        "airline_icao": operator_icao,
                        "airline_iata": operator_iata,
                    }),
                },
            })
    except Exception as e:
        print(f"[lookup] _fa_lookup_callsign error: {e}")
        import traceback; traceback.print_exc()
    return results


# --- Routes ---

@app.get("/")
def index():
    try:
        from utilities.wifi_setup_mode import is_setup_active
        if is_setup_active():
            return redirect("/wifi-setup")
    except Exception:
        pass
    try:
        from config import JOURNEY_CODE_SELECTED
    except Exception:
        JOURNEY_CODE_SELECTED = "ORD"
    return render_template("index.html", airport_code=JOURNEY_CODE_SELECTED)

@app.get("/closest/json")
def closest_json():
    _ensure_distance_records_current()
    return jsonify(load_json(CLOSEST_FILE, []))

@app.get("/farthest/json")
def farthest_json():
    _ensure_distance_records_current()
    return jsonify(load_json(FARTHEST_FILE, []))

@app.get("/closest")
def closest_page():
    _ensure_distance_records_current()
    try:
        import importlib
        from web import map_generator
        importlib.reload(map_generator)
        map_generator.generate_closest_map(load_json(CLOSEST_FILE, []), filename="closest.html")
    except Exception as e:
        print(f"Could not regenerate closest map: {e}")
    return render_template("closest_map.html", map_version=int(datetime.now().timestamp()))

@app.get("/farthest")
def farthest_page():
    _ensure_distance_records_current()
    try:
        import importlib
        from web import map_generator
        importlib.reload(map_generator)
        map_generator.generate_farthest_map(load_json(FARTHEST_FILE, []), filename="farthest.html")
    except Exception as e:
        print(f"Could not regenerate farthest map: {e}")
    return render_template("farthest_map.html", map_version=int(datetime.now().timestamp()))

@app.get("/tracked/json")
def tracked_json():
    return jsonify(load_json(TRACKED_FILE, {"callsign": ""}))

@app.get("/tracked")
def tracked_page():
    return render_template("tracked.html")

@app.post("/tracked/lookup")
def tracked_lookup():
    """
    Call 1: Look up a callsign.
    - If airborne: FR24 gRPC live feed → returns position + cached route from FR24 details
    - If not airborne: FlightAware /flights/{ident} → returns scheduled instances to pick from
    - Fallback: adsb.lol position only
    """
    data     = request.get_json(force=True)
    callsign = data.get("callsign", "").strip().upper()
    if not callsign:
        return jsonify({"found": False, "error": "No callsign provided"})

    try:
        # ── Path 1: FR24 live feed (airborne flights) ──
        try:
            from utilities.fr24_unofficial import is_available as _fr24u_ok
            from utilities.fr24_client import FR24Client
            if _fr24u_ok():
                client = FR24Client()
                flights_raw = client._run_with_client(
                    lambda fr24: client._find_by_callsign_async(fr24, callsign)
                )
                print(f"[lookup] FR24 live: {len(flights_raw) if flights_raw else 0} results for {callsign}")
                if flights_raw:
                    from utilities.airports import get_airport_coords as _gac
                    from utilities.airlines import get_airline_name as _aln
                    results = []
                    for f in flights_raw:
                        airline_icao = f.airline_icao or callsign[:3]
                        origin_iata  = f.origin_airport_iata or ""
                        dest_iata    = f.destination_airport_iata or ""
                        oc = _gac(origin_iata) if origin_iata else {}
                        dc = _gac(dest_iata)   if dest_iata   else {}
                        sched_dep = sched_arr = actual_dep = eta = None
                        if f.flight_id:
                            try:
                                details  = client.get_flight_details(f)
                                schedule = details.get("schedule_info", {})
                                progress = details.get("flight_progress", {})
                                sched_dep  = schedule.get("scheduled_departure")
                                sched_arr  = schedule.get("scheduled_arrival")
                                actual_dep = schedule.get("actual_departure")
                                eta        = progress.get("eta") or (f.eta if f.eta else None)
                                f.set_flight_details(details)
                            except Exception:
                                pass
                        results.append({
                            "flight_id":           f.flight_id,
                            "callsign":            f.callsign,
                            "registration":        f.registration,
                            "aircraft":            f.aircraft_code,
                            "airline":             _aln(airline_icao) or f.airline_name or airline_icao,
                            "origin":              origin_iata,
                            "destination":         dest_iata,
                            "latitude":            f.latitude,
                            "longitude":           f.longitude,
                            "altitude":            f.altitude,
                            "is_live":             not f.on_ground and f.altitude > 0,
                            "status":              "Airborne" if (not f.on_ground and f.altitude > 0) else "Ground",
                            "scheduled_departure": float(sched_dep) if sched_dep else None,
                            "cached_route": {
                                "origin":               origin_iata,
                                "origin_latitude":      oc.get("lat"),
                                "origin_longitude":     oc.get("lon"),
                                "destination":          dest_iata,
                                "destination_latitude": dc.get("lat"),
                                "destination_longitude":dc.get("lon"),
                                "time_scheduled_departure": float(sched_dep)  if sched_dep  else None,
                                "time_scheduled_arrival":   float(sched_arr)  if sched_arr  else None,
                                "time_real_departure":      float(actual_dep) if actual_dep else None,
                                "time_estimated_arrival":   float(eta)        if eta        else None,
                            },
                        })
                    if len(results) == 1:
                        r = results[0]
                        return jsonify({"found": True, "multiple": False, "callsign": callsign, "flight": r,
                                        "summary": f"{r['airline'] or callsign} {r['origin']}→{r['destination']} ({r['status']})"})
                    elif len(results) > 1:
                        return jsonify({"found": True, "multiple": True, "callsign": callsign, "flights": results,
                                        "summary": f"{len(results)} flights found — please select one"})
        except Exception as e:
            print(f"[lookup] FR24 live error: {e}")
            import traceback; traceback.print_exc()

        # ── Path 2: FlightAware scheduled lookup (not yet airborne) ──
        results = _fa_lookup_callsign(callsign)
        if results:
            if len(results) == 1:
                r = results[0]
                dep = r.get("scheduled_departure")
                dep_str = f" DEP {datetime.fromtimestamp(dep, tz=timezone.utc).strftime('%H:%M UTC')}" if dep else ""
                return jsonify({"found": True, "multiple": False, "callsign": callsign, "flight": r,
                                "summary": f"{r['airline'] or callsign} {r['origin']}→{r['destination']} ({r['status']}){dep_str}"})
            else:
                return jsonify({"found": True, "multiple": True, "callsign": callsign, "flights": results,
                                "summary": f"{len(results)} scheduled flights found — please select one"})

        # ── Path 3: adsb.lol position-only fallback ──
        from utilities.opensky import OpenSkyClient
        state = OpenSkyClient().find_callsign(callsign)
        if state:
            return jsonify({
                "found": True, "multiple": False, "callsign": callsign,
                "flight": {
                    "flight_id": "", "callsign": callsign,
                    "registration": state.get("icao24", ""),
                    "aircraft": "", "airline": "", "origin": "", "destination": "",
                    "latitude": state.get("latitude"), "longitude": state.get("longitude"),
                    "altitude": state.get("altitude", 0), "is_live": True,
                    "status": "Airborne", "scheduled_departure": None, "cached_route": None,
                },
                "summary": f"{callsign} is airborne (position only — no route data)",
            })

        return jsonify({
            "found": False, "callsign": callsign,
            "summary": f"{callsign} not found — check the callsign or try again closer to departure.",
        })

    except Exception as e:
        print(f"[lookup] Error: {e}")
        import traceback; traceback.print_exc()
        return jsonify({"found": False, "error": str(e)})

@app.post("/tracked/set")
def tracked_set():
    data         = request.get_json(force=True)
    callsign     = data.get("callsign", "").strip().upper()
    cached_route = data.get("cached_route")
    callsign     = _normalise_tracked_callsign(callsign, cached_route)
    flight_id    = data.get("flight_id", "")
    sched_dep    = data.get("scheduled_departure")
    try:
        payload = {"callsign": callsign}
        if callsign:
            payload["override_flights"] = bool(data.get("override_flights", False))
            payload["override_idle"] = bool(data.get("override_idle", True))
        if cached_route:  payload["cached_route"]        = cached_route
        if flight_id:     payload["flight_id"]           = flight_id
        if sched_dep:     payload["scheduled_departure"] = sched_dep
        with open(TRACKED_FILE, "w", encoding="utf-8") as f:
            json.dump(payload, f, indent=2)
        current_payload = _tracked_waiting_payload(callsign, payload) if callsign else {"callsign": ""}
        with open(os.path.join(BASE_DIR, "current_tracked.json"), "w", encoding="utf-8") as f:
            json.dump(current_payload, f, indent=2)
        msg = f"Now tracking {callsign}." if callsign else "Tracking cleared."
        return jsonify({"message": msg})
    except Exception as e:
        return jsonify({"message": f"Error saving: {e}"}), 500

@app.post("/tracked/options")
def tracked_options():
    data = request.get_json(force=True)
    try:
        payload = load_json(TRACKED_FILE, {"callsign": ""})
        callsign = str(payload.get("callsign", "")).strip().upper()
        if not callsign:
            return jsonify({"message": "No tracked flight saved.", "callsign": ""}), 400

        payload["callsign"] = callsign
        payload["override_flights"] = bool(data.get("override_flights", False))
        payload["override_idle"] = bool(data.get("override_idle", True))
        with open(TRACKED_FILE, "w", encoding="utf-8") as f:
            json.dump(payload, f, indent=2)
        return jsonify({
            "message": "Tracked flight display options saved.",
            "callsign": callsign,
            "override_flights": payload["override_flights"],
            "override_idle": payload["override_idle"],
        })
    except Exception as e:
        return jsonify({"message": f"Error saving options: {e}"}), 500

@app.post("/search/route")
def search_route_endpoint():
    data        = request.get_json(force=True)
    origin      = data.get("origin", "").strip().upper()
    destination = data.get("destination", "").strip().upper()
    if not origin or not destination:
        return jsonify({"found": False, "error": "Origin and destination required"})
    return jsonify(search_route(origin, destination))

@app.get("/debug/route")
def debug_route():
    """Debug endpoint — not available without FR24 scraper."""
    return jsonify({"error": "Debug route endpoint deprecated"})

@app.get("/weather/json")
def weather_json():
    """Serve current temperature, humidity and forecast to slave trackers."""
    try:
        from utilities.temperature import _load_file_cache, _TEMP_CACHE_FILE, _FORECAST_CACHE_FILE
        temp_data, temp_ts   = _load_file_cache(_TEMP_CACHE_FILE)
        forecast_data, f_ts  = _load_file_cache(_FORECAST_CACHE_FILE)
        if isinstance(forecast_data, dict):
            forecast_data = forecast_data.get("forecast", [])
        temp = hum = None
        if isinstance(temp_data, dict):
            temp = temp_data.get("temperature")
            hum = temp_data.get("humidity")
        elif temp_data:
            d = temp_data if isinstance(temp_data, (list, tuple)) else [None, None]
            temp, hum = (d[0], d[1]) if len(d) >= 2 else (None, None)
        return jsonify({
            "temperature": temp,
            "humidity":    hum,
            "forecast":    forecast_data or [],
        })
    except Exception:
        pass
    return jsonify({"temperature": None, "humidity": None, "forecast": []})

@app.get("/forecast/json")
def forecast_json():
    """Serve forecast intervals to slave trackers."""
    try:
        from utilities.temperature import _load_file_cache, _FORECAST_CACHE_FILE
        cached, ts = _load_file_cache(_FORECAST_CACHE_FILE)
        if isinstance(cached, dict):
            cached = cached.get("forecast", [])
        if cached:
            return jsonify(cached)
    except Exception:
        pass
    return jsonify([])

@app.get("/overhead/json")
def overhead_json():
    """Return current overhead flight data for slave trackers."""
    try:
        with open(os.path.join(BASE_DIR, "current_overhead.json"), "r") as f:
            data = json.load(f)
        if not isinstance(data, list):
            data = []
    except Exception:
        data = []
    return jsonify(data)

@app.get("/tracked/json/live")
def tracked_json_live():
    """Return current tracked flight data for slave trackers."""
    try:
        with open(os.path.join(BASE_DIR, "current_tracked.json"), "r") as f:
            return jsonify(json.load(f))
    except Exception:
        return jsonify({"callsign": ""})

@app.get("/tracked/debug/json")
def tracked_debug_json():
    saved = load_json(TRACKED_FILE, {"callsign": ""})
    live = load_json(os.path.join(BASE_DIR, "current_tracked.json"), {"callsign": ""})
    try:
        import config as _config
        order = list(getattr(_config, "API_SOURCE_ORDER", []))
        enabled = dict(getattr(_config, "API_SOURCE_ENABLED", {}))
    except Exception:
        order = []
        enabled = {}

    callsign = str(saved.get("callsign", "")).strip().upper()
    cached_route = saved.get("cached_route") or {}
    public_number = _public_flight_number(callsign, cached_route)
    try:
        from utilities.fr24_unofficial import is_available as _fr24u_ok
        fr24_unofficial_available = _fr24u_ok()
    except Exception:
        fr24_unofficial_available = False

    aliases = {}
    equivalent_callsigns = []
    try:
        from utilities.fr24_client import _airline_aliases, _equivalent_callsigns
        equivalent_callsigns = _equivalent_callsigns(callsign) if callsign else []
        if callsign:
            aliases[callsign] = _airline_aliases(callsign)
        if public_number:
            aliases[public_number] = _airline_aliases(public_number)
    except Exception:
        aliases = {}

    return jsonify({
        "saved": saved,
        "live": live,
        "api_sources": {
            "order": order,
            "enabled": enabled,
        },
        "fr24_unofficial_available": fr24_unofficial_available,
        "queries": {
            "callsign": callsign,
            "public_number": public_number,
            "equivalent_callsigns": equivalent_callsigns,
            "aliases": aliases,
        },
    })

@app.get("/tracked/probe/fr24-unofficial/json")
def tracked_probe_fr24_unofficial_json():
    saved = load_json(TRACKED_FILE, {"callsign": ""})
    callsign = request.args.get("callsign", "").strip().upper()
    if not callsign:
        callsign = str(saved.get("callsign", "")).strip().upper()
    cached_route = saved.get("cached_route") or {}
    public_number = (
        request.args.get("public_number", "").strip().upper()
        or _public_flight_number(callsign, cached_route)
        or saved.get("number")
        or saved.get("public_number")
        or ""
    )
    queries = [q for q in (callsign, public_number) if q]
    try:
        from utilities.fr24_client import _equivalent_callsigns
        for equivalent in _equivalent_callsigns(callsign):
            if equivalent:
                queries.append(equivalent)
    except Exception:
        pass
    queries = list(dict.fromkeys(queries))

    result = {
        "available": False,
        "queries": queries,
        "results": [],
    }
    try:
        from utilities.fr24_unofficial import is_available as _fr24u_ok
        result["available"] = _fr24u_ok()
        if not result["available"]:
            result["error"] = "FlightRadarAPI package is not available"
            return jsonify(result)

        from utilities.fr24_client import (
            FR24Client,
            _airline_aliases,
            _equivalent_callsigns,
            _flight_identifiers,
        )
        client = FR24Client()
        for query in queries:
            targets = set(_equivalent_callsigns(query))
            item = {"query": query, "aliases": _airline_aliases(query), "targets": list(targets)}
            try:
                tracked = client.get_tracked_flight(query)
                item["found"] = bool(tracked)
                item["tracked"] = tracked or None
                if tracked:
                    item["has_position"] = tracked.get("latitude") is not None and tracked.get("longitude") is not None

                item["direct_search"] = client.debug_search(query)

                candidates = []
                for flight in client._candidate_flights(query):
                    identifiers = sorted(_flight_identifiers(flight))
                    matched = bool(targets.intersection(identifiers))
                    if matched or len(candidates) < 20:
                        candidates.append({
                            "matched": matched,
                            "identifiers": identifiers,
                            "id": getattr(flight, "id", None),
                            "callsign": getattr(flight, "callsign", None),
                            "number": getattr(flight, "number", None),
                            "flight": getattr(flight, "flight", None),
                            "flight_number": getattr(flight, "flight_number", None),
                            "airline_iata": getattr(flight, "airline_iata", None),
                            "airline_icao": getattr(flight, "airline_icao", None),
                            "origin": getattr(flight, "origin_airport_iata", None),
                            "destination": getattr(flight, "destination_airport_iata", None),
                            "latitude": getattr(flight, "latitude", None),
                            "longitude": getattr(flight, "longitude", None),
                            "altitude": getattr(flight, "altitude", None),
                            "on_ground": getattr(flight, "on_ground", None),
                        })
                    if matched:
                        break
                item["candidate_count_shown"] = len(candidates)
                item["candidates"] = candidates
            except Exception as e:
                item["found"] = False
                item["error"] = str(e)
            result["results"].append(item)
        return jsonify(result)
    except Exception as e:
        result["error"] = str(e)
        return jsonify(result), 500

@app.get("/stats")
def stats_page():
    return render_template("stats.html")

@app.get("/stats/<date>")
def stats_day_page(date):
    return render_template("stats_day.html")

@app.get("/database")
def database_page():
    return render_template("database.html")

@app.get("/database/json")
def database_json():
    return jsonify(load_flight_database())

@app.get("/database/history/<int:event_id>/json")
def database_history_raw_json(event_id):
    try:
        from utilities.flight_history import event_raw
        data = event_raw(event_id)
        if data is None:
            return jsonify({"error": "Flight history record not found."}), 404
        return jsonify(data)
    except Exception as e:
        return jsonify({"error": str(e)}), 500

@app.get("/database/route/<callsign>/json")
def database_route_raw_json(callsign):
    import sqlite3
    callsign = (callsign or "").strip().upper()
    instance_date = (request.args.get("flight_date") or "").strip()
    db_path = os.path.join(BASE_DIR, ".cache", "route_cache.sqlite3")
    try:
        from utilities import route_db
        route_db.stats()
        with sqlite3.connect(db_path, timeout=10) as conn:
            conn.row_factory = sqlite3.Row
            if instance_date:
                row = conn.execute(
                    """
                    SELECT callsign, flight_date, data_json, source,
                           created_by, parent_flight_date, last_match_type,
                           first_seen, last_seen, last_used, hit_count
                    FROM route_cache
                    WHERE callsign = ? AND flight_date = ?
                    """,
                    (callsign, instance_date),
                ).fetchone()
            else:
                row = conn.execute(
                    """
                    SELECT callsign, flight_date, data_json, source,
                           created_by, parent_flight_date, last_match_type,
                           first_seen, last_seen, last_used, hit_count
                    FROM route_cache
                    WHERE callsign = ?
                    ORDER BY flight_date DESC, last_seen DESC
                    LIMIT 1
                    """,
                    (callsign,),
                ).fetchone()
        if not row:
            return jsonify({"error": "Route-cache record not found."}), 404
        try:
            cached = json.loads(row["data_json"] or "{}")
        except Exception:
            cached = {"unparsed_data_json": row["data_json"]}
        return jsonify({
            "callsign": row["callsign"],
            "flight_date": row["flight_date"],
            "source": row["source"],
            "original_source": row["source"],
            "created_by": row["created_by"],
            "parent_flight_date": row["parent_flight_date"],
            "last_match_type": row["last_match_type"],
            "first_seen": row["first_seen"],
            "last_seen": row["last_seen"],
            "last_used": row["last_used"],
            "hit_count": row["hit_count"],
            "data": cached,
        })
    except Exception as e:
        return jsonify({"error": str(e)}), 500

@app.get("/airport-code")
def airport_code():
    try:
        import importlib
        import config as _config
        importlib.reload(_config)
        code = getattr(_config, "JOURNEY_CODE_SELECTED", "ORD")
        lat, lon = getattr(_config, "LOCATION_HOME", [None, None])
    except Exception:
        code = "ORD"
        lat, lon = None, None

    location_name = ""
    if lat is not None and lon is not None:
        try:
            import requests as _req
            r = _req.get(
                "https://nominatim.openstreetmap.org/reverse",
                params={"lat": lat, "lon": lon, "format": "json", "zoom": 13},
                headers={"User-Agent": "plane-tracker-rgb-pi/1.0"},
                timeout=5,
            )
            if r.status_code == 200:
                addr = r.json().get("address", {})
                # Try neighbourhood/suburb first, then fall back to town/city
                neighbourhood = (
                    addr.get("neighbourhood")
                    or addr.get("suburb")
                    or addr.get("quarter")
                    or addr.get("village")
                )
                city = (
                    addr.get("city")
                    or addr.get("town")
                    or addr.get("county")
                )
                if neighbourhood and city:
                    location_name = f"{neighbourhood}, {city}"
                elif city:
                    location_name = city
        except Exception as e:
            print(f"Reverse geocode failed: {e}")

    return jsonify({"code": code, "name": location_name})

@app.get("/counter")
def flight_counter():
    """Return full flight counter log in new date-keyed format."""
    try:
        from utilities.flight_history import legacy_log
        return jsonify(legacy_log(
            request.args.get("start") or None,
            request.args.get("end") or None,
        ))
    except Exception as e:
        print(f"[Counter] read failed: {e}")
        return jsonify({})

@app.get("/counter/summary")
def flight_counter_summary():
    """Return daily summary stats for graphing."""
    try:
        from utilities.flight_history import daily_summary
        return jsonify(daily_summary(
            request.args.get("start") or None,
            request.args.get("end") or None,
        ))

        # Handle both old format (flat) and new format (date-keyed)
        if "date" in log and "callsigns" in log:
            # Old format — single day flat object
            log = {log["date"]: {
                "date": log["date"],
                "count": log["count"],
                "flights": [{"callsign": c, "hour": 0} for c in log["callsigns"]],
                "first_seen": "",
                "last_seen": "",
            }}

        summary = []
        for day, data in sorted(log.items()):
            by_hour = [0] * 24
            for flight in data.get("flights", []):
                by_hour[flight.get("hour", 0)] += 1
            summary.append({
                "date":       day,
                "count":      data["count"],
                "by_hour":    by_hour,
                "first_seen": data.get("first_seen", ""),
                "last_seen":  data.get("last_seen", ""),
            })
        return jsonify(summary)
    except Exception as e:
        return jsonify([])

@app.get("/history/calendar/json")
def history_calendar_json():
    try:
        from calendar import monthrange
        from utilities.flight_history import daily_summary, totals
        month = request.args.get("month", "").strip()
        start = end = None
        if len(month) == 7:
            year, month_number = [int(part) for part in month.split("-")]
            start = f"{year:04d}-{month_number:02d}-01"
            end = f"{year:04d}-{month_number:02d}-{monthrange(year, month_number)[1]:02d}"
        return jsonify({"days": daily_summary(start, end), "totals": totals()})
    except Exception as e:
        return jsonify({"days": [], "totals": {}, "error": str(e)}), 500

@app.delete("/history/range")
def history_delete_range():
    data = request.get_json(force=True, silent=True) or {}
    start = str(data.get("start") or "").strip()
    end = str(data.get("end") or start).strip()
    if not start or not end:
        return jsonify({"ok": False, "message": "Start and end dates are required."}), 400
    try:
        start_value = datetime.strptime(start, "%Y-%m-%d")
        end_value = datetime.strptime(end, "%Y-%m-%d")
    except ValueError:
        return jsonify({"ok": False, "message": "Dates must use YYYY-MM-DD."}), 400
    if start_value > end_value:
        return jsonify({"ok": False, "message": "Start date must not be after end date."}), 400
    if str(data.get("confirm") or "").strip().upper() != "DELETE":
        return jsonify({"ok": False, "message": "Confirmation must be DELETE."}), 400
    try:
        from utilities.flight_history import delete_range
        deleted = delete_range(start, end)
        return jsonify({"ok": True, "deleted": deleted, "start": start, "end": end})
    except Exception as e:
        return jsonify({"ok": False, "message": str(e)}), 500

@app.get("/history/export.csv")
def history_export_csv():
    try:
        import csv
        import io
        from utilities.flight_history import legacy_log
        log = legacy_log(request.args.get("start") or None, request.args.get("end") or None)
        out = io.StringIO()
        writer = csv.writer(out)
        writer.writerow(["date", "time", "callsign", "origin", "destination", "airline", "aircraft_type", "source", "latitude", "longitude", "altitude", "distance"])
        for day, data in sorted(log.items()):
            for flight in data.get("flights", []):
                writer.writerow([
                    day, flight.get("time", ""), flight.get("callsign", ""),
                    flight.get("origin", ""), flight.get("dest", ""),
                    flight.get("airline", ""), flight.get("aircraft_type", ""),
                    flight.get("source", ""), flight.get("latitude", ""),
                    flight.get("longitude", ""), flight.get("altitude", ""),
                    flight.get("distance", ""),
                ])
        return Response(
            out.getvalue(),
            mimetype="text/csv",
            headers={"Content-Disposition": "attachment; filename=flight_history.csv"},
        )
    except Exception as e:
        return Response(f"export failed: {e}\n", mimetype="text/plain", status=500)

@app.get("/maps/<path:filename>")
def maps(filename):
    maps_dir = os.path.join(WEB_DIR, "static/maps")
    response = send_from_directory(maps_dir, filename)
    response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0"
    response.headers["Pragma"] = "no-cache"
    response.headers["Expires"] = "0"
    return response

@app.get("/config")
def config_page():
    try:
        from utilities.wifi_setup_mode import is_setup_active
        if is_setup_active():
            return redirect("/wifi-setup")
    except Exception:
        pass
    response = app.make_response(render_template("config.html"))
    response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0"
    response.headers["Pragma"] = "no-cache"
    response.headers["Expires"] = "0"
    return response

@app.get("/wifi-setup")
def wifi_setup_page():
    response = app.make_response(render_template("wifi_setup.html"))
    response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0"
    response.headers["Pragma"] = "no-cache"
    response.headers["Expires"] = "0"
    return response


def _bounding_box_data():
    cfg_path = os.path.join(BASE_DIR, "config", "config.json")
    cfg = load_json(cfg_path, {})
    location = cfg.get("location", {})
    zone = location.get("zone_home", {})
    home = location.get("location_home", [])
    try:
        north = float(zone["tl_y"])
        west = float(zone["tl_x"])
        south = float(zone["br_y"])
        east = float(zone["br_x"])
        home_lat = float(home[0])
        home_lon = float(home[1])
    except (KeyError, IndexError, TypeError, ValueError):
        return None
    return {
        "home": {"lat": home_lat, "lon": home_lon},
        "top_left": {"lat": north, "lon": west},
        "top_right": {"lat": north, "lon": east},
        "bottom_right": {"lat": south, "lon": east},
        "bottom_left": {"lat": south, "lon": west},
    }


@app.get("/bounding-box")
def bounding_box_page():
    response = app.make_response(render_template("bounding_box.html"))
    response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0"
    response.headers["Pragma"] = "no-cache"
    response.headers["Expires"] = "0"
    return response


@app.get("/api/bounding-box")
def bounding_box_json():
    data = _bounding_box_data()
    if not data:
        return jsonify({"ok": False, "error": "Bounding box or home coordinates are incomplete."}), 400
    response = jsonify({"ok": True, **data})
    response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0"
    return response


@app.get("/bounding-box.kml")
def bounding_box_kml():
    data = _bounding_box_data()
    if not data:
        return "Bounding box or home coordinates are incomplete.", 400
    tl = data["top_left"]
    tr = data["top_right"]
    br = data["bottom_right"]
    bl = data["bottom_left"]
    home = data["home"]
    coordinates = " ".join([
        f"{tl['lon']},{tl['lat']},0",
        f"{tr['lon']},{tr['lat']},0",
        f"{br['lon']},{br['lat']},0",
        f"{bl['lon']},{bl['lat']},0",
        f"{tl['lon']},{tl['lat']},0",
    ])
    placemarks = "".join(
        f"<Placemark><name>{name}</name><Point><coordinates>{point['lon']},{point['lat']},0"
        f"</coordinates></Point></Placemark>"
        for name, point in (
            ("Home", home),
            ("Top Left", tl),
            ("Top Right", tr),
            ("Bottom Right", br),
            ("Bottom Left", bl),
        )
    )
    kml = (
        '<?xml version="1.0" encoding="UTF-8"?>'
        '<kml xmlns="http://www.opengis.net/kml/2.2"><Document>'
        '<name>Plane Tracker Bounding Box</name>'
        '<Style id="zone"><LineStyle><color>ffad8500</color><width>4</width></LineStyle>'
        '<PolyStyle><color>3300a5ff</color></PolyStyle></Style>'
        f'<Placemark><name>Bounding Box</name><styleUrl>#zone</styleUrl><Polygon>'
        f'<outerBoundaryIs><LinearRing><coordinates>{coordinates}</coordinates>'
        '</LinearRing></outerBoundaryIs></Polygon></Placemark>'
        f'{placemarks}</Document></kml>'
    )
    response = app.response_class(kml, mimetype="application/vnd.google-earth.kml+xml")
    response.headers["Content-Disposition"] = 'attachment; filename="plane-tracker-bounding-box.kml"'
    response.headers["Cache-Control"] = "no-store"
    return response


DISPLAY_FIELD_CATALOG = {
    "flight_details": [
        {"id": "airline", "label": "Airline name"},
        {"id": "flight_number", "label": "Flight number"},
        {"id": "callsign", "label": "Callsign"},
        {"id": "owner_icao", "label": "Operator ICAO"},
        {"id": "owner_iata", "label": "Operator IATA"},
        {"id": "origin", "label": "Origin"},
        {"id": "destination", "label": "Destination"},
        {"id": "plane", "label": "Aircraft type"},
        {"id": "altitude", "label": "Altitude"},
        {"id": "ground_speed", "label": "Ground speed"},
        {"id": "heading", "label": "Heading"},
        {"id": "vertical_speed", "label": "Vertical speed"},
        {"id": "distance", "label": "Distance from home"},
        {"id": "direction", "label": "Direction from home"},
        {"id": "distance_origin", "label": "Distance from origin"},
        {"id": "distance_destination", "label": "Distance to destination"},
        {"id": "plane_latitude", "label": "Plane latitude"},
        {"id": "plane_longitude", "label": "Plane longitude"},
        {"id": "origin_latitude", "label": "Origin latitude"},
        {"id": "origin_longitude", "label": "Origin longitude"},
        {"id": "destination_latitude", "label": "Destination latitude"},
        {"id": "destination_longitude", "label": "Destination longitude"},
        {"id": "time_scheduled_departure", "label": "Scheduled departure"},
        {"id": "time_scheduled_arrival", "label": "Scheduled arrival"},
        {"id": "time_real_departure", "label": "Actual departure"},
        {"id": "time_estimated_arrival", "label": "Estimated arrival"},
        {"id": "icao24", "label": "ICAO24 hex"},
    ],
    "plane_details": [
        {"id": "plane", "label": "Aircraft type"},
        {"id": "home_distance", "label": "Distance from home"},
        {"id": "direction", "label": "Direction from home"},
        {"id": "altitude", "label": "Altitude"},
        {"id": "vertical_speed", "label": "Climb/descent arrow"},
        {"id": "ground_speed", "label": "Ground speed"},
        {"id": "heading", "label": "Heading"},
        {"id": "callsign", "label": "Callsign"},
        {"id": "flight_number", "label": "Flight number"},
        {"id": "airline", "label": "Airline name"},
        {"id": "origin", "label": "Origin"},
        {"id": "destination", "label": "Destination"},
        {"id": "distance_origin", "label": "Distance from origin"},
        {"id": "distance_destination", "label": "Distance to destination"},
        {"id": "plane_latitude", "label": "Plane latitude"},
        {"id": "plane_longitude", "label": "Plane longitude"},
        {"id": "origin_latitude", "label": "Origin latitude"},
        {"id": "origin_longitude", "label": "Origin longitude"},
        {"id": "destination_latitude", "label": "Destination latitude"},
        {"id": "destination_longitude", "label": "Destination longitude"},
        {"id": "time_scheduled_departure", "label": "Scheduled departure"},
        {"id": "time_scheduled_arrival", "label": "Scheduled arrival"},
        {"id": "time_real_departure", "label": "Actual departure"},
        {"id": "time_estimated_arrival", "label": "Estimated arrival"},
        {"id": "owner_icao", "label": "Operator ICAO"},
        {"id": "icao24", "label": "ICAO24 hex"},
    ],
}

DEFAULT_DISPLAY_FIELDS = {
    "flight_details": ["airline", "flight_number"],
    "plane_details": ["plane", "home_distance", "direction", "altitude", "vertical_speed"],
}

@app.get("/display-fields")
def display_fields_page():
    return render_template("display_fields.html")

@app.get("/api/display-fields")
def display_fields_get():
    cfg_path = os.path.join(BASE_DIR, "config", "config.json")
    try:
        with open(cfg_path, "r", encoding="utf-8") as f:
            cfg = json.load(f)
    except Exception:
        cfg = {}
    return jsonify({
        "catalog": DISPLAY_FIELD_CATALOG,
        "selected": cfg.get("display_fields", DEFAULT_DISPLAY_FIELDS),
        "defaults": DEFAULT_DISPLAY_FIELDS,
    })

@app.post("/api/display-fields")
def display_fields_save():
    data = request.get_json(force=True)
    selected = data.get("display_fields", {})
    cleaned = {}
    for section, catalog in DISPLAY_FIELD_CATALOG.items():
        allowed = {f["id"] for f in catalog}
        values = [v for v in selected.get(section, []) if v in allowed]
        cleaned[section] = values or DEFAULT_DISPLAY_FIELDS[section]

    cfg_path = os.path.join(BASE_DIR, "config", "config.json")
    os.makedirs(os.path.dirname(cfg_path), exist_ok=True)
    try:
        with open(cfg_path, "r", encoding="utf-8") as f:
            cfg = json.load(f)
    except Exception:
        cfg = {}
    cfg["display_fields"] = cleaned
    with open(cfg_path, "w", encoding="utf-8") as f:
        json.dump(cfg, f, indent=2)
    try:
        import config as _config
        _config.reload()
    except Exception:
        pass
    return jsonify({"ok": True, "display_fields": cleaned})

@app.get("/api/config")
def config_get():
    cfg_path = os.path.join(BASE_DIR, "config", "config.json")
    sec_path = os.path.join(BASE_DIR, "config", "secrets.json")
    os.makedirs(os.path.join(BASE_DIR, "config"), exist_ok=True)
    try:
        with open(cfg_path, "r") as f: cfg = json.load(f)
    except Exception: cfg = {}
    try:
        with open(sec_path, "r") as f: sec = json.load(f)
    except Exception: sec = {}
    def _mask(v):
        if isinstance(v, str) and len(v) > 8: return v[:4] + "****" + v[-4:]
        return v
    sec_masked = {}
    for k, v in sec.items():
        if isinstance(v, list): sec_masked[k] = [_mask(i) for i in v]
        else: sec_masked[k] = _mask(v)
    response = jsonify({"config": cfg, "secrets": sec_masked})
    response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0"
    response.headers["Pragma"] = "no-cache"
    response.headers["Expires"] = "0"
    return response


@app.get("/api/config/runtime")
def config_runtime():
    cfg_path = os.path.join(BASE_DIR, "config", "config.json")
    disk = load_json(cfg_path, {})
    try:
        import config as runtime_config
        runtime = {
            "temperature_units": getattr(runtime_config, "TEMPERATURE_UNITS", None),
            "position_poll_seconds": getattr(runtime_config, "POSITION_POLL_SECONDS", None),
            "panel_count": getattr(runtime_config, "PANEL_COUNT", None),
            "zone_home": getattr(runtime_config, "ZONE_HOME", None),
        }
    except Exception as e:
        runtime = {"error": str(e)}
    try:
        modified = os.path.getmtime(cfg_path)
    except OSError:
        modified = None
    return jsonify({
        "base_dir": BASE_DIR,
        "config_path": cfg_path,
        "config_exists": os.path.exists(cfg_path),
        "config_modified_ts": modified,
        "disk": {
            "temperature_units": disk.get("location", {}).get("temperature_units"),
            "position_poll_seconds": disk.get("position_sources", {}).get("poll_seconds"),
            "panel_count": disk.get("display", {}).get("panel_count"),
            "zone_home": disk.get("location", {}).get("zone_home"),
        },
        "runtime": runtime,
    })


@app.post("/api/config")
def config_save():
    data = request.get_json(force=True)
    errors = []
    home_location_changed = False
    if "config" in data:
        try:
            cfg_path = os.path.join(BASE_DIR, "config", "config.json")
            os.makedirs(os.path.dirname(cfg_path), exist_ok=True)
            try:
                with open(cfg_path, "r", encoding="utf-8") as f:
                    old_cfg = json.load(f)
            except Exception:
                old_cfg = {}
            new_cfg = _merge_config(old_cfg, data["config"])
            flights = new_cfg.setdefault("flights", {})
            try:
                max_display_flights = int(flights.get("max_display_flights", 5))
            except (TypeError, ValueError):
                max_display_flights = 5
            flights["max_display_flights"] = max(1, min(5, max_display_flights))
            updates = new_cfg.setdefault("updates", {})
            updates["enabled"] = bool(updates.get("enabled", False))
            updates["check_on_start"] = bool(updates.get("check_on_start", True))
            updates["auto_install"] = bool(updates.get("auto_install", True))
            updates["manifest_url"] = str(updates.get("manifest_url", "") or "").strip()
            try:
                update_timeout = int(updates.get("timeout_seconds", 20))
            except (TypeError, ValueError):
                update_timeout = 20
            updates["timeout_seconds"] = max(5, min(120, update_timeout))
            try:
                keep_backups = int(updates.get("keep_backups", 2))
            except (TypeError, ValueError):
                keep_backups = 2
            updates["keep_backups"] = max(1, min(10, keep_backups))
            position_sources = new_cfg.setdefault("position_sources", {})
            try:
                poll_seconds = int(position_sources.get("poll_seconds", 120))
            except (TypeError, ValueError):
                poll_seconds = 120
            position_sources["poll_seconds"] = (
                poll_seconds if poll_seconds in {30, 45, 60, 85, 120} else 120
            )
            new_home = _home_location(data["config"])
            home_location_changed = (
                new_home is not None
                and (
                    (_home_location(old_cfg) is not None and _home_location(old_cfg) != new_home)
                    or _distance_records_home() != new_home
                )
            )
            _atomic_write_json(cfg_path, new_cfg, keep_backup=True)
            if home_location_changed:
                _reset_distance_records()
                _save_distance_records_home(new_home)
        except Exception as e: errors.append(f"Config save error: {e}")
    if "secrets" in data:
        try:
            sec_path = os.path.join(BASE_DIR, "config", "secrets.json")
            os.makedirs(os.path.dirname(sec_path), exist_ok=True)
            try:
                with open(sec_path, "r") as f: existing = json.load(f)
            except Exception: existing = {}
            for k, v in data["secrets"].items():
                if isinstance(v, list):
                    merged = []
                    for i, item in enumerate(v):
                        if isinstance(item, str) and "****" in item:
                            merged.append(existing.get(k, [None]*(i+1))[i] if i < len(existing.get(k,[])) else item)
                        else: merged.append(item)
                    existing[k] = merged
                elif isinstance(v, str) and "****" in v: pass
                else: existing[k] = v
            _atomic_write_json(sec_path, existing, keep_backup=True)
        except Exception as e: errors.append(f"Secrets save error: {e}")
    try:
        import config as _config; _config.reload()
    except Exception as e: errors.append(f"Reload error: {e}")
    if errors: return jsonify({"ok": False, "errors": errors}), 500
    message = "Config saved and reloaded"
    if home_location_changed:
        message += "; closest/farthest records reset for new home location"
    return jsonify({"ok": True, "message": message, "home_location_changed": home_location_changed})

@app.post("/api/restart")
def api_restart():
    def _do_restart():
        import time; time.sleep(0.5)
        result = subprocess.run(["sudo", "/bin/systemctl", "restart", "its-a-plane"], capture_output=True)
        if result.returncode != 0:
            open(os.path.join(BASE_DIR, ".restart_requested"), "w").close()
    threading.Thread(target=_do_restart, daemon=True).start()
    return jsonify({"ok": True})


@app.get("/api/update/status")
def api_update_status():
    try:
        from utilities import updater
        return jsonify({"ok": True, "update": updater.status()})
    except Exception as e:
        return jsonify({"ok": False, "error": str(e)}), 500


@app.post("/api/update/check")
def api_update_check():
    try:
        from utilities import updater
        return jsonify(updater.check_for_update())
    except Exception as e:
        try:
            from utilities import updater
            updater.record_progress(f"update check failed: {e}")
        except Exception:
            pass
        return jsonify({"ok": False, "error": str(e)}), 500


def _restart_after_update():
    import time
    time.sleep(1.5)
    result = subprocess.run(["sudo", "/bin/systemctl", "restart", "its-a-plane"], capture_output=True)
    if result.returncode != 0:
        try:
            from utilities import updater
            updater.record_progress("restart requested; use Restart App")
        except Exception:
            pass
        open(os.path.join(BASE_DIR, ".restart_requested"), "w").close()


@app.post("/api/update/install")
def api_update_install():
    try:
        from utilities import updater
        if not updater.status().get("enabled"):
            return jsonify({"ok": False, "error": "Enable remote updates and save the manifest URL first"}), 400
    except Exception as e:
        return jsonify({"ok": False, "error": str(e)}), 500

    def _run():
        try:
            from utilities import updater

            def progress(message, percent=None):
                updater.record_progress(message, percent)

            updater.install_update(progress=progress)
            updater.record_progress("update installed; restarting", 100)
            _restart_after_update()
        except Exception as e:
            try:
                from utilities import updater
                updater.record_progress(f"update failed: {e}")
            except Exception:
                pass

    threading.Thread(target=_run, daemon=True).start()
    return jsonify({"ok": True, "message": "Update install started"})


@app.post("/api/update/rollback")
def api_update_rollback():
    try:
        from utilities import updater
        if not updater.status().get("rollback_available"):
            return jsonify({"ok": False, "error": "No rollback backup is available"}), 400
    except Exception as e:
        return jsonify({"ok": False, "error": str(e)}), 500

    def _run():
        try:
            from utilities import updater

            def progress(message, percent=None):
                updater.record_progress(message, percent)

            updater.rollback(progress=progress)
            updater.record_progress("rollback complete; restarting", 100)
            _restart_after_update()
        except Exception as e:
            try:
                from utilities import updater
                updater.record_progress(f"rollback failed: {e}")
            except Exception:
                pass

    threading.Thread(target=_run, daemon=True).start()
    return jsonify({"ok": True, "message": "Rollback started"})

@app.get("/api/wifi/status")
def api_wifi_status():
    return jsonify(_wifi_status())

@app.get("/api/wifi/scan")
def api_wifi_scan():
    return jsonify(_wifi_scan())

@app.get("/api/wifi/deep-scan")
def api_wifi_deep_scan():
    return jsonify(_wifi_deep_scan())

@app.post("/api/wifi/connect")
def api_wifi_connect():
    if not _nmcli_available():
        return jsonify({
            "ok": False,
            "message": "NetworkManager/nmcli is not available. Enable NetworkManager before using web Wi-Fi switching.",
        }), 503

    data = request.get_json(force=True)
    ssid = (data.get("ssid") or "").strip()
    password = data.get("password") or ""
    if not ssid:
        return jsonify({"ok": False, "message": "SSID is required."}), 400

    def _connect():
        try:
            from utilities.wifi_setup_mode import connect_to_wifi
            result = connect_to_wifi(ssid, password)
            log_path = os.path.join(BASE_DIR, "wifi_connect.log")
            with open(log_path, "a", encoding="utf-8") as f:
                stamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
                message = result.get("message", "Unknown result")
                f.write(f"{stamp}  SSID={ssid!r}  {message}\n")
        except Exception as e:
            try:
                with open(os.path.join(BASE_DIR, "wifi_connect.log"), "a", encoding="utf-8") as f:
                    stamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
                    f.write(f"{stamp}  SSID={ssid!r}  error: {e}\n")
            except Exception:
                pass

    threading.Thread(target=_connect, daemon=True).start()
    return jsonify({
        "ok": True,
        "message": (
            f"Starting connection to {ssid}. The setup network will disappear while the Pi connects. "
            "Join your home Wi-Fi on this device, then connect to the address on the card supplied "
            "with this display, for example http://your-device-name.local:8080. "
            "If connection fails, the PlaneTracker setup network will return in about 10 seconds."
        ),
    })

@app.get("/api/wifi/setup")
def api_wifi_setup():
    try:
        from utilities.wifi_setup_mode import status
        return jsonify({"available": True, **status()})
    except Exception as e:
        return jsonify({"available": False, "active": False, "message": str(e)})

@app.get("/api/wifi/connect-result")
def api_wifi_connect_result():
    try:
        from utilities.wifi_setup_mode import connection_result
        return jsonify(connection_result())
    except Exception as e:
        return jsonify({"ok": False, "state": "error", "message": str(e)})

@app.get("/api/system")
def api_system():
    import os
    result = {}
    try:
        with open("/sys/class/thermal/thermal_zone0/temp") as f:
            result["cpu_temp_c"] = round(int(f.read().strip()) / 1000, 1)
    except Exception: result["cpu_temp_c"] = None
    try:
        # Added back here to pull the first value from the split list
        with open("/proc/uptime") as f:
            result["uptime_secs"] = int(float(f.read().split()[0]))
    except Exception: result["uptime_secs"] = None
    try:
        # Copy the existing environment and safely add/override TZ and LANG
        custom_env = os.environ.copy()
        custom_env["LANG"] = "C"
        custom_env["TZ"] = "UTC"
        
        r = subprocess.run(["systemctl", "show", "its-a-plane", "--property=ActiveEnterTimestamp"],
                           capture_output=True, text=True, timeout=3, env=custom_env)
        ts_str = r.stdout.strip().replace("ActiveEnterTimestamp=", "")
        if ts_str and ts_str != "n/a":
            from datetime import timezone as _tz
            dt = datetime.strptime(ts_str, "%a %Y-%m-%d %H:%M:%S UTC").replace(tzinfo=_tz.utc)
            result["service_uptime_secs"] = int((datetime.now(_tz.utc) - dt).total_seconds())
        else: result["service_uptime_secs"] = None
    except Exception as e: 
        print(f"Uptime parse error: {e}")
        result["service_uptime_secs"] = None
    return jsonify(result)

@app.get("/api/usage")
def api_usage():
    try:
        with open(os.path.join(BASE_DIR, "api_usage.json"), "r") as f:
            data = json.load(f)
        data.setdefault("FR24Unofficial", 0)
        try:
            from utilities import route_db
            data["RouteCache"] = route_db.stats()
        except Exception:
            data["RouteCache"] = {"count": 0, "hits": 0}
        return jsonify(data)
    except Exception:
        return jsonify({"FlightStats": 0, "AirLabs": 0, "FlightAware": 0.0, "FR24Unofficial": 0, "RouteCache": {"count": 0, "hits": 0}})

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8080, debug=False, use_reloader=False)
