#!/usr/bin/env python3
"""
Abflug Client GUI v2 - Graphical interface for forwarding Aerofly GPS data

A redesigned GUI wrapper around abflug_client.py with a two-column layout.

Includes ATC Mode with fixed position functionality.
When ATC Mode is enabled, the client sends a fixed position instead of simulator GPS data.
This is intended for control tower operators who use the ATC_[ICAO]_[FUNCTION] callsign format.
"""

import tkinter as tk
from tkinter import ttk, font
import threading
import time
from typing import Optional
import sys
import os
import socket
import requests
import json

# Import the AbflugClient class
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))

from terminal_debug import load_config_terminal_debug_spec

load_config_terminal_debug_spec()

from abflug_client_web import AbflugClient, DEFAULT_AEROFLY_PORT
from simulator_data_source import DataSourceType
from config_utils import get_butter_api_key, get_server_url, get_debug_api_requests, compute_own_icao
from dpi_utils import configure_tk_dpi, get_dpi_scale_factor, get_safe_window_geometry

# Constants
DEFAULT_SERVER_URL = get_server_url()  # Get from config file
STATUS_UPDATE_INTERVAL = 1000  # milliseconds
HEALTH_CHECK_INTERVAL = 5000  # milliseconds
APP_NAME = "Abflug Client"
APP_VERSION = "3.1.1"

# Debug flag for API request/response logging
DEBUG_API_REQUESTS = get_debug_api_requests()  # Set to False to disable debug messages


