#!/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 hashlib
import json

# Import the AbflugClient class
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
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
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 = "2.6.0"

# 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."""
    
    def __init__(self, root: tk.Tk):
        self.root = root
        self.root.title(APP_NAME)
        
        # Configure DPI scaling for Windows
        self.scale_factor = configure_tk_dpi(root)
        
        # Set safe window geometry that fits on screen and accounts for DPI
        # Base size: 800x900, will be constrained to screen and centered
        safe_geometry = get_safe_window_geometry(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
        
        # 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
        main_frame = ttk.Frame(self.root, 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")
        
        # Discord webhook configuration (unchanged, API v2.2 requirement)
        discord_frame = ttk.LabelFrame(left_column, text="Discord Webhook", padding="10")
        discord_frame.pack(fill="x", pady=(0, 10))
        
        self.send_to_discord_var = tk.BooleanVar(value=False)
        self.send_to_discord_checkbox = tk.Checkbutton(discord_frame, text="Enable Discord webhooks", variable=self.send_to_discord_var)
        self.send_to_discord_checkbox.pack(anchor="w", pady=2)
        
        webhook_url_frame = ttk.Frame(discord_frame)
        webhook_url_frame.pack(fill="x", pady=2)
        ttk.Label(webhook_url_frame, text="Webhook URL:").pack(side="left", padx=5)
        self.discord_webhook_entry = ttk.Entry(webhook_url_frame, width=30)
        self.discord_webhook_entry.pack(side="left", padx=5, fill="x", expand=True)
        
        # Connect and Clear Log buttons (moved to left column, API v2.2 requirement)
        button_frame = ttk.Frame(left_column)
        button_frame.pack(fill="x", pady=(0, 10))
        
        self.connect_button = ttk.Button(
            button_frame,
            text="Connect",
            command=self.toggle_connection,
            width=15
        )
        self.connect_button.pack(side="left", padx=5)
        
        ttk.Button(
            button_frame,
            text="Clear Log",
            command=self.clear_status,
            width=15
        ).pack(side="left", padx=5)
        
        # ========== 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 button for flight plan route
        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)
        
        # ========== 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, 'send_to_discord_checkbox'):
                self.send_to_discord_checkbox.config(state="disabled")
            if hasattr(self, 'discord_webhook_entry'):
                self.discord_webhook_entry.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, 'send_to_discord_checkbox'):
                self.send_to_discord_checkbox.config(state="normal")
            if hasattr(self, 'discord_webhook_entry'):
                self.discord_webhook_entry.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
                self.add_status_message("Making PUT /api/position request to validate API key...")
                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()
                }
                
                # 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:
            # Calculate own ICAO address (same method as server uses)
            hash_obj = hashlib.md5(api_key.encode())
            self.own_icao = hash_obj.hexdigest()[:6].upper()
        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.discord_webhook_entry.config(state="disabled")
        self.send_to_discord_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.discord_webhook_entry.config(state="normal")
                self.send_to_discord_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
            else:
                fswidget_ip = self.fswidget_ip_entry.get().strip() or "localhost"
            
            # 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
            
            # Get flightplan_route (API v2.2) - sanitize at connection
            flightplan_route_raw = None
            if self.flightplan_route_text:
                flightplan_route_raw = self.flightplan_route_text.get("1.0", tk.END).strip()
            
            # Sanitize flight plan route
            #from abflug_client_web import AbflugClient
            flightplan_route = AbflugClient.sanitize_flightplan_route(flightplan_route_raw) if flightplan_route_raw else None
            
            # Get squawk code (API v2.2) - use current transponder value or default
            squawk_code = self.current_squawk_code if hasattr(self, 'current_squawk_code') else "0000"
            
            # Parse cruise altitude (handle FL notation)
            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"):
                    # FL notation: FL300 = 30000 ft, FL270 = 27000 ft, FL15 = 1500 ft
                    try:
                        fl_number = int(cruise_alt_str_upper[2:])
                        if fl_number < 0:
                            # Update GUI field to show absolute value
                            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 to stop processing
                            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:
                            # This is our negative FL error
                            self.add_status_message(f"Error: {error_msg}")
                        else:
                            # This is a regular ValueError from int() conversion
                            self.add_status_message(f"Error: Invalid FL notation '{cruise_alt_str}'. Use format FL### (e.g., FL300)")
                        # 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.discord_webhook_entry.config(state="normal")
                        self.send_to_discord_checkbox.config(state="normal")
                        self.connect_button.config(text="Connect", state="normal")
                        return
                else:
                    # Direct feet value
                    try:
                        cruise_altitude = int(cruise_alt_str)
                        if cruise_altitude < 0:
                            # Update GUI field to show absolute value
                            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 to stop processing
                            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:
                            # This is our negative number error
                            self.add_status_message(f"Error: {error_msg}")
                        else:
                            # This is a regular ValueError from int() conversion
                            self.add_status_message(f"Error: Invalid cruise altitude '{cruise_alt_str}'. Must be a number or FL notation (e.g., 30000 or FL300)")
                        # 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.discord_webhook_entry.config(state="normal")
                        self.send_to_discord_checkbox.config(state="normal")
                        self.connect_button.config(text="Connect", state="normal")
                        return
            
            # Get Discord webhook setting
            # Only use webhook URL if checkbox is enabled and URL is provided
            discord_webhook_url = None
            if self.send_to_discord_var.get():
                webhook_url = self.discord_webhook_entry.get().strip()
                if webhook_url:
                    # Validate webhook URL format
                    if not webhook_url.startswith("https://discord.com/api/webhooks/"):
                        self.add_status_message("Error: Invalid Discord webhook URL (must start with https://discord.com/api/webhooks/)")
                        # 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.discord_webhook_entry.config(state="normal")
                        self.send_to_discord_checkbox.config(state="normal")
                        self.connect_button.config(text="Connect", state="normal")
                        return
                    discord_webhook_url = webhook_url
                else:
                    self.add_status_message("Warning: Discord webhooks enabled but no URL provided. Webhooks will be disabled.")
            
            # Create client instance with user-provided callsign (if any)
            # Callsign will be sent via flight plan (highest precedence)
            # API v2.2: Include flightplan_route and squawk_code
            # ATC Mode: Pass fixed position parameters if ATC mode is enabled
            self.client = AbflugClient(
                api_key=api_key,
                server_url=server_url,
                aerofly_port=DEFAULT_AEROFLY_PORT,
                callsign=user_callsign,  # Use user callsign if provided, None otherwise (sent via flight plan)
                use_fswidget=use_fswidget,
                fswidget_ip=fswidget_ip,
                send_to_discord=discord_webhook_url,
                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,  # API v2.2
                squawk_code=squawk_code,  # API v2.2
                # ATC Mode parameters
                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
            )
            
            # Store source type for status updates
            self.client_source_type = DataSourceType.FSWIDGET if use_fswidget else DataSourceType.UDP
            
            # Set connected state BEFORE starting threads (so they see is_connected=True)
            self.is_connected = True
            
            # Start client in a separate thread
            self.client_thread = threading.Thread(target=self.run_client, daemon=True)
            self.client_thread.start()
            
            # Only start traffic polling if we need to fetch callsign from server
            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")
            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:
                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}")
            
        except Exception as e:
            self.add_status_message(f"Connection error: {e}")
            self.client = None
            self.is_connected = False
            self.own_icao = None
            # 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.discord_webhook_entry.config(state="normal")
            self.send_to_discord_checkbox.config(state="normal")
            self.connect_button.config(text="Connect", state="normal")
            return
        
        # Update UI
        self.update_ui_state()
    
    def disconnect(self):
        """Disconnect from the server (API v2.2)."""
        self.add_status_message("Disconnecting...")
        
        # 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.discord_webhook_entry.config(state="disabled")
            self.send_to_discord_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.discord_webhook_entry.config(state="normal")
            self.send_to_discord_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:
                        # 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.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:
                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.update_sim_status(False, "No data received")
            self.update_udp_status(False, "No data received")

        # 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()