class AbflugClientGUI:
    """GUI application for Abflug Client. Can run standalone or embedded in a parent (e.g. tab)."""
    
    def __init__(self, container, shared_data_source=None, on_connected=None, get_simulator_data_source=None):
        """
        Args:
            container: tk.Tk (standalone) or ttk.Frame (embedded). UI is built inside this.
            shared_data_source: Optional SimulatorDataSource to share (unified app). Caller owns lifecycle.
            on_connected: Optional callback(server_url, api_key, **kwargs). Unified app may pass
                observer_lat= and observer_lon= when ATC mode is on (Map tab traffic uses fixed position).
            get_simulator_data_source: Optional callback(use_fswidget, fswidget_ip, auto_detect) -> SimulatorDataSource.
                When provided (unified app), called when user clicks Connect to get/create data source with their selection.
        """
        self.container = container
        self.root = container.winfo_toplevel()
        self.shared_data_source = shared_data_source
        self.on_connected = on_connected
        self.get_simulator_data_source = get_simulator_data_source
        
        # Configure DPI scaling for Windows
        self.scale_factor = configure_tk_dpi(self.root)
        
        # When standalone (container is root), set geometry and title
        if isinstance(container, tk.Tk):
            self.root.title(APP_NAME)
            safe_geometry = get_safe_window_geometry(self.root, 1000, 900, self.scale_factor, center=True)
            self.root.geometry(safe_geometry)
            self.root.resizable(True, True)
        
        # Client instance
        self.client: Optional[AbflugClient] = None
        self.client_thread: Optional[threading.Thread] = None
        self.is_connected = False

        # UDP monitoring (Aerofly) – independent from server connection
        self.udp_socket: Optional[socket.socket] = None
        self.udp_listener_thread: Optional[threading.Thread] = None
        self.udp_monitor_running: bool = False
        self.last_udp_time: float = 0.0
        
        # Status tracking
        self.last_health_check = 0
        self.health_status = "Unknown"
        self.last_api_key_check = 0
        self.api_key_valid = False
        self.server_online = False
        
        # One-time "Connected to simulator" message (avoids confusion)
        self.sim_connected_message_shown = False
        
        # Traffic polling for callsign retrieval
        self.own_icao: Optional[str] = None
        self.traffic_poll_thread: Optional[threading.Thread] = None
        self.traffic_poll_running = False
        self.server_callsign: Optional[str] = None
        self.traffic_poll_interval = 5.0  # seconds between traffic polls
        
        # Squawk code fields (API v2.2)
        self.squawk_code_input: Optional[tk.Entry] = None
        self.transponder_display: Optional[tk.Entry] = None
        self.current_squawk_code = "0000"  # Current squawk code being sent
        
        # Flight plan route (API v2.2)
        self.flightplan_route_text: Optional[tk.Text] = None
        self.flightplan_route_sanitized: Optional[str] = None
        self.flightplan_finalized = False  # True once flightplan is sent at connection
        
        # ATC Mode - Fixed Position (ATC version)
        self.atc_mode_enabled = False
        self.atc_latitude: Optional[float] = None
        self.atc_longitude: Optional[float] = None
        self.atc_altitude: float = 0.0
        self.atc_heading: float = 0.0
        self.atc_ground_speed: float = 0.0
        self.atc_vertical_speed: float = 0.0
        
        self.setup_ui()
        self.start_udp_monitor()
        self.update_status_loop()
    
    def setup_ui(self):
        """Set up the user interface with two-column layout."""
        # Main frame with padding (packs into container)
        main_frame = ttk.Frame(self.container, padding="10")
        main_frame.pack(fill="both", expand=True)
        
        # Title
        title_label = tk.Label(
            main_frame,
            text=f"{APP_NAME} v{APP_VERSION}",
            font=("Arial", 16, "bold")
        )
        title_label.pack(pady=(0, 10))
        
        # Create two-column layout
        content_frame = ttk.Frame(main_frame)
        content_frame.pack(fill="both", expand=True)
        
        # Left column - Configuration (with scrollbar for ATC fields)
        left_column_container = ttk.Frame(content_frame)
        left_column_container.pack(side="left", fill="both", expand=True, padx=(0, 10))
        
        # Create canvas and scrollbar for scrollable left column
        self.left_canvas = tk.Canvas(left_column_container, highlightthickness=0)
        left_scrollbar = ttk.Scrollbar(left_column_container, orient="vertical", command=self.left_canvas.yview)
        left_column = ttk.Frame(self.left_canvas)
        
        def update_scroll_region(event=None):
            self.left_canvas.configure(scrollregion=self.left_canvas.bbox("all"))
        
        left_column.bind("<Configure>", update_scroll_region)
        
        canvas_window = self.left_canvas.create_window((0, 0), window=left_column, anchor="nw")
        self.left_canvas.configure(yscrollcommand=left_scrollbar.set)
        
        # Update canvas window width when canvas is resized
        def configure_canvas_width(event):
            canvas_width = event.width
            self.left_canvas.itemconfig(canvas_window, width=canvas_width)
        self.left_canvas.bind('<Configure>', configure_canvas_width)
        
        self.left_canvas.pack(side="left", fill="both", expand=True)
        left_scrollbar.pack(side="right", fill="y")
        
        # Bind mousewheel to canvas (only when mouse is over the canvas)
        def _on_mousewheel(event):
            self.left_canvas.yview_scroll(int(-1*(event.delta/120)), "units")
        self.left_canvas.bind("<MouseWheel>", _on_mousewheel)
        # Also bind to the frame for better UX
        left_column.bind("<MouseWheel>", _on_mousewheel)
        
        # Right column - Status indicators
        right_column = ttk.Frame(content_frame)
        right_column.pack(side="right", fill="y", padx=(10, 0))
        
        # ========== LEFT COLUMN ==========
        
        # Server Configuration (API v2.2 redesign: with status dot)
        server_frame = ttk.LabelFrame(left_column, text="Server Configuration", padding="10")
        server_frame.pack(fill="x", pady=(0, 10))
        
        # Status row with dot and text
        server_status_row = ttk.Frame(server_frame)
        server_status_row.grid(row=0, column=0, columnspan=2, sticky="w", pady=5)
        ttk.Label(server_status_row, text="Status:").pack(side="left", padx=(0, 5))
        self.server_status_dot_left = tk.Canvas(server_status_row, width=20, height=20, highlightthickness=0)
        self.server_status_dot_left.pack(side="left", padx=5)
        self.server_status_label_left = ttk.Label(server_status_row, text="Not connected")
        self.server_status_label_left.pack(side="left", padx=5)
        
        # Server URL
        ttk.Label(server_frame, text="Server URL:").grid(row=1, column=0, sticky="w", pady=5)
        self.server_url_entry = ttk.Entry(server_frame, width=30)
        self.server_url_entry.insert(0, DEFAULT_SERVER_URL)
        self.server_url_entry.grid(row=1, column=1, padx=5, pady=5, sticky="ew")
        
        # API Key
        ttk.Label(server_frame, text="API Key:").grid(row=2, column=0, sticky="w", pady=5)
        self.api_key_entry = ttk.Entry(server_frame, width=30, show="*")
        self.api_key_entry.grid(row=2, column=1, padx=5, pady=5, sticky="ew")
        # Try to load API key from butter.json
        butter_api_key = get_butter_api_key()
        if butter_api_key:
            self.api_key_entry.insert(0, butter_api_key)
        
        # Note: Callsign removed from Server Configuration (API v2.2 requirement)
        
        server_frame.columnconfigure(1, weight=1)
        
        # Simulator Data Source (API v2.2 redesign: with status dot, FSWidget TCP as default)
        simulator_frame = ttk.LabelFrame(left_column, text="Simulator Data Source", padding="10")
        simulator_frame.pack(fill="x", pady=(0, 10))
        
        # Status row with dot and text
        sim_status_row = ttk.Frame(simulator_frame)
        sim_status_row.grid(row=0, column=0, columnspan=2, sticky="w", pady=5)
        ttk.Label(sim_status_row, text="Status:").pack(side="left", padx=(0, 5))
        self.sim_status_dot = tk.Canvas(sim_status_row, width=20, height=20, highlightthickness=0)
        self.sim_status_dot.pack(side="left", padx=5)
        self.sim_status_label = ttk.Label(sim_status_row, text="Waiting for data...")
        self.sim_status_label.pack(side="left", padx=5)
        
        # Auto-detect checkbox (moved to top, API v2.2 requirement)
        self.auto_detect_var = tk.BooleanVar(value=False)
        self.auto_detect_checkbox = tk.Checkbutton(
            simulator_frame,
            text="Autodetect simulator IP",
            variable=self.auto_detect_var
        )
        self.auto_detect_checkbox.grid(row=1, column=0, columnspan=2, sticky="w", pady=2)
        
        # Radio buttons (reordered: FSWidget TCP first as default, API v2.2 requirement)
        self.data_source_var = tk.StringVar(value="fswidget")  # Default changed to FSWidget TCP
        self.fswidget_radio = ttk.Radiobutton(
            simulator_frame,
            text="FSWidget TCP",
            variable=self.data_source_var,
            value="fswidget"
        )
        self.fswidget_radio.grid(row=2, column=0, columnspan=2, sticky="w", pady=2)
        
        self.udp_radio = ttk.Radiobutton(
            simulator_frame,
            text="Aerofly UDP",
            variable=self.data_source_var,
            value="udp"
        )
        self.udp_radio.grid(row=3, column=0, columnspan=2, sticky="w", pady=2)
        
        # Simulator IP address field
        fswidget_frame = ttk.Frame(simulator_frame)
        fswidget_frame.grid(row=4, column=0, columnspan=2, sticky="w", pady=5)
        ttk.Label(fswidget_frame, text="Simulator IP:").pack(side="left", padx=5)
        self.fswidget_ip_entry = ttk.Entry(fswidget_frame, width=20)
        self.fswidget_ip_entry.insert(0, "localhost")
        self.fswidget_ip_entry.pack(side="left", padx=5)
        
        # Update FSWidget field state based on selection and auto-detect
        def update_fswidget_field_state(*args):
            auto_detect = self.auto_detect_var.get()
            if self.data_source_var.get() == "fswidget":
                if auto_detect:
                    self.fswidget_ip_entry.config(state="disabled")
                    self.fswidget_ip_entry.delete(0, tk.END)
                    self.fswidget_ip_entry.insert(0, "auto-detecting...")
                else:
                    self.fswidget_ip_entry.config(state="normal")
            else:
                self.fswidget_ip_entry.config(state="disabled")
        
        self.data_source_var.trace_add("write", update_fswidget_field_state)
        self.auto_detect_var.trace_add("write", update_fswidget_field_state)
        update_fswidget_field_state()  # Initial state
        
        # ATC Mode - Fixed Position (ATC version)
        atc_frame = ttk.LabelFrame(left_column, text="ATC Mode - Fixed Position", padding="10")
        atc_frame.pack(fill="x", pady=(0, 10))
        
        # ATC Mode toggle checkbox
        self.atc_mode_var = tk.BooleanVar(value=False)
        atc_checkbox = tk.Checkbutton(
            atc_frame,
            text="Enable ATC Mode (Fixed Position)",
            variable=self.atc_mode_var,
            command=self.toggle_atc_mode
        )
        atc_checkbox.grid(row=0, column=0, columnspan=2, sticky="w", pady=5)
        
        # ATC Position input fields (initially disabled)
        atc_inputs_frame = ttk.Frame(atc_frame)
        atc_inputs_frame.grid(row=1, column=0, columnspan=2, sticky="ew", pady=5)
        atc_frame.columnconfigure(0, weight=1)
        
        # Latitude
        ttk.Label(atc_inputs_frame, text="Latitude:").grid(row=0, column=0, sticky="w", padx=5, pady=2)
        self.atc_latitude_entry = ttk.Entry(atc_inputs_frame, width=20)
        self.atc_latitude_entry.grid(row=0, column=1, padx=5, pady=2, sticky="ew")
        self.atc_latitude_entry.insert(0, "0.0")
        self.atc_latitude_entry.config(state="disabled")
        
        # Longitude
        ttk.Label(atc_inputs_frame, text="Longitude:").grid(row=1, column=0, sticky="w", padx=5, pady=2)
        self.atc_longitude_entry = ttk.Entry(atc_inputs_frame, width=20)
        self.atc_longitude_entry.grid(row=1, column=1, padx=5, pady=2, sticky="ew")
        self.atc_longitude_entry.insert(0, "0.0")
        self.atc_longitude_entry.config(state="disabled")
        
        # Altitude
        ttk.Label(atc_inputs_frame, text="Altitude (ft):").grid(row=2, column=0, sticky="w", padx=5, pady=2)
        self.atc_altitude_entry = ttk.Entry(atc_inputs_frame, width=20)
        self.atc_altitude_entry.grid(row=2, column=1, padx=5, pady=2, sticky="ew")
        self.atc_altitude_entry.insert(0, "0")
        self.atc_altitude_entry.config(state="disabled")
        
        # Heading
        ttk.Label(atc_inputs_frame, text="Heading (°):").grid(row=3, column=0, sticky="w", padx=5, pady=2)
        self.atc_heading_entry = ttk.Entry(atc_inputs_frame, width=20)
        self.atc_heading_entry.grid(row=3, column=1, padx=5, pady=2, sticky="ew")
        self.atc_heading_entry.insert(0, "0")
        self.atc_heading_entry.config(state="disabled")
        
        # Ground Speed
        ttk.Label(atc_inputs_frame, text="Ground Speed (kts):").grid(row=4, column=0, sticky="w", padx=5, pady=2)
        self.atc_ground_speed_entry = ttk.Entry(atc_inputs_frame, width=20)
        self.atc_ground_speed_entry.grid(row=4, column=1, padx=5, pady=2, sticky="ew")
        self.atc_ground_speed_entry.insert(0, "0")
        self.atc_ground_speed_entry.config(state="disabled")
        
        atc_inputs_frame.columnconfigure(1, weight=1)
        
        # Note about callsign format
        note_label = ttk.Label(
            atc_frame,
            text="Note: Use ATC_[ICAO]_[FUNCTION] format in callsign (e.g., ATC_EDDF_GROUND)",
            font=("Arial", 8),
            foreground="gray"
        )
        note_label.grid(row=2, column=0, columnspan=2, sticky="w", padx=5, pady=5)
        
        # Squawk Code section (API v2.2)
        squawk_frame = ttk.LabelFrame(left_column, text="Squawk Code", padding="10")
        squawk_frame.pack(fill="x", pady=(0, 10))
        
        # Editable squawk code input field
        squawk_input_frame = ttk.Frame(squawk_frame)
        squawk_input_frame.pack(fill="x", pady=5)
        ttk.Label(squawk_input_frame, text="Squawk Code:").pack(side="left", padx=5)
        self.squawk_code_input = ttk.Entry(squawk_input_frame, width=15)
        self.squawk_code_input.pack(side="left", padx=5)
        self.squawk_apply_button = ttk.Button(
            squawk_input_frame,
            text="Apply",
            command=self.apply_squawk_code,
            width=10
        )
        self.squawk_apply_button.pack(side="left", padx=5)
        
        # Transponder display (blocked field, default 0000)
        transponder_frame = ttk.Frame(squawk_frame)
        transponder_frame.pack(fill="x", pady=5)
        ttk.Label(transponder_frame, text="Transponder:").pack(side="left", padx=5)
        self.transponder_display = ttk.Entry(transponder_frame, width=15, state="readonly")
        self.transponder_display.pack(side="left", padx=5)
        self.transponder_display.config(state="normal")
        self.transponder_display.insert(0, "0000")
        self.transponder_display.config(state="readonly")
        
        # Aviator Ledger CSV (import file beside executable / script dir)
        ledger_frame = ttk.LabelFrame(left_column, text="Aviator Ledger", padding="10")
        ledger_frame.pack(fill="x", pady=(0, 10))

        self.aviator_ledger_var = tk.BooleanVar(value=False)
        self.aviator_ledger_checkbox = tk.Checkbutton(
            ledger_frame,
            text="Create Aviator ledger import file (CSV on depart / land)",
            variable=self.aviator_ledger_var,
        )
        self.aviator_ledger_checkbox.pack(anchor="w", pady=2)
        
        # ========== RIGHT COLUMN ==========
        
        # Flight Plan section (moved to right column, API v2.2 requirement)
        flight_plan_frame = ttk.LabelFrame(right_column, text="Flight Plan", padding="10")
        flight_plan_frame.pack(fill="both", expand=True, pady=(0, 10))
        
        # Departure and Arrival airports
        dep_arr_frame = ttk.Frame(flight_plan_frame)
        dep_arr_frame.pack(fill="x", pady=5)
        dep_arr_frame.columnconfigure(0, minsize=120)  # Fixed width for label column
        dep_arr_frame.columnconfigure(2, minsize=120)  # Fixed width for second label column
        ttk.Label(dep_arr_frame, text="Departure ARPT:").grid(row=0, column=0, sticky="e", padx=5, pady=2)
        self.departure_airport_entry = ttk.Entry(dep_arr_frame, width=12)
        self.departure_airport_entry.grid(row=0, column=1, padx=5, pady=2, sticky="w")
        ttk.Label(dep_arr_frame, text="Arrival ARPT:").grid(row=0, column=2, sticky="e", padx=(15, 5), pady=2)
        self.arrival_airport_entry = ttk.Entry(dep_arr_frame, width=12)
        self.arrival_airport_entry.grid(row=0, column=3, padx=5, pady=2, sticky="w")
        
        # Aircraft model and Airline
        aircraft_frame = ttk.Frame(flight_plan_frame)
        aircraft_frame.pack(fill="x", pady=5)
        aircraft_frame.columnconfigure(0, minsize=120)  # Fixed width for label column
        aircraft_frame.columnconfigure(2, minsize=120)  # Fixed width for second label column
        ttk.Label(aircraft_frame, text="Aircraft model:").grid(row=0, column=0, sticky="e", padx=5, pady=2)
        self.airplane_model_entry = ttk.Entry(aircraft_frame, width=12)
        self.airplane_model_entry.grid(row=0, column=1, padx=5, pady=2, sticky="w")
        ttk.Label(aircraft_frame, text="Airline:").grid(row=0, column=2, sticky="e", padx=(15, 5), pady=2)
        self.airplane_livery_entry = ttk.Entry(aircraft_frame, width=12)
        self.airplane_livery_entry.grid(row=0, column=3, padx=5, pady=2, sticky="w")
        
        # Callsign and Cruise Altitude
        callsign_alt_frame = ttk.Frame(flight_plan_frame)
        callsign_alt_frame.pack(fill="x", pady=5)
        callsign_alt_frame.columnconfigure(0, minsize=120)  # Fixed width for label column
        callsign_alt_frame.columnconfigure(2, minsize=120)  # Fixed width for second label column
        ttk.Label(callsign_alt_frame, text="Callsign:").grid(row=0, column=0, sticky="e", padx=5, pady=2)
        self.callsign_entry = ttk.Entry(callsign_alt_frame, width=12)
        self.callsign_entry.grid(row=0, column=1, padx=5, pady=2, sticky="w")
        ttk.Label(callsign_alt_frame, text="Cruise Alt:").grid(row=0, column=2, sticky="e", padx=(15, 5), pady=2)
        self.cruise_altitude_entry = ttk.Entry(callsign_alt_frame, width=12)
        self.cruise_altitude_entry.grid(row=0, column=3, padx=5, pady=2, sticky="w")
        
        # Flight plan route text frame (API v2.2)
        route_label_frame = ttk.Frame(flight_plan_frame)
        route_label_frame.pack(fill="x", pady=(10, 5))
        ttk.Label(route_label_frame, text="Flight plan route:").pack(anchor="w", padx=5)
        
        route_text_frame = ttk.Frame(flight_plan_frame)
        route_text_frame.pack(fill="both", expand=True, pady=5)
        
        self.flightplan_route_text = tk.Text(
            route_text_frame,
            height=6,
            width=40,
            wrap=tk.WORD,
            font=("Consolas", 9)
        )
        route_scrollbar = ttk.Scrollbar(route_text_frame, orient="vertical", command=self.flightplan_route_text.yview)
        self.flightplan_route_text.configure(yscrollcommand=route_scrollbar.set)
        self.flightplan_route_text.pack(side="left", fill="both", expand=True)
        route_scrollbar.pack(side="right", fill="y")
        
        # Save, Connect, Clear Log buttons (grouped so user doesn't have to scroll)
        route_button_frame = ttk.Frame(flight_plan_frame)
        route_button_frame.pack(fill="x", pady=5)
        ttk.Button(
            route_button_frame,
            text="Save",
            command=self.save_flightplan_route,
            width=15
        ).pack(side="left", padx=5)
        self.connect_button = ttk.Button(
            route_button_frame,
            text="Connect",
            command=self.toggle_connection,
            width=15
        )
        self.connect_button.pack(side="left", padx=5)
        ttk.Button(
            route_button_frame,
            text="Clear Log",
            command=self.clear_status,
            width=15
        ).pack(side="left", padx=5)
        
        # ========== BOTTOM SECTION (SPANS COLUMNS) ==========
        
        # Status Messages (spans across columns, API v2.2 requirement)
        status_messages_frame = ttk.LabelFrame(main_frame, text="Status Messages", padding="10")
        status_messages_frame.pack(fill="both", expand=True, pady=(10, 10))
        
        # Create text widget with scrollbar
        text_frame = ttk.Frame(status_messages_frame)
        text_frame.pack(fill="both", expand=True)
        
        self.status_text = tk.Text(
            text_frame,
            height=8,
            width=80,
            wrap=tk.WORD,
            font=("Consolas", 9),
            state=tk.DISABLED
        )
        scrollbar = ttk.Scrollbar(text_frame, orient="vertical", command=self.status_text.yview)
        self.status_text.configure(yscrollcommand=scrollbar.set)
        
        self.status_text.pack(side="left", fill="both", expand=True)
        scrollbar.pack(side="right", fill="y")
        
        # Initialize status dots (API v2.2: dots in left column)
        self.update_sim_status(False, "Waiting for data...")
        self.update_server_status_left(False, "Not connected")
        
        # Initialize status dots for right column (keep for backward compatibility during transition)
        self.update_udp_status(False, "Waiting for data...")
        self.update_server_status(False, "Not connected")
        
        # Add initial status message
        self.add_status_message("Ready. Enter API key and click Connect to start.")
    
    def update_server_status_left(self, connected: bool, message: str = ""):
        """Update server connection status indicator in left column (API v2.2)."""
        if hasattr(self, 'server_status_dot_left'):
            self.server_status_dot_left.delete("all")
            
            if connected:
                # Green dot
                self.server_status_dot_left.create_oval(5, 5, 15, 15, fill="#00ff00", outline="#00aa00")
                status_text = "Connected" if not message else message
            else:
                # Red dot
                self.server_status_dot_left.create_oval(5, 5, 15, 15, fill="#ff0000", outline="#aa0000")
                status_text = "Disconnected" if not message else message
            
            if hasattr(self, 'server_status_label_left'):
                self.server_status_label_left.config(text=status_text)
    
    def update_sim_status(self, receiving: bool, message: str = ""):
        """Update simulator status indicator in left column (API v2.2)."""
        if hasattr(self, 'sim_status_dot'):
            self.sim_status_dot.delete("all")
            
            if receiving:
                # Green dot
                self.sim_status_dot.create_oval(5, 5, 15, 15, fill="#00ff00", outline="#00aa00")
                status_text = "Receiving data" if not message else message
            else:
                # Gray dot
                self.sim_status_dot.create_oval(5, 5, 15, 15, fill="#888888", outline="#666666")
                status_text = "No data" if not message else message
            
            if hasattr(self, 'sim_status_label'):
                self.sim_status_label.config(text=status_text)
    
    def update_udp_status(self, receiving: bool, message: str = ""):
        """Update UDP status indicator (kept for backward compatibility)."""
        if hasattr(self, 'udp_status_dot'):
            self.udp_status_dot.delete("all")
            
            if receiving:
                # Green dot
                self.udp_status_dot.create_oval(5, 5, 15, 15, fill="#00ff00", outline="#00aa00")
                status_text = "Receiving data" if not message else message
            else:
                # Gray dot
                self.udp_status_dot.create_oval(5, 5, 15, 15, fill="#888888", outline="#666666")
                status_text = "No data" if not message else message
            
            if hasattr(self, 'udp_status_label'):
                self.udp_status_label.config(text=status_text)
    
    def update_server_status(self, connected: bool, message: str = ""):
        """Update server connection status indicator (kept for backward compatibility)."""
        if hasattr(self, 'server_status_dot'):
            self.server_status_dot.delete("all")
            
            if connected:
                # Green dot
                self.server_status_dot.create_oval(5, 5, 15, 15, fill="#00ff00", outline="#00aa00")
                status_text = "Connected" if not message else message
            else:
                # Red dot
                self.server_status_dot.create_oval(5, 5, 15, 15, fill="#ff0000", outline="#aa0000")
                status_text = "Disconnected" if not message else message
            
            if hasattr(self, 'server_status_label'):
                self.server_status_label.config(text=status_text)
    
    def apply_squawk_code(self):
        """Apply squawk code: validate, pad, and update transponder display (API v2.2)."""
        raw_squawk = self.squawk_code_input.get().strip() if self.squawk_code_input else ""
        
        # Validate and pad squawk code
        #from abflug_client_web import AbflugClient
        validated_squawk = AbflugClient.validate_and_pad_squawk_code(raw_squawk)
        
        # Update transponder display (blocked field)
        if self.transponder_display:
            self.transponder_display.config(state="normal")
            self.transponder_display.delete(0, tk.END)
            self.transponder_display.insert(0, validated_squawk)
            self.transponder_display.config(state="readonly")
        
        # Update current squawk code (will be sent with position updates)
        self.current_squawk_code = validated_squawk
        
        # Update client if connected
        if self.client and self.is_connected:
            self.client.update_squawk_code(validated_squawk)
        
        # Write to status message frame (API v2.2 requirement)
        self.add_status_message(f"Squawk code applied: {validated_squawk}")
    
    def toggle_atc_mode(self):
        """Toggle ATC Mode: enable/disable fixed position mode and update UI state."""
        self.atc_mode_enabled = self.atc_mode_var.get()
        
        # Enable/disable ATC input fields
        state = "normal" if self.atc_mode_enabled else "disabled"
        self.atc_latitude_entry.config(state=state)
        self.atc_longitude_entry.config(state=state)
        self.atc_altitude_entry.config(state=state)
        self.atc_heading_entry.config(state=state)
        self.atc_ground_speed_entry.config(state=state)
        
        # Disable/enable simulator data source options when ATC mode is enabled
        if self.atc_mode_enabled:
            # Disable simulator options
            self.fswidget_radio.config(state="disabled")
            self.udp_radio.config(state="disabled")
            self.auto_detect_checkbox.config(state="disabled")
            self.fswidget_ip_entry.config(state="disabled")
            # Disable squawk code fields
            if hasattr(self, 'squawk_code_input'):
                self.squawk_code_input.config(state="disabled")
            if hasattr(self, 'squawk_apply_button'):
                self.squawk_apply_button.config(state="disabled")
            if hasattr(self, 'transponder_display'):
                # Keep transponder display readonly but visually indicate it's disabled
                pass  # Already readonly
            # Disable Discord webhook fields
            if hasattr(self, 'aviator_ledger_checkbox'):
                self.aviator_ledger_checkbox.config(state="disabled")
            # Update status to show ATC mode
            self.update_sim_status(True, "ATC Mode: Fixed Position")
            self.add_status_message("ATC Mode enabled - Using fixed position")
        else:
            # Re-enable simulator options
            self.fswidget_radio.config(state="normal")
            self.udp_radio.config(state="normal")
            self.auto_detect_checkbox.config(state="normal")
            # Update FSWidget field state based on current selection
            auto_detect = self.auto_detect_var.get()
            if self.data_source_var.get() == "fswidget":
                if auto_detect:
                    self.fswidget_ip_entry.config(state="disabled")
                else:
                    self.fswidget_ip_entry.config(state="normal")
            else:
                self.fswidget_ip_entry.config(state="disabled")
            # Re-enable squawk code fields
            if hasattr(self, 'squawk_code_input'):
                self.squawk_code_input.config(state="normal")
            if hasattr(self, 'squawk_apply_button'):
                self.squawk_apply_button.config(state="normal")
            # Re-enable Discord webhook fields
            if hasattr(self, 'aviator_ledger_checkbox'):
                self.aviator_ledger_checkbox.config(state="normal")
            # Update status back to normal
            self.update_sim_status(False, "Waiting for data...")
            self.add_status_message("ATC Mode disabled - Using simulator data source")
    
    def validate_atc_position(self) -> tuple[bool, Optional[str]]:
        """
        Validate ATC position input fields.
        
        Returns:
            Tuple of (is_valid, error_message)
        """
        if not self.atc_mode_enabled:
            return True, None
        
        try:
            # Validate latitude
            lat_str = self.atc_latitude_entry.get().strip()
            if not lat_str:
                return False, "Latitude is required"
            latitude = float(lat_str)
            if not (-90.0 <= latitude <= 90.0):
                return False, "Latitude must be between -90.0 and 90.0"
            
            # Validate longitude
            lon_str = self.atc_longitude_entry.get().strip()
            if not lon_str:
                return False, "Longitude is required"
            longitude = float(lon_str)
            if not (-180.0 <= longitude <= 180.0):
                return False, "Longitude must be between -180.0 and 180.0"
            
            # Validate altitude (optional, defaults to 0)
            alt_str = self.atc_altitude_entry.get().strip()
            altitude = float(alt_str) if alt_str else 0.0
            
            # Validate heading (optional, defaults to 0)
            heading_str = self.atc_heading_entry.get().strip()
            heading = float(heading_str) if heading_str else 0.0
            if not (0.0 <= heading <= 360.0):
                return False, "Heading must be between 0.0 and 360.0"
            
            # Validate ground speed (optional, defaults to 0)
            speed_str = self.atc_ground_speed_entry.get().strip()
            ground_speed = float(speed_str) if speed_str else 0.0
            if ground_speed < 0.0:
                return False, "Ground speed must be >= 0.0"
            
            # Store validated values
            self.atc_latitude = latitude
            self.atc_longitude = longitude
            self.atc_altitude = altitude
            self.atc_heading = heading
            self.atc_ground_speed = ground_speed
            self.atc_vertical_speed = 0.0  # Always 0 for fixed position
            
            return True, None
            
        except ValueError as e:
            return False, f"Invalid number format: {str(e)}"
    
    def save_flightplan_route(self):
        """Save flight plan route: sanitize and update (API v2.2)."""
        if self.flightplan_finalized:
            self.add_status_message("Flight plan route is locked (already sent at connection)")
            return
        
        raw_route = self.flightplan_route_text.get("1.0", tk.END).strip() if self.flightplan_route_text else ""
        
        # Sanitize flight plan route
        #from abflug_client_web import AbflugClient
        sanitized_route = AbflugClient.sanitize_flightplan_route(raw_route)
        
        # Store sanitized route
        self.flightplan_route_sanitized = sanitized_route
        
        # Update client if connected (but route will be sent at connection)
        if self.client and self.is_connected:
            self.client.update_flightplan_route(sanitized_route)
            # Re-register flight plan with new route
            if hasattr(self.client, 'register_flight_plan'):
                self.client.register_flight_plan()
        
        # Write to status message frame
        if sanitized_route == "NO DATA":
            self.add_status_message(f"Flight plan route saved: {sanitized_route} (sanitized from input)")
        else:
            self.add_status_message(f"Flight plan route saved: {sanitized_route[:50]}...")
    
    def add_status_message(self, message: str):
        """Add a message to the status text box."""
        self.status_text.config(state=tk.NORMAL)
        timestamp = time.strftime("%H:%M:%S")
        self.status_text.insert(tk.END, f"[{timestamp}] {message}\n")
        self.status_text.see(tk.END)
        self.status_text.config(state=tk.DISABLED)
    
    def clear_status(self):
        """Clear the status text box."""
        self.status_text.config(state=tk.NORMAL)
        self.status_text.delete(1.0, tk.END)
        self.status_text.config(state=tk.DISABLED)

    def start_udp_monitor(self):
        """Start a background listener for Aerofly UDP, independent of server connection."""
        if self.udp_monitor_running:
            return

        try:
            self.udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            self.udp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
            if hasattr(socket, "SO_REUSEPORT"):
                self.udp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
            self.udp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
            self.udp_socket.settimeout(1.0)
            # Listen on the same default Aerofly port used by the client
            self.udp_socket.bind(("", DEFAULT_AEROFLY_PORT))
        except Exception as e:
            self.add_status_message(f"Warning: Could not start Aerofly UDP monitor: {e}")
            if self.udp_socket:
                try:
                    self.udp_socket.close()
                except Exception:
                    pass
                self.udp_socket = None
            return

        self.udp_monitor_running = True

        def _udp_listener():
            while self.udp_monitor_running and self.udp_socket:
                try:
                    # We don't care about the data, just that something arrived
                    self.udp_socket.recvfrom(4096)
                    self.last_udp_time = time.time()
                except socket.timeout:
                    # Normal – just loop again
                    continue
                except OSError:
                    # Socket closed
                    break
                except Exception:
                    # Any other error: keep loop but don't spam logs
                    continue

        self.udp_listener_thread = threading.Thread(target=_udp_listener, daemon=True)
        self.udp_listener_thread.start()

    def stop_udp_monitor(self):
        """Stop the background Aerofly UDP listener."""
        self.udp_monitor_running = False
        if self.udp_socket:
            try:
                self.udp_socket.close()
            except Exception:
                pass
            self.udp_socket = None
    
    def check_server_health(self):
        """Check server health endpoint (API key validity is inferred from successful position updates)."""
        if not self.is_connected or not self.client:
            return
        
        current_time = time.time()
        # Only check every 5 seconds
        if current_time - self.last_health_check < (HEALTH_CHECK_INTERVAL / 1000):
            return
        
        self.last_health_check = current_time
        
        # Check server health (no API key required)
        # Note: API key validity is inferred from successful position updates sent by the client
        # No need to send redundant test PUT requests with zero coordinates
        try:
            response = self.client.http_session.get(
                f"{self.client.server_url}/api/health",
                timeout=5.0
            )
            if response.status_code == 200:
                data = response.json()
                self.health_status = f"OK (uptime: {data.get('uptime_seconds', 0):.0f}s)"
                self.server_online = True
                # If server is online and client is running, assume API key is valid
                # (it was validated at connection time, and ongoing position updates confirm it)
                self.api_key_valid = True
                # Update both left column and old status indicators (API v2.2)
                self.update_server_status_left(True, self.health_status)
                self.update_server_status(True, self.health_status)
            else:
                self.health_status = f"Health check error: {response.status_code}"
                self.server_online = False
                self.update_server_status_left(False, self.health_status)
                self.update_server_status(False, self.health_status)
        except requests.exceptions.RequestException as e:
            # Check if response contains server logs
            if hasattr(e, 'response') and e.response is not None:
                try:
                    response_text = e.response.text[:200]
                    if "INFO:" in response_text:
                        self.add_status_message("ERROR: GET /api/health returned server logs in response!")
                except:
                    pass
            self.health_status = f"Server unreachable: {str(e)[:50]}"
            self.server_online = False
            self.update_server_status_left(False, self.health_status)
            self.update_server_status(False, self.health_status)
        except Exception as e:
            self.health_status = f"Server unreachable: {str(e)[:50]}"
            self.server_online = False
            self.update_server_status_left(False, self.health_status)
            self.update_server_status(False, self.health_status)
    
    def run_client(self):
        """Run the client in a separate thread."""
        try:
            # Start the client (this will block, so it's in a thread)
            # We need to override the start method to not block on the main loop
            self.start_client_non_blocking()
        except Exception as e:
            self.add_status_message(f"Client error: {e}")
            self.is_connected = False
            self.update_ui_state()
    
    def start_client_non_blocking(self):
        """Start client without the blocking main loop."""
        # Check if client exists
        if not self.client:
            self.add_status_message("ERROR: Client not initialized")
            self.is_connected = False
            self.root.after(0, self.update_ui_state)
            return
        
        # Data source initialization is handled by AbflugClient.start()
        
        # Test server connection (health check - no API key needed)
        try:
            self.add_status_message("Making GET /api/health request...")
            test_response = self.client.http_session.get(
                f"{self.client.server_url}/api/health",
                timeout=5.0
            )
            if test_response.status_code == 200:
                self.add_status_message(f"Server is online (status: {test_response.status_code})")
                # Update server status dot to indicate server is online
                self.root.after(0, lambda: self.update_server_status_left(True, "Server online"))
                self.root.after(0, lambda: self.update_server_status(True, "Server online"))
                # Now test API key with a PUT request
                # Use actual GPS position when available (unified app: data source already running)
                # to avoid registering at (0,0) which can cause "own aircraft not found" in traffic
                latest_gps = None
                if self.client.data_source:
                    latest_gps = self.client.data_source.get_latest_gps()
                if latest_gps:
                    test_payload = {
                        "latitude": latest_gps.latitude,
                        "longitude": latest_gps.longitude,
                        "altitude": latest_gps.altitude,
                        "track": latest_gps.track,
                        "ground_speed": latest_gps.ground_speed,
                        "vertical_speed": latest_gps.vertical_speed,
                        "timestamp": time.time()
                    }
                else:
                    test_payload = {
                        "latitude": 0.0,
                        "longitude": 0.0,
                        "altitude": 0.0,
                        "track": 0.0,
                        "ground_speed": 0.0,
                        "vertical_speed": 0.0,  # Required field in API v2.2
                        "timestamp": time.time()
                    }
                self.add_status_message("Making PUT /api/position request to validate API key...")
                
                # Debug: Log the payload being sent
                if DEBUG_API_REQUESTS:
                    payload_json = json.dumps(test_payload, indent=2)
                    self.add_status_message(f"[DEBUG] PUT /api/position payload:\n{payload_json}")
                
                put_response = self.client.http_session.put(
                    f"{self.client.server_url}/api/position",
                    json=test_payload,
                    timeout=5.0
                )
                
                # Debug: Log the response
                if DEBUG_API_REQUESTS:
                    self.add_status_message(f"[DEBUG] PUT /api/position response status: {put_response.status_code}")
                    try:
                        response_json = put_response.json()
                        response_str = json.dumps(response_json, indent=2)
                        self.add_status_message(f"[DEBUG] PUT /api/position response body:\n{response_str}")
                    except (ValueError, json.JSONDecodeError):
                        # Response is not JSON, show text instead
                        response_text = put_response.text[:500]  # First 500 chars
                        self.add_status_message(f"[DEBUG] PUT /api/position response body (non-JSON):\n{response_text}")
                    except Exception as e:
                        self.add_status_message(f"[DEBUG] Error reading response body: {e}")
                
                if put_response.status_code == 200:
                    self.add_status_message("API key validated successfully")
                    self.api_key_valid = True
                    # Update server status dot to green (connected)
                    self.root.after(0, lambda: self.update_server_status_left(True, "Connected"))
                    self.root.after(0, lambda: self.update_server_status(True, "Connected"))
                elif put_response.status_code == 401:
                    self.add_status_message("ERROR: Invalid API key (401 Unauthorized)")
                    self.add_status_message("Please check your API key and try again")
                    self.api_key_valid = False
                    # Update server status dot to red
                    self.root.after(0, lambda: self.update_server_status_left(False, "Invalid API key"))
                    self.root.after(0, lambda: self.update_server_status(False, "Invalid API key"))
                    # Disconnect immediately since API key is invalid
                    self.root.after(0, self.disconnect)
                    return
                else:
                    # For 422 errors, try to extract detail from response
                    error_detail = ""
                    try:
                        error_json = put_response.json()
                        error_detail = error_json.get("detail", "")
                        if error_detail:
                            self.add_status_message(f"ERROR: {error_detail}")
                    except (ValueError, json.JSONDecodeError, AttributeError):
                        pass
                    
                    self.add_status_message(f"Warning: API key check returned status {put_response.status_code}")
                    if not error_detail:
                        self.add_status_message(f"Response: {put_response.text[:200]}")
                    self.api_key_valid = False
            else:
                self.add_status_message(f"Server health check failed: {test_response.status_code}")
                self.server_online = False
                # Update server status dot to red
                self.root.after(0, lambda: self.update_server_status_left(False, f"Server error: {test_response.status_code}"))
                self.root.after(0, lambda: self.update_server_status(False, f"Server error: {test_response.status_code}"))
                return
        except requests.exceptions.RequestException as e:
            # Identify which request failed by checking the error message
            error_str = str(e)
            if "health" in error_str.lower() or "GET" in error_str:
                request_type = "GET /api/health"
            elif "position" in error_str.lower() or "PUT" in error_str:
                request_type = "PUT /api/position"
            else:
                request_type = "unknown request"
            
            # Check if response content contains server logs
            if hasattr(e, 'response') and e.response is not None:
                try:
                    response_text = e.response.text[:500]  # First 500 chars
                    if "INFO:" in response_text or "HTTP" in response_text:
                        self.add_status_message(f"ERROR: Server returned logs in {request_type} response!")
                        self.add_status_message(f"Response preview: {response_text[:200]}")
                except:
                    pass
            
            self.add_status_message(f"Warning: {request_type} failed: {str(e)[:200]}")
            self.server_online = False
            # Update server status dot to red
            self.root.after(0, lambda: self.update_server_status_left(False, "Connection failed"))
            self.root.after(0, lambda: self.update_server_status(False, "Connection failed"))
            return
        
        # Check if client still exists before starting
        if not self.client:
            self.add_status_message("ERROR: Client was not initialized properly")
            self.is_connected = False
            self.root.after(0, self.update_ui_state)
            return
        
        # Start client (this will start data source and send thread)
        try:
            if hasattr(self.client, 'start'):
                self.client.start()
            else:
                self.add_status_message("ERROR: Client object does not have start() method")
                self.is_connected = False
                self.root.after(0, self.update_ui_state)
                return
        except AttributeError as e:
            self.add_status_message(f"ERROR: Failed to start client: {e}")
            self.is_connected = False
            self.root.after(0, self.update_ui_state)
            return
        
        self.add_status_message("Client started successfully")
        if self.atc_mode_enabled:
            self.add_status_message(f"ATC Mode: Using fixed position ({self.atc_latitude:.6f}, {self.atc_longitude:.6f})")
        elif hasattr(self, 'client_source_type') and self.client_source_type == DataSourceType.FSWIDGET:
            if self.client.data_source:
                self.add_status_message(f"Connecting to FSWidget at {self.client.data_source.fswidget_ip}:{self.client.data_source.fswidget_port}")
        else:
            if self.client.data_source:
                self.add_status_message(f"Listening for Aerofly data on port {self.client.data_source.udp_port}")
        
        # Keep thread alive while running
        while self.client.running:
            time.sleep(1)
    
    def toggle_connection(self):
        """Connect or disconnect from the server."""
        if self.is_connected:
            # Disconnect
            self.disconnect()
        else:
            # Connect
            self.connect()
    
    def connect(self):
        """Connect to the server."""
        server_url = self.server_url_entry.get().strip()
        api_key = self.api_key_entry.get().strip()
        
        if not server_url:
            self.add_status_message("Error: Server URL is required")
            return
        
        if not api_key:
            self.add_status_message("Error: API Key is required")
            return
        
        # Check if user provided a callsign
        user_callsign = self.callsign_entry.get().strip() or None
        need_fetch_from_server = (user_callsign is None)
        
        # Only calculate ICAO if we need to fetch callsign from server
        if need_fetch_from_server:
            self.own_icao = compute_own_icao(api_key)
        else:
            self.own_icao = None
        
        self.server_callsign = None
        
        # Disable inputs (API v2.2: also disable flightplan_route text field after connection)
        self.server_url_entry.config(state="disabled")
        self.api_key_entry.config(state="disabled")
        self.callsign_entry.config(state="disabled")
        self.departure_airport_entry.config(state="disabled")
        self.arrival_airport_entry.config(state="disabled")
        self.airplane_model_entry.config(state="disabled")
        self.airplane_livery_entry.config(state="disabled")
        self.cruise_altitude_entry.config(state="disabled")
        self.aviator_ledger_checkbox.config(state="disabled")
        self.fswidget_ip_entry.config(state="disabled")
        self.udp_radio.config(state="disabled")
        self.fswidget_radio.config(state="disabled")
        self.auto_detect_checkbox.config(state="disabled")
        # Disable ATC mode toggle and fields when connecting
        if hasattr(self, 'atc_mode_var'):
            # Disable ATC input fields (they're already disabled if ATC mode is off)
            self.atc_latitude_entry.config(state="disabled")
            self.atc_longitude_entry.config(state="disabled")
            self.atc_altitude_entry.config(state="disabled")
            self.atc_heading_entry.config(state="disabled")
            self.atc_ground_speed_entry.config(state="disabled")
        # Block flightplan_route text field after connection (API v2.2 requirement)
        if self.flightplan_route_text:
            self.flightplan_route_text.config(state="disabled")
            self.flightplan_finalized = True  # Mark as finalized
        self.connect_button.config(text="Connecting...", state="disabled")
        
        self.add_status_message(f"Connecting to {server_url}...")
        
        # Validate ATC position if ATC mode is enabled
        if self.atc_mode_enabled:
            is_valid, error_msg = self.validate_atc_position()
            if not is_valid:
                self.add_status_message(f"Error: {error_msg}")
                # Re-enable inputs
                self.server_url_entry.config(state="normal")
                self.api_key_entry.config(state="normal")
                self.callsign_entry.config(state="normal")
                self.departure_airport_entry.config(state="normal")
                self.arrival_airport_entry.config(state="normal")
                self.airplane_model_entry.config(state="normal")
                self.airplane_livery_entry.config(state="normal")
                self.cruise_altitude_entry.config(state="normal")
                self.aviator_ledger_checkbox.config(state="normal")
                self.connect_button.config(text="Connect", state="normal")
                return
        
        try:
            # Get data source selection (ignored if ATC mode is enabled)
            use_fswidget = (self.data_source_var.get() == "fswidget") if not self.atc_mode_enabled else False
            auto_detect = self.auto_detect_var.get() if not self.atc_mode_enabled else False
            
            # Get FSWidget IP (only used if not auto-detecting)
            if auto_detect:
                fswidget_ip = "localhost"  # Will be auto-detected by SimulatorDataSource
            else:
                fswidget_ip = self.fswidget_ip_entry.get().strip() or "localhost"
            
            # Unified app: get/create SimulatorDataSource from callback (uses autodetect + IP from UI)
            # When autodetect is used, this scans the network and can take several seconds - run in thread to avoid UI freeze
            if self.get_simulator_data_source:
                needs_async = auto_detect  # Autodetect scans network and blocks
                if needs_async:
                    self.add_status_message("Detecting sim... (this may take up to 30 seconds)")
                    self.connect_button.config(text="Detecting sim...")
                    self.root.update_idletasks()  # Show message before starting thread
                    ds_result = [None]
                    def _detect_thread():
                        try:
                            ds_result[0] = self.get_simulator_data_source(use_fswidget, fswidget_ip, auto_detect)
                        except Exception as e:
                            ds_result[0] = e
                    t = threading.Thread(target=_detect_thread, daemon=True)
                    t.start()
                    def _on_detect_done():
                        if not self.root.winfo_exists():
                            return
                        ds = ds_result[0]
                        if isinstance(ds, Exception):
                            self.add_status_message(f"Connection error: {ds}")
                            self._revert_connect_ui()
                            return
                        self.shared_data_source = ds
                        self.connect_button.config(text="Connecting...")
                        try:
                            self._connect_phase2(server_url, api_key, need_fetch_from_server, user_callsign,
                                                use_fswidget, fswidget_ip, auto_detect)
                        except Exception as e:
                            self.add_status_message(f"Connection error: {e}")
                            self._revert_connect_ui()
                    def _poll():
                        if t.is_alive():
                            self.root.after(200, _poll)
                        else:
                            self.root.after(0, _on_detect_done)
                    self.root.after(200, _poll)
                    return  # Exit connect(); _connect_phase2 will continue
                else:
                    self.shared_data_source = self.get_simulator_data_source(use_fswidget, fswidget_ip, auto_detect)
            
            self._connect_phase2(server_url, api_key, need_fetch_from_server, user_callsign,
                                use_fswidget, fswidget_ip, auto_detect)
            
        except Exception as e:
            self.add_status_message(f"Connection error: {e}")
            self._revert_connect_ui()
            return
    
    def _revert_connect_ui(self):
        """Re-enable inputs after a failed connection attempt."""
        self.client = None
        self.is_connected = False
        self.own_icao = None
        self.update_ui_state()
    
    def _connect_phase2(self, server_url, api_key, need_fetch_from_server, user_callsign,
                        use_fswidget, fswidget_ip, auto_detect):
        """Continue connect flow after SimulatorDataSource is ready (flight plan, client creation, etc.)."""
        # Get flight plan data
        departure_airport = self.departure_airport_entry.get().strip() or None
        arrival_airport = self.arrival_airport_entry.get().strip() or None
        airplane_model = self.airplane_model_entry.get().strip() or None
        airplane_livery = self.airplane_livery_entry.get().strip() or None
        
        flightplan_route_raw = None
        if self.flightplan_route_text:
            flightplan_route_raw = self.flightplan_route_text.get("1.0", tk.END).strip()
        flightplan_route = AbflugClient.sanitize_flightplan_route(flightplan_route_raw) if flightplan_route_raw else None
        
        squawk_code = self.current_squawk_code if hasattr(self, 'current_squawk_code') else "0000"
        
        cruise_altitude = None
        cruise_alt_str = self.cruise_altitude_entry.get().strip()
        if cruise_alt_str:
            cruise_alt_str_upper = cruise_alt_str.upper()
            if cruise_alt_str_upper.startswith("FL"):
                try:
                    fl_number = int(cruise_alt_str_upper[2:])
                    if fl_number < 0:
                        corrected_fl = abs(fl_number)
                        self.cruise_altitude_entry.config(state="normal")
                        self.cruise_altitude_entry.delete(0, tk.END)
                        self.cruise_altitude_entry.insert(0, f"FL{corrected_fl}")
                        self.cruise_altitude_entry.config(state="normal")
                        raise ValueError(f"Invalid FL notation '{cruise_alt_str}'. Must be positive. Corrected to FL{corrected_fl}")
                    cruise_altitude = fl_number * 100
                except ValueError as e:
                    error_msg = str(e)
                    if "Invalid FL notation" in error_msg:
                        self.add_status_message(f"Error: {error_msg}")
                    else:
                        self.add_status_message(f"Error: Invalid FL notation '{cruise_alt_str}'. Use format FL### (e.g., FL300)")
                    self._revert_connect_ui()
                    return
            else:
                try:
                    cruise_altitude = int(cruise_alt_str)
                    if cruise_altitude < 0:
                        self.cruise_altitude_entry.config(state="normal")
                        self.cruise_altitude_entry.delete(0, tk.END)
                        self.cruise_altitude_entry.insert(0, str(abs(cruise_altitude)))
                        self.cruise_altitude_entry.config(state="normal")
                        raise ValueError(f"Invalid cruise altitude '{cruise_alt_str}'. Must be a positive number. Corrected to {abs(cruise_altitude)}")
                except ValueError as e:
                    error_msg = str(e)
                    if "Invalid cruise altitude" in error_msg:
                        self.add_status_message(f"Error: {error_msg}")
                    else:
                        self.add_status_message(f"Error: Invalid cruise altitude '{cruise_alt_str}'. Must be a number or FL notation (e.g., 30000 or FL300)")
                    self._revert_connect_ui()
                    return
        
        aviator_ledger_export = self.aviator_ledger_var.get()
        
        client_kwargs = dict(
            api_key=api_key,
            server_url=server_url,
            aerofly_port=DEFAULT_AEROFLY_PORT,
            callsign=user_callsign,
            use_fswidget=use_fswidget,
            fswidget_ip=fswidget_ip,
            aviator_ledger_export=aviator_ledger_export,
            departure_airport=departure_airport,
            arrival_airport=arrival_airport,
            airplane_model=airplane_model,
            airplane_livery=airplane_livery,
            cruise_altitude=cruise_altitude,
            auto_detect=auto_detect,
            flightplan_route=flightplan_route,
            squawk_code=squawk_code,
            atc_mode_enabled=self.atc_mode_enabled,
            atc_latitude=self.atc_latitude if self.atc_mode_enabled else None,
            atc_longitude=self.atc_longitude if self.atc_mode_enabled else None,
            atc_altitude=self.atc_altitude if self.atc_mode_enabled else None,
            atc_heading=self.atc_heading if self.atc_mode_enabled else None,
            atc_ground_speed=self.atc_ground_speed if self.atc_mode_enabled else None,
            atc_vertical_speed=self.atc_vertical_speed if self.atc_mode_enabled else None
        )
        if self.shared_data_source is not None:
            client_kwargs["data_source"] = self.shared_data_source
        self.client = AbflugClient(**client_kwargs)
        
        if self.shared_data_source is not None:
            self.client_source_type = self.shared_data_source.source_type
        else:
            self.client_source_type = DataSourceType.FSWIDGET if use_fswidget else DataSourceType.UDP
        
        self.is_connected = True
        
        self.client_thread = threading.Thread(target=self.run_client, daemon=True)
        self.client_thread.start()
        
        if need_fetch_from_server:
            self.start_traffic_polling()
        else:
            self.add_status_message(f"Using user-provided callsign: {user_callsign}")
        
        self.add_status_message("Client started successfully")
        self.sim_connected_message_shown = False
        if self.atc_mode_enabled:
            self.add_status_message(f"ATC Mode: Using fixed position ({self.atc_latitude:.6f}, {self.atc_longitude:.6f})")
        elif use_fswidget and self.client.data_source:
            # Use actual detected IP from data source (not fswidget_ip param which may be "localhost" placeholder)
            self.add_status_message(f"Connecting to FSWidget at {self.client.data_source.fswidget_ip}:{self.client.data_source.fswidget_port}")
        elif use_fswidget:
            self.add_status_message(f"Connecting to FSWidget at {fswidget_ip}:58585")
        else:
            self.add_status_message(f"Listening for Aerofly data on port {DEFAULT_AEROFLY_PORT}")
        
        if self.on_connected:
            try:
                obs_kw = {}
                if self.atc_mode_enabled and self.atc_latitude is not None and self.atc_longitude is not None:
                    obs_kw["observer_lat"] = self.atc_latitude
                    obs_kw["observer_lon"] = self.atc_longitude
                self.on_connected(server_url, api_key, **obs_kw)
            except Exception as cb_err:
                self.add_status_message(f"Warning: on_connected callback error: {cb_err}")
        
        self.update_ui_state()
    
    def disconnect(self):
        """Disconnect from the server (API v2.2)."""
        self.add_status_message("Disconnecting...")
        self.sim_connected_message_shown = False  # Reset for next connect
        
        # Stop traffic polling
        self.stop_traffic_polling()
        
        if self.client:
            self.client.stop()
            self.client = None
        
        self.is_connected = False
        self.own_icao = None
        self.server_callsign = None
        self.flightplan_finalized = False  # Reset finalized flag (API v2.2)
        self.update_callsign_display()
        self.update_ui_state()
        self.add_status_message("Disconnected")
    
    def update_ui_state(self):
        """Update UI elements based on connection state (API v2.2)."""
        if self.is_connected:
            self.connect_button.config(text="Disconnect", state="normal")
            self.server_url_entry.config(state="disabled")
            self.api_key_entry.config(state="disabled")
            self.callsign_entry.config(state="disabled")
            self.departure_airport_entry.config(state="disabled")
            self.arrival_airport_entry.config(state="disabled")
            self.airplane_model_entry.config(state="disabled")
            self.airplane_livery_entry.config(state="disabled")
            self.cruise_altitude_entry.config(state="disabled")
            self.aviator_ledger_checkbox.config(state="disabled")
            # Block flightplan_route text field after connection (API v2.2 requirement)
            if self.flightplan_route_text:
                self.flightplan_route_text.config(state="disabled")
            # Keep the selected data source, just disable the controls
            self.fswidget_ip_entry.config(state="disabled")
            # Disable radio buttons by making them unclickable (but keep selection visible)
            self.udp_radio.config(state="disabled")
            self.fswidget_radio.config(state="disabled")
            self.auto_detect_checkbox.config(state="disabled")
            # Disable ATC mode toggle and fields when connected
            if hasattr(self, 'atc_mode_var'):
                # Disable ATC checkbox (can't change ATC mode while connected)
                # Note: We can't easily get the checkbox widget, so we'll handle this in connect()
                pass
            # Squawk code input field remains enabled (can change at any time, API v2.2 requirement)
            # Transponder display is always readonly
        else:
            self.connect_button.config(text="Connect", state="normal")
            self.server_url_entry.config(state="normal")
            self.api_key_entry.config(state="normal")
            self.callsign_entry.config(state="normal")
            self.departure_airport_entry.config(state="normal")
            self.arrival_airport_entry.config(state="normal")
            self.airplane_model_entry.config(state="normal")
            self.airplane_livery_entry.config(state="normal")
            self.cruise_altitude_entry.config(state="normal")
            self.aviator_ledger_checkbox.config(state="normal")
            # Re-enable flightplan_route text field (API v2.2)
            if self.flightplan_route_text:
                self.flightplan_route_text.config(state="normal")
                self.flightplan_finalized = False
            # Re-enable data source selection
            self.udp_radio.config(state="normal")
            self.fswidget_radio.config(state="normal")
            self.auto_detect_checkbox.config(state="normal")
            # Re-enable ATC mode toggle (fields will be enabled/disabled based on toggle state)
            if hasattr(self, 'atc_mode_var'):
                # Re-enable ATC fields based on current toggle state
                self.toggle_atc_mode()  # This will set the correct state based on atc_mode_var
            # Update FSWidget field state based on selection and auto-detect
            if self.data_source_var.get() == "fswidget":
                if self.auto_detect_var.get():
                    self.fswidget_ip_entry.config(state="disabled")
                else:
                    self.fswidget_ip_entry.config(state="normal")
            else:
                self.fswidget_ip_entry.config(state="disabled")
            self.update_server_status_left(False, "Not connected")
            self.update_server_status(False, "Not connected")
    
    def update_status_loop(self):
        """Periodically update status indicators (API v2.2: update left column status dots)."""
        # Check simulator data status (UDP or FSWidget) - update left column status dot
        if self.is_connected and self.client:
            # ATC Mode: Show fixed position status
            if self.atc_mode_enabled:
                self.update_sim_status(True, "ATC Mode: Fixed Position")
            elif self.client.data_source and self.client.data_source.is_receiving_data(5.0):
                latest_gps = self.client.data_source.get_latest_gps()
                if latest_gps:
                    time_since_data = time.time() - self.client.data_source.last_gps_time
                    if time_since_data < 5.0:
                        if not self.sim_connected_message_shown:
                            self.add_status_message("Connected to simulator")
                            self.sim_connected_message_shown = True
                        # Update left column simulator status (API v2.2)
                        self.update_sim_status(True, f"Receiving (last: {time_since_data:.1f}s ago)")
                        # Keep backward compatibility
                        self.update_udp_status(True, f"Receiving (last: {time_since_data:.1f}s ago)")
                    else:
                        self.update_sim_status(False, f"Stale data ({time_since_data:.1f}s old)")
                        self.update_udp_status(False, f"Stale data ({time_since_data:.1f}s old)")
                else:
                    self.update_sim_status(False, "No data received")
                    self.update_udp_status(False, "No data received")
            else:
                self.update_sim_status(False, "No data received")
                self.update_udp_status(False, "No data received")
        elif self.shared_data_source and self.shared_data_source.is_receiving_data(5.0):
            # Shared data source (unified app) - sim data before server connect
            latest_gps = self.shared_data_source.get_latest_gps()
            if latest_gps:
                time_since_data = time.time() - self.shared_data_source.last_gps_time
                if time_since_data < 5.0:
                    if not self.sim_connected_message_shown:
                        self.add_status_message("Connected to simulator")
                        self.sim_connected_message_shown = True
                    self.update_sim_status(True, f"Receiving (last: {time_since_data:.1f}s ago)")
                    self.update_udp_status(True, f"Receiving (last: {time_since_data:.1f}s ago)")
                else:
                    self.update_sim_status(False, f"Stale data ({time_since_data:.1f}s old)")
                    self.update_udp_status(False, f"Stale data ({time_since_data:.1f}s old)")
            else:
                self.update_sim_status(False, "No data received")
                self.update_udp_status(False, "No data received")
        elif self.last_udp_time > 0:
            # Fallback to UDP monitoring if client not connected
            time_since_udp = time.time() - self.last_udp_time
            if time_since_udp < 5.0:
                if not self.sim_connected_message_shown:
                    self.add_status_message("Connected to simulator")
                    self.sim_connected_message_shown = True
                self.update_sim_status(True, f"Receiving (last: {time_since_udp:.1f}s ago)")
                self.update_udp_status(True, f"Receiving (last: {time_since_udp:.1f}s ago)")
            else:
                self.update_sim_status(False, f"Stale data ({time_since_udp:.1f}s old)")
                self.update_udp_status(False, f"Stale data ({time_since_udp:.1f}s old)")
        else:
            self.sim_connected_message_shown = False  # Reset when no data
            self.update_sim_status(False, "Sim Not Ready — please spawn somewhere")
            self.update_udp_status(False, "Sim Not Ready — please spawn somewhere")

        # Server-related checks only when connected - update left column status dot
        if self.is_connected and self.client:
            # Check server health
            self.check_server_health()
            # Update left column server status (API v2.2)
            if self.server_online:
                self.update_server_status_left(True, self.health_status)
            else:
                self.update_server_status_left(False, self.health_status)
            # Keep backward compatibility
            self.update_server_status(self.server_online, self.health_status)
        else:
            self.update_server_status_left(False, "Not connected")
            self.update_server_status(False, "Not connected")
        
        # Schedule next update
        self.root.after(STATUS_UPDATE_INTERVAL, self.update_status_loop)
    
    def start_traffic_polling(self):
        """Start polling traffic API to retrieve callsign from server."""
        if self.traffic_poll_running:
            return
        
        self.traffic_poll_running = True
        self.traffic_poll_thread = threading.Thread(target=self._poll_traffic_for_callsign, daemon=True)
        self.traffic_poll_thread.start()
        self.add_status_message("Traffic polling started (will fetch callsign from server)")
    
    def stop_traffic_polling(self):
        """Stop polling traffic API."""
        self.traffic_poll_running = False
    
    def _poll_traffic_for_callsign(self):
        """Poll HTTP API for traffic to extract our own callsign from server response."""
        poll_count = 0
        
        while self.traffic_poll_running and self.is_connected and self.client:
            try:
                # Determine position to use: GPS data if available, otherwise use 0,0 (test coordinates)
                latest_gps = self.client.data_source.get_latest_gps()
                if latest_gps:
                    query_lat = latest_gps.latitude
                    query_lon = latest_gps.longitude
                    using_gps = True
                else:
                    # Use test coordinates (0,0) if no GPS data available
                    query_lat = 0.0
                    query_lon = 0.0
                    using_gps = False
                
                # Log first request attempt
                if poll_count == 0:
                    if using_gps:
                        self.root.after(0, lambda: self.add_status_message("Fetching callsign from server (using GPS position)..."))
                    else:
                        self.root.after(0, lambda: self.add_status_message("Fetching callsign from server (using test coordinates 0,0)..."))
                
                # Build API URL
                api_url = f"{self.client.server_url}/api/traffic"
                params = {
                    'lat': query_lat,
                    'lon': query_lon,
                    'radius_miles': 50.0  # Default radius
                }

                poll_count += 1
                
                # Debug: Log that we're making a request
                if poll_count == 1:
                    self.root.after(0, lambda: self.add_status_message(f"Making traffic API request (lat={query_lat:.4f}, lon={query_lon:.4f})"))
                
                # Make HTTP GET request
                response = self.client.http_session.get(api_url, params=params, timeout=5.0)
                response.raise_for_status()
                
                # Check if response contains server logs (should be JSON)
                try:
                    data = response.json()
                except ValueError as json_err:
                    # Response is not valid JSON - might contain server logs
                    response_text = response.text[:500]
                    if "INFO:" in response_text or "HTTP" in response_text:
                        self.root.after(0, lambda: self.add_status_message(f"ERROR: GET /api/traffic returned unexpected data. contact developer at support@abflug.cloud with the following information:"))
                        self.root.after(0, lambda: self.add_status_message(f" - Response preview: {response_text[:200]}"))
                    raise
                
                traffic_list = data.get('traffic', [])
                
                # Look for our own traffic (matching ICAO)
                found_own_traffic = False
                for traffic_dict in traffic_list:
                    if self.own_icao and traffic_dict.get('icao_address') == self.own_icao:
                        found_own_traffic = True
                        callsign = traffic_dict.get('callsign')
                        if callsign and callsign != self.server_callsign:
                            self.server_callsign = callsign
                            # Update callsign display and status message in GUI thread
                            self.root.after(0, lambda c=callsign: self.update_callsign_display())
                            self.root.after(0, lambda c=callsign: self.add_status_message(f"Callsign from server: {c}"))
                            # Stop polling once we've found and set the callsign
                            self.stop_traffic_polling()
                            self.root.after(0, lambda: self.add_status_message("Traffic polling stopped (callsign retrieved)"))
                            return  # Exit the thread cleanly
                        break
                
                # Log if we didn't find our own traffic (only first few times to avoid spam)
                if not found_own_traffic and poll_count <= 3:
                    self.root.after(0, lambda: self.add_status_message(f"Traffic query successful but own aircraft not found yet (poll #{poll_count})"))
                
            except requests.exceptions.RequestException as e:
                # Log first few errors for debugging
                if poll_count < 3:
                    error_msg = str(e)[:100]
                    self.root.after(0, lambda msg=error_msg: self.add_status_message(f"Traffic API error: {msg}"))
            except Exception as e:
                # Log unexpected errors
                if poll_count < 3:
                    error_msg = str(e)[:100]
                    self.root.after(0, lambda msg=error_msg: self.add_status_message(f"Unexpected error in traffic polling: {msg}"))
            
            # Wait before next poll
            time.sleep(self.traffic_poll_interval)
    
    def update_callsign_display(self):
        """Update the callsign entry field with server-provided callsign."""
        # Only update if we don't have a user-provided callsign
        # (This should only be called when fetching from server)
        if not self.callsign_entry.get().strip():
            self.callsign_entry.config(state="normal")
            self.callsign_entry.delete(0, tk.END)
            if self.server_callsign:
                self.callsign_entry.insert(0, self.server_callsign)
            self.callsign_entry.config(state="disabled")


def main():
    """Main entry point."""
    root = tk.Tk()
    app = AbflugClientGUI(root)
    root.mainloop()


if __name__ == '__main__':
    main()

