#!/usr/bin/env python3
"""
Abflug Client Sender - Forward Aerofly GPS Data to Multiplayer Server (API v2.0)

This script receives GPS data from flight simulators (UDP or FSWidget TCP) and
forwards it to the Abflug multiplayer server via HTTP API. It acts as the "uplink" part of the multiplayer system.

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.

Usage:
    python3 abflug_client_web.py --api-key YOUR_API_KEY --server SERVER_URL --aviator-ledger
    Example:
    python3 abflug_client_web.py --api-key 1234567890 --server https://abflug.cloud --aviator-ledger
"""

import os
import threading
import time
import uuid
import argparse
import requests
from typing import Optional, Dict, Any
from datetime import datetime
# Import shared simulator data source (local import)
from simulator_data_source import SimulatorDataSource, DataSourceType, GPSData
from aircraft_status import aircraft as aircraft_status
from aircraft_status import airport as airport_status
from config_utils import get_butter_api_key, get_server_url, _get_base_dir
# Constants
DEFAULT_AEROFLY_PORT = 49002  # Port to receive data from Aerofly
DEFAULT_SERVER_PORT = 80       # HTTP port (or 443 for HTTPS)
DEFAULT_SERVER_PATH = "/api/position"  # API endpoint for position updates
UPDATE_INTERVAL = 1.0  # Seconds between server updates (rate limiting)
HTTP_TIMEOUT = 5.0  # HTTP request timeout in seconds
# How long to wait without new simulator data before pausing sends (seconds)
STALE_GPS_TIMEOUT = 5.0
FSWIDGET_PORT = 58585  # Fixed port for FSWidget TCP protocol
APP_VERSION = "3.1.1"
APP_NAME = "Abflug Client CLI v3.1.1"
from discord_hooks_module import (
    build_ledger_csv_row,
    ledger_column_names,
    ledger_final_basename,
    sanitize_aircraft_token,
    write_ledger_csv_line,
)
from terminal_debug import terminal_debug_print
# IMPORTANT: Simulator data source converts altitude to feet if it is in meters from UDP data source
# NO NEED TO CONVERT ALTITUDE IN AIRCRAFT STATUS

# Config file is no longer used - runways and gates are passed as empty lists

class AbflugClient:
    """Client that forwards simulator GPS data to the multiplayer server."""
    
    def __init__(self, api_key: str, server_url: str, aerofly_port: int = DEFAULT_AEROFLY_PORT,
                 use_https: bool = False, callsign: Optional[str] = None,
                 use_fswidget: bool = False, fswidget_ip: str = "localhost", aviator_ledger_export: bool = False,
                 departure_airport: Optional[str] = None, arrival_airport: Optional[str] = None,
                 airplane_model: Optional[str] = None, airplane_livery: Optional[str] = None,
                 cruise_altitude: Optional[int] = None, auto_detect: bool = False,
                 flightplan_route: Optional[str] = None, squawk_code: Optional[str] = None,
                 # ATC Mode parameters
                 atc_mode_enabled: bool = False,
                 atc_latitude: Optional[float] = None,
                 atc_longitude: Optional[float] = None,
                 atc_altitude: Optional[float] = None,
                 atc_heading: Optional[float] = None,
                 atc_ground_speed: Optional[float] = None,
                 atc_vertical_speed: Optional[float] = None,
                 data_source: Optional[SimulatorDataSource] = None):
        self.api_key = api_key
        self.callsign = callsign  # Optional user-provided callsign (will be sent via flight plan)
        # Parse server URL (can be IP or domain, with or without port)
        print(f"Server URL: {server_url}")
        print(f"Departure Airport: {departure_airport}")
        print(f"Arrival Airport: {arrival_airport}")
        if not server_url.startswith(('http://', 'https://')):
            protocol = 'https://' if use_https else 'http://'
            server_url = f"{protocol}{server_url}"
        # Ensure URL doesn't end with /
        self.server_url = server_url.rstrip('/')
        self.api_url = f"{self.server_url}{DEFAULT_SERVER_PATH}"
        self.aviator_ledger_export = bool(aviator_ledger_export)
        self._ledger_csv_temp_path: Optional[str] = None
        self._ledger_warned_missing_airports: bool = False
        self._ledger_warned_wip_cleared_by_ground: bool = False

        # Flight plan data - stored for flight plan registration
        self.departure_airport_code = departure_airport
        self.arrival_airport_code = arrival_airport
        self.airplane_model = airplane_model
        self.airplane_livery = airplane_livery
        # Cruise altitude in feet (default: 30000 if not provided)
        self.cruise_altitude = cruise_altitude if cruise_altitude is not None else 30000
        # Flight plan route text (API v2.2) - stored as raw text, sanitized when sent
        self.flightplan_route = flightplan_route
        # Squawk code (API v2.2) - validated and padded to 4 digits
        self.squawk_code = self.validate_and_pad_squawk_code(squawk_code) if squawk_code else "0000"
        
        # ATC Mode - Fixed Position (ATC version)
        self.atc_mode_enabled = atc_mode_enabled
        self.atc_latitude = atc_latitude
        self.atc_longitude = atc_longitude
        self.atc_altitude = atc_altitude if atc_altitude is not None else 0.0
        self.atc_heading = atc_heading if atc_heading is not None else 0.0
        self.atc_ground_speed = atc_ground_speed if atc_ground_speed is not None else 0.0
        self.atc_vertical_speed = atc_vertical_speed if atc_vertical_speed is not None else 0.0
        
        # Initialize simulator data source (only if ATC mode is not enabled)
        if self.atc_mode_enabled:
            self.data_source = None
            self._owns_data_source = False
        elif data_source is not None:
            # External data source provided (e.g. from unified app) - caller owns lifecycle
            self.data_source = data_source
            self._owns_data_source = False
        else:
            source_type = DataSourceType.FSWIDGET if use_fswidget else DataSourceType.UDP
            self.data_source = SimulatorDataSource(
                source_type=source_type,
                udp_port=aerofly_port,
                fswidget_ip=fswidget_ip,
                fswidget_port=FSWIDGET_PORT,
                auto_detect=auto_detect
            )
            self._owns_data_source = True
        
        # Aircraft status tracking
        self.aircraft_status_obj: Optional[aircraft_status] = None
        self.current_aircraft_status = "unknown"
        
        # Aviator Ledger / legacy one-shot gates for depart→land lifecycle
        self.sent_departure_message = False
        self.sent_arrival_message = False

        # State
        self.running = False
        self.stale_disconnect_sent: bool = False
        self.last_send_time = 0
        
        # HTTP session for connection reuse
        self.http_session = requests.Session()
        # Explicitly enable SSL certificate verification to prevent MITM attacks
        self.http_session.verify = True
        self.http_session.headers.update({
            'X-API-Key': self.api_key,
            'Content-Type': 'application/json'
        })
        
        # Threads
        self.send_thread: Optional[threading.Thread] = None
        
        # Statistics
        self.messages_sent = 0
        self.http_errors = 0
        self.start_time = time.time()
        
        if self.aviator_ledger_export:
            terminal_debug_print("ledger", f"[DEBUG LEDGER] Aviator Ledger export enabled; CSV directory: {_get_base_dir()}")

    @staticmethod
    def sanitize_flightplan_route(route: Optional[str]) -> str:
        """
        Sanitize flight plan route text according to API v2.2 specification.
        
        Rules:
        - Max 500 characters before sanitization
        - Only alphanumeric characters, spaces, periods (.), and forward slashes (/) allowed
        - Other non-alphanumeric characters are stripped
        - Leading and trailing spaces are trimmed
        - If empty, missing, or all spaces after sanitization, returns "NO DATA"
        
        Args:
            route: Raw route text from user input
            
        Returns:
            Sanitized route text or "NO DATA" if invalid/empty
        """
        if not route:
            return "NO DATA"
        
        # Truncate to 500 characters if longer
        route = route[:500]
        
        # Strip non-alphanumeric characters (except spaces, periods, and forward slashes)
        sanitized = ''.join(c if c.isalnum() or c.isspace() or c == '.' or c == '/' else '' for c in route)
        
        # Trim leading and trailing spaces
        sanitized = sanitized.strip()
        
        # If empty or all spaces after sanitization, return "NO DATA"
        if not sanitized:
            return "NO DATA"
        
        return sanitized
    
    @staticmethod
    def validate_and_pad_squawk_code(squawk: Optional[str]) -> str:
        """
        Validate and pad squawk code according to API v2.2 specification.
        
        Rules:
        - Exactly 4 digits (0000-9999)
        - Handled as string
        - Left-padded with zeros if less than 4 digits
        - Defaults to "0000" if invalid or not set
        
        Args:
            squawk: Raw squawk code from user input
            
        Returns:
            Validated and padded squawk code (always 4 digits) or "0000" if invalid
        """
        if not squawk:
            return "0000"
        
        # Remove any whitespace
        squawk = squawk.strip()
        
        # If empty after stripping, return default
        if not squawk:
            return "0000"
        
        # Try to parse as integer to validate it's all digits
        try:
            squawk_int = int(squawk)
            # Validate range
            if squawk_int < 0 or squawk_int > 9999:
                return "0000"
            # Left-pad with zeros to 4 digits
            return f"{squawk_int:04d}"
        except ValueError:
            # Not a valid integer, return default
            return "0000"
    
    def get_airport_info(self, icao_code: str) -> Optional[Dict[str, Any]]:
        """
        Get airport information from the server API.
        
        Args:
            icao_code: ICAO airport code (e.g., "KSFO")
        
        Returns:
            Dictionary with airport data, or None if not found/error
        """
        try:
            response = self.http_session.get(
                f"{self.server_url}/api/airport",
                params={'icao_code': icao_code},
                timeout=HTTP_TIMEOUT
            )
            response.raise_for_status()
            return response.json()
        except requests.exceptions.RequestException as e:
            if hasattr(e, 'response') and e.response is not None:
                if e.response.status_code == 404:
                    print(f"Airport {icao_code} not found in server database")
                else:
                    print(f"Error fetching airport info for {icao_code}: {e.response.status_code} - {e.response.text[:100]}")
            else:
                print(f"Error fetching airport info for {icao_code}: {e}")
            return None
    
    def register_flight_plan(self) -> bool:
        """
        Register flight plan with the server (API v2.2 includes flightplan_route).
        
        In ATC mode: Use airplane_model (ICAO) for both departure and arrival airports
        to satisfy server validation, while keeping model/livery for tower detection.
        
        Returns:
            True if successful, False otherwise
        """
        # Build flight plan payload (all fields optional)
        flight_plan = {}
        
        # In ATC mode, use airplane_model (ICAO) for both departure and arrival
        # to satisfy server validation (server requires valid ICAO codes)
        if self.atc_mode_enabled and self.airplane_model:
            # Use model as ICAO for both airports (validates on server)
            import re
            icao_pattern = re.compile(r'^[A-Z0-9]{3,4}$')
            if icao_pattern.match(self.airplane_model.upper()):
                flight_plan['departure_airport'] = self.airplane_model.upper()
                flight_plan['arrival_airport'] = self.airplane_model.upper()
                terminal_debug_print("atc", f"[DEBUG ATC] ATC mode: Using model '{self.airplane_model}' as ICAO for both departure and arrival airports")
        else:
            # Normal mode: use provided departure/arrival airports
            if self.departure_airport_code:
                flight_plan['departure_airport'] = self.departure_airport_code.upper()
            if self.arrival_airport_code:
                flight_plan['arrival_airport'] = self.arrival_airport_code.upper()
        
        if self.airplane_model:
            flight_plan['airplane_model'] = self.airplane_model
        if self.airplane_livery:
            flight_plan['airplane_livery'] = self.airplane_livery
        if self.callsign:
            flight_plan['callsign'] = self.callsign
        
        # Add flightplan_route (API v2.2) - sanitize before sending
        if self.flightplan_route is not None:
            sanitized_route = self.sanitize_flightplan_route(self.flightplan_route)
            flight_plan['flightplan_route'] = sanitized_route
        
        # Only send if we have at least one field
        if not flight_plan:
            terminal_debug_print("atc", "[DEBUG ATC] No flight plan data to register (all fields optional)")
            return True  # Not an error, just no data
        
        # DEBUG: Log flight plan being sent
        terminal_debug_print("atc", f"[DEBUG ATC] Sending flight plan: {flight_plan}")
        
        try:
            response = self.http_session.put(
                f"{self.server_url}/api/flightplan",
                json=flight_plan,
                timeout=HTTP_TIMEOUT
            )
            response.raise_for_status()
            data = response.json()
            terminal_debug_print("atc", f"[DEBUG ATC] Flight plan registered successfully: {data.get('message', 'OK')}")
            terminal_debug_print("atc", f"[DEBUG ATC] Server response: {data}")
            return True
        except requests.exceptions.RequestException as e:
            terminal_debug_print("atc", f"[DEBUG ATC] ERROR registering flight plan: {e}", always=True)
            if hasattr(e, 'response') and e.response is not None:
                terminal_debug_print("atc", f"[DEBUG ATC]   Response status: {e.response.status_code}", always=True)
                try:
                    error_json = e.response.json()
                    terminal_debug_print("atc", f"[DEBUG ATC]   Response JSON: {error_json}", always=True)
                    error_detail = error_json.get('detail', 'Unknown error')
                    if e.response.status_code == 400:
                        terminal_debug_print("atc", f"[DEBUG ATC] Error (400 Bad Request): {error_detail}", always=True)
                        if 'invalid airport code' in error_detail.lower():
                            terminal_debug_print("atc", "[DEBUG ATC]   One or more airport codes are invalid", always=True)
                    elif e.response.status_code == 422:
                        terminal_debug_print("atc", f"[DEBUG ATC] Error (422 Unprocessable Entity): {error_detail}", always=True)
                    else:
                        terminal_debug_print("atc", f"[DEBUG ATC] Error ({e.response.status_code}): {error_detail}", always=True)
                except:
                    terminal_debug_print("atc", f"[DEBUG ATC]   Response text: {e.response.text[:200]}", always=True)
            else:
                terminal_debug_print("atc", f"[DEBUG ATC]   No response object available", always=True)
            return False
    
    def send_loop(self) -> None:
        """Send GPS data to server via HTTP PUT at regular intervals."""
        terminal_debug_print("atc", "[DEBUG ATC] Send loop started")
        loop_count = 0
        while self.running:
            try:
                current_time = time.time()
                loop_count += 1
                if loop_count == 1:
                    terminal_debug_print("atc", f"[DEBUG ATC] Send loop iteration #{loop_count} - ATC mode: {self.atc_mode_enabled}")
                
                # ATC Mode: Use fixed position instead of simulator data
                if self.atc_mode_enabled:
                    # Create fixed position GPS data object with all required fields
                    latest_gps = GPSData(
                        latitude=self.atc_latitude,
                        longitude=self.atc_longitude,
                        altitude=self.atc_altitude,
                        track=self.atc_heading,
                        magnetic_track=self.atc_heading,  # Use same as true heading (no magnetic declination for ATC)
                        indicated_air_speed=0.0,  # Not applicable for fixed position
                        vertical_speed=self.atc_vertical_speed,
                        ground_speed=self.atc_ground_speed,
                        raw_message=f"ATC_FIXED_POSITION:{self.atc_latitude},{self.atc_longitude}",  # Dummy raw message
                        source_type=DataSourceType.UDP  # Use UDP as source type (doesn't matter for ATC mode)
                    )
                    # Only print position every 10 iterations to reduce spam
                    if loop_count % 10 == 1 or loop_count <= 3:
                        terminal_debug_print("atc", f"[DEBUG ATC] Using fixed position: lat={self.atc_latitude:.6f}, lon={self.atc_longitude:.6f}, alt={self.atc_altitude:.1f}ft, hdg={self.atc_heading:.1f}°, spd={self.atc_ground_speed:.1f}kts")
                else:
                    # Get latest GPS data from data source
                    latest_gps = self.data_source.get_latest_gps()
                    
                    # If GPS data is stale, pause sending and optionally disconnect once
                    if latest_gps and not self.data_source.is_receiving_data(STALE_GPS_TIMEOUT):
                        if not self.stale_disconnect_sent:
                            # Send an explicit disconnect to the server
                            try:
                                self.http_session.delete(self.api_url, timeout=HTTP_TIMEOUT)
                            except requests.exceptions.RequestException:
                                # Ignore errors on disconnect attempt
                                pass
                            self.stale_disconnect_sent = True
                        # Stop sending until fresh data arrives
                        time.sleep(0.5)
                        continue

                # Check if we should send (time interval check)
                time_since_last_send = current_time - self.last_send_time
                should_send = latest_gps and time_since_last_send >= UPDATE_INTERVAL
                
                if loop_count <= 5:
                    terminal_debug_print("atc", f"[DEBUG ATC] Loop #{loop_count}: latest_gps={latest_gps is not None}, time_since_last={time_since_last_send:.2f}s, UPDATE_INTERVAL={UPDATE_INTERVAL}, should_send={should_send}")
                
                if should_send:
                    # Reset stale disconnect flag when we have fresh data
                    self.stale_disconnect_sent = False
                    
                    # Prepare JSON payload (callsign removed - it's sent via flight plan with highest precedence)
                    # API v2.2: Include squawk_code in position updates
                    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,  # Feet per minute (0.0 for UDP, actual value for FSWidget)
                        "timestamp": current_time,
                        "squawk_code": self.squawk_code  # Always send squawk code (defaults to "0000")
                    }
                    # NOTE: Callsign is NOT included here - it should only be sent via flight plan endpoint
                    # Flight plan callsign has highest precedence
                    
                    # DEBUG: Log position update being sent (every update, not just first few)
                    terminal_debug_print("atc", f"[DEBUG ATC] Sending position update #{loop_count}: lat={payload['latitude']:.6f}, lon={payload['longitude']:.6f}, alt={payload['altitude']:.1f}ft, track={payload['track']:.1f}°, speed={payload['ground_speed']:.1f}kts, squawk={payload['squawk_code']}")
                    
                    # Update aircraft status if we have airport information
                    if self.departure_airport_code and self.arrival_airport_code:
                        # Initialize aircraft status object if not already done
                        if self.aircraft_status_obj is None:
                            # Fetch airport data from API (for altitude) - this ensures state machine works out-of-the-box
                            dep_api_data = self.get_airport_info(self.departure_airport_code)
                            arr_api_data = self.get_airport_info(self.arrival_airport_code)
                            
                            # Get altitude from API (elevation_ft field), fallback to 0 if not found
                            dep_altitude = dep_api_data.get('elevation_ft', 0) if dep_api_data else 0
                            arr_altitude = arr_api_data.get('elevation_ft', 0) if arr_api_data else 0
                            
                            # Log if airport data was found or not
                            if not dep_api_data:
                                print(f"Warning: Airport {self.departure_airport_code} not found in server database, using default altitude 0")
                            else:
                                print(f"Airport {self.departure_airport_code} elevation: {dep_altitude} ft (from API)")
                            
                            if not arr_api_data:
                                print(f"Warning: Airport {self.arrival_airport_code} not found in server database, using default altitude 0")
                            else:
                                print(f"Airport {self.arrival_airport_code} elevation: {arr_altitude} ft (from API)")
                            
                            # Use empty lists for runways and gates (config file no longer used)
                            dep_runways = []
                            dep_gates = []
                            arr_runways = []
                            arr_gates = []
                            
                            # Create departure airport object with API altitude and config runways/gates
                            dep_airport = airport_status(
                                self.departure_airport_code,
                                dep_altitude,
                                dep_runways,
                                dep_gates
                            )
                            
                            # Create arrival airport object with API altitude and config runways/gates
                            arr_airport = airport_status(
                                self.arrival_airport_code,
                                arr_altitude,
                                arr_runways,
                                arr_gates
                            )
                            self.aircraft_status_obj = aircraft_status(
                                self.callsign or "UNKNOWN",
                                dep_airport,
                                arr_airport,
                                "",  # departure_time - not used yet
                                "",  # arrival_time - not used yet
                                self.callsign or "UNKNOWN",  # pilot
                                cruise_altitude=self.cruise_altitude,
                                action="",
                                gate="",
                                runway=""
                            )
                        
                        # Update aircraft status with current GPS data
                        if self.aircraft_status_obj:
                            #print(f"Aircraft status object: {self.aircraft_status_obj}")
                            # Ground speed is already in knots (normalized in GPSData)
                            # UDP: converted from m/s to knots during parsing
                            # FSWidget: already in knots, used as-is
                            if latest_gps.indicated_air_speed > 0:
                                # use the indicated air speed if it is available
                                speed_knots = latest_gps.indicated_air_speed
                            else:
                                speed_knots = latest_gps.ground_speed
                            self.aircraft_status_obj.set_speed(speed_knots)
                            #print(f"Setting altitude: {latest_gps.altitude}")
                            # why I do not get any other print statements after this?
                            self.aircraft_status_obj.set_altitude(latest_gps.altitude)
                            #print(f"Altitude set to: {latest_gps.altitude}")
                            #print(f"Altitude set to: {self.aircraft_status_obj.get_altitude()}")
                            # Update state based on current status
                            #print(f"Updating state based on current status")
                            # Run state machine ONCE - it both sets the state internally and returns the new state
                            # Note: aircraft_state_machine() calls set_state() internally, so we don't need to set it again
                            previous_state = self.aircraft_status_obj.get_state()
                            ar_status = self.aircraft_status_obj.aircraft_state_machine()
                            
                            # Check if state changed
                            #if ar_status != previous_state:
                                #print(f"🔄 State changed from '{previous_state}' to '{ar_status}'")
                            
                            # Update current status
                            self.current_aircraft_status = ar_status
                            
                            # Naive local wall-clock stamps (same clock for departure + landing → duration is correct)
                            if ar_status == "departing":
                                if not (
                                    self.aircraft_status_obj.departure_time
                                    and str(self.aircraft_status_obj.departure_time).strip()
                                ):
                                    self.aircraft_status_obj.departure_time = datetime.now().strftime(
                                        "%Y-%m-%d %H:%M:%S"
                                    )
                            if ar_status == "landed":
                                if not (
                                    self.aircraft_status_obj.arrival_time
                                    and str(self.aircraft_status_obj.arrival_time).strip()
                                ):
                                    self.aircraft_status_obj.arrival_time = datetime.now().strftime(
                                        "%Y-%m-%d %H:%M:%S"
                                    )
                            # Aviator Ledger CSV (depart: header only, land: row + rename with timestamp)
                            if self.aviator_ledger_export and self.aircraft_status_obj:
                                _ledger_obj = self.aircraft_status_obj
                                _arr_elev = _ledger_obj.arrival_airport.get_altitude()
                                _land_ceiling_msl = _ledger_obj._arrival_landing_ceiling_msl()
                                _cruise_floor = _ledger_obj.cruise_altitude - 2000  # sync aircraft_status._CRUISE_CLEARANCE_BELOW_FT
                                _wip_name = (
                                    os.path.basename(self._ledger_csv_temp_path)
                                    if self._ledger_csv_temp_path
                                    else None
                                )
                                terminal_debug_print(
                                    "ledger",
                                    (
                                        f"[DEBUG LEDGER] sm_ret={ar_status!r} sm_state_before_tick={previous_state!r} "
                                        f"internal_now={_ledger_obj.get_state()!r} spd_kts={speed_knots:.1f} "
                                        f"alt_ft={_ledger_obj.get_altitude():.0f} arr_elev_ft={_arr_elev:.0f} "
                                        f"land_band_top_msl_<={_land_ceiling_msl:.0f} "
                                        f"airborne_bridge_need_alt_below_cruise_floor={_cruise_floor:.0f} "
                                        f"sent_dep_csv={self.sent_departure_message} sent_land_csv={self.sent_arrival_message} "
                                        f"wip_file={_wip_name!r} dep_t={_ledger_obj.departure_time!r} arr_t={_ledger_obj.arrival_time!r}"
                                    ),
                                )
                                if ar_status == "departing" and not self.sent_departure_message:
                                    self.sent_departure_message = True
                                    self.sent_arrival_message = False
                                    terminal_debug_print("ledger", "[DEBUG LEDGER] Trigger: first departing tick → write CSV header (WIP).")
                                    self._aviator_ledger_on_departing()
                                elif ar_status == "landed" and not self.sent_arrival_message:
                                    self.sent_arrival_message = True
                                    self.sent_departure_message = False
                                    terminal_debug_print("ledger", "[DEBUG LEDGER] Trigger: first landed tick → append row and rename CSV.")
                                    self._aviator_ledger_on_landed()
                                elif (
                                    self._ledger_csv_temp_path
                                    and ar_status == "ground"
                                    and previous_state != "ground"
                                    and not self._ledger_warned_wip_cleared_by_ground
                                ):
                                    self._ledger_warned_wip_cleared_by_ground = True
                                    terminal_debug_print(
                                        "ledger",
                                        "[DEBUG LEDGER] Warning: WIP CSV still open but state went to ground "
                                        f"(prev={previous_state!r}). Landing may never have produced a 'landed' tick "
                                        "(state machine requires descending → landed when altitude ≤ arrival_elev×1.2 before speed hits 0). "
                                        "Check arr_elev_ft vs simulated altitude in lines above.",
                                    )
                    else:
                        # No airport information, set status to unknown
                        if self.aviator_ledger_export and not self._ledger_warned_missing_airports:
                            terminal_debug_print(
                                "ledger",
                                "[DEBUG LEDGER] Ledger enabled but departure/arrival airports missing in flight plan; "
                                "no internal state machine / CSV hooks running.",
                            )
                            self._ledger_warned_missing_airports = True
                        print(f"No airport information, setting status to unknown")
                        self.current_aircraft_status = "unknown"
                    # Send HTTP PUT request
                    try:
                        terminal_debug_print("atc", f"[DEBUG ATC] PUT {self.api_url}")
                        response = self.http_session.put(
                            self.api_url,
                            json=payload,
                            timeout=HTTP_TIMEOUT
                        )
                        response.raise_for_status()  # Raise exception for bad status codes
                        
                        terminal_debug_print("atc", f"[DEBUG ATC] Position update sent successfully: status={response.status_code}")
                        if hasattr(response, 'text') and response.text:
                            try:
                                response_json = response.json()
                                terminal_debug_print("atc", f"[DEBUG ATC] Server response: {response_json}")
                            except:
                                terminal_debug_print("atc", f"[DEBUG ATC] Server response (text): {response.text[:200]}")
                        
                        self.messages_sent += 1
                        self.last_send_time = current_time
                        terminal_debug_print("atc", f"[DEBUG ATC] Updated last_send_time to {self.last_send_time:.2f}, next update in {UPDATE_INTERVAL}s")
                    
                    except requests.exceptions.RequestException as e:
                        self.http_errors += 1
                        if self.running:
                            terminal_debug_print("atc", f"[DEBUG ATC] ✗ HTTP error sending position: {e}", always=True)
                            if hasattr(e, 'response') and e.response is not None:
                                terminal_debug_print("atc", f"[DEBUG ATC]   Response: {e.response.status_code} - {e.response.text[:100]}", always=True)
                
                # Sleep a bit to avoid busy waiting
                time.sleep(0.1)
                
            except Exception as e:
                if self.running:
                    terminal_debug_print("atc", f"[DEBUG ATC] Error in send loop: {e}", always=True)
                    import traceback
                    traceback.print_exc()
                    time.sleep(1)  # Back off on error
    
    def start(self) -> None:
        """Start the client sender."""
        # Start simulator data source (only if not in ATC mode)
        if not self.atc_mode_enabled and self.data_source and self._owns_data_source:
            self.data_source.start()
        elif self.atc_mode_enabled:
            terminal_debug_print("atc", "[DEBUG ATC] Skipping data source start (ATC mode - using fixed position)")
        
        # Follow API v2.0 connection flow:
        # 1. Health Check (optional)
        try:
            test_response = self.http_session.get(
                f"{self.server_url}/api/health",
                timeout=HTTP_TIMEOUT
            )
            if test_response.status_code == 200:
                print(f"Server health check: OK")
        except requests.exceptions.RequestException as e:
            print(f"⚠ Warning: Could not reach server health endpoint: {e}")
            print("  Continuing anyway - will retry on first position update...")
        
        # 2. Register Flight Plan (before initial position)
        terminal_debug_print("atc", "[DEBUG ATC] Registering flight plan...")
        flight_plan_success = self.register_flight_plan()
        if flight_plan_success:
            terminal_debug_print("atc", "[DEBUG ATC] Flight plan registered successfully")
        else:
            terminal_debug_print("atc", "[DEBUG ATC] ⚠ Warning: Flight plan registration failed, continuing anyway...", always=True)
        
        # 3. Initial Position (register client in system)
        # Send initial position with 0,0 to register client
        try:
            initial_payload = {
                "latitude": 0.0,
                "longitude": 0.0,
                "altitude": 0.0,
                "track": 0.0,
                "ground_speed": 0.0,
                "vertical_speed": 0.0,  # Feet per minute
                "timestamp": time.time(),
                "squawk_code": self.squawk_code  # API v2.2: Include squawk code in initial position
            }
            initial_response = self.http_session.put(
                self.api_url,
                json=initial_payload,
                timeout=HTTP_TIMEOUT
            )
            initial_response.raise_for_status()
            terminal_debug_print("atc", "[DEBUG ATC] Initial position registered (0,0)")
            # Set last_send_time to allow immediate first real position update
            self.last_send_time = 0  # This ensures the first real update will be sent immediately
        except requests.exceptions.RequestException as e:
            terminal_debug_print("atc", f"[DEBUG ATC] ⚠ Warning: Could not register initial position: {e}", always=True)
            terminal_debug_print("atc", "[DEBUG ATC]   Continuing anyway - will retry on first position update...", always=True)
            self.last_send_time = 0  # Still set to 0 to allow first update
        
        # 4. Start send thread (position updates)
        self.running = True
        self.send_thread = threading.Thread(target=self.send_loop, daemon=True)
        self.send_thread.start()
        terminal_debug_print("atc", "[DEBUG ATC] Send thread started - position updates will be sent every 1 second")
        
        # Note: The blocking while True loop is removed for GUI usage
        # The GUI handles the main loop, and the send_thread runs in the background
    
    def stop(self) -> None:
        """Stop the client."""
        self.running = False
        
        wip = getattr(self, "_ledger_csv_temp_path", None)
        if wip:
            try:
                if os.path.isfile(wip):
                    os.remove(wip)
                    terminal_debug_print("ledger", f"[DEBUG LEDGER] Removed unfinished WIP on disconnect/stop: {wip}")
                else:
                    terminal_debug_print("ledger", f"[DEBUG LEDGER] WIP path was set but file missing on stop: {wip}")
            except OSError as rm_err:
                terminal_debug_print("ledger", f"[DEBUG LEDGER] Could not remove WIP on stop ({wip}): {rm_err}", always=True)
            self._ledger_csv_temp_path = None
        
        # Stop simulator data source only if we own it (created it)
        if self.data_source and self._owns_data_source:
            self.data_source.stop()
        elif self.data_source:
            pass  # External data source - caller owns lifecycle
        else:
            terminal_debug_print("atc", "[DEBUG ATC] No data source to stop (ATC mode)")
        
        if self.send_thread:
            self.send_thread.join(timeout=2)
        
        # Send explicit disconnect
        try:
            self.http_session.delete(self.api_url, timeout=HTTP_TIMEOUT)
            print("Sent disconnect to server")
        except requests.exceptions.RequestException:
            # Ignore errors on disconnect
            pass
        
        if self.http_session:
            self.http_session.close()
    
    def print_status(self) -> None:
        """Print current status."""
        uptime = time.time() - self.start_time
        success_rate = (self.messages_sent / (self.messages_sent + self.http_errors) * 100) if (self.messages_sent + self.http_errors) > 0 else 0
        
        latest_gps = self.data_source.get_latest_gps() if self.data_source else None
        if latest_gps:
            print(f"Last position: {latest_gps.latitude:.4f}, {latest_gps.longitude:.4f}")
        else:
            print("No GPS data received yet")
        
        print("=" * 60 + "\n")
    
    def get_aircraft_status(self) -> str:
        """Get the current aircraft status string."""
        return self.current_aircraft_status
    
    def update_flightplan_route(self, route: Optional[str]) -> None:
        """
        Update flight plan route text (API v2.2).
        
        Args:
            route: Raw route text from user input (will be sanitized when sent)
        """
        self.flightplan_route = route
    
    def update_squawk_code(self, squawk: Optional[str]) -> None:
        """
        Update squawk code (API v2.2).
        
        Args:
            squawk: Raw squawk code from user input (will be validated and padded)
        """
        self.squawk_code = self.validate_and_pad_squawk_code(squawk)
    
    def _aviator_ledger_on_departing(self) -> None:
        """Create a work-in-progress CSV with header only (same directory as exe / script)."""
        base = _get_base_dir()
        wip = os.path.join(base, f"AviatorLedger_WIP_{uuid.uuid4().hex[:8]}.csv")
        try:
            with open(wip, "w", encoding="utf-8", newline="") as f:
                write_ledger_csv_line(f, ledger_column_names())
            self._ledger_csv_temp_path = wip
            self._ledger_warned_wip_cleared_by_ground = False
            terminal_debug_print("ledger", f"[DEBUG LEDGER] WIP CSV created (header only): {wip}")
        except OSError as e:
            terminal_debug_print("ledger", f"[DEBUG LEDGER] ERROR: could not create temp CSV: {e}", always=True)
            self._ledger_csv_temp_path = None
    
    def _aviator_ledger_on_landed(self) -> None:
        """Append the flight row and rename to timestamped final name."""
        obj = self.aircraft_status_obj
        if not obj or not self._ledger_csv_temp_path:
            terminal_debug_print(
                "ledger",
                "[DEBUG LEDGER] Skip finalize: "
                + ("no aircraft_status_obj" if not obj else "no WIP path (nothing to finalize)"),
            )
            self._ledger_csv_temp_path = None
            return
        aircraft_tok = sanitize_aircraft_token(self.airplane_model)
        if not aircraft_tok:
            terminal_debug_print("ledger", "[DEBUG LEDGER] WARNING: airplane_model empty → CSV aircraft column will be blank.")
        row = build_ledger_csv_row(
            obj.get_departure_airport_name(),
            obj.get_arrival_airport_name(),
            aircraft_tok,
            obj.departure_time or "",
            obj.arrival_time or "",
            obj.aircraft_number or "",
        )
        land_utc = datetime.now()
        wip = self._ledger_csv_temp_path
        try:
            with open(wip, "a", encoding="utf-8", newline="") as f:
                write_ledger_csv_line(f, row)
            final_name = ledger_final_basename(
                obj.get_departure_airport_name(),
                obj.get_arrival_airport_name(),
                land_utc,
            )
            final_path = os.path.join(_get_base_dir(), final_name)
            if os.path.isfile(final_path):
                stem, ext = os.path.splitext(final_name)
                final_path = os.path.join(_get_base_dir(), f"{stem}_{uuid.uuid4().hex[:6]}{ext}")
            os.replace(wip, final_path)
            self._ledger_csv_temp_path = None
            terminal_debug_print("ledger", f"[DEBUG LEDGER] Final CSV written: {final_path}")
            terminal_debug_print("ledger", f"[DEBUG LEDGER] Row data: {row}")
        except OSError as e:
            terminal_debug_print("ledger", f"[DEBUG LEDGER] ERROR: could not finalize CSV ({wip} → disk): {e}", always=True)


def main():
    """Main entry point."""
    parser = argparse.ArgumentParser(
        description="Abflug Client - Forward Aerofly data to multiplayer server (API v2.0)"
    )
    parser.add_argument(
        '--api-key',
        required=False,
        default=None,
        help='Your API key for authentication (will try butter.json if not provided)'
    )
    parser.add_argument(
        '--server',
        required=False,
        default=None,
        help='Server URL (e.g., https://abflug.cloud, http://abflug.cloud, or your-server.com). Will use config file if not provided.'
    )
    parser.add_argument(
        '--use-https',
        action='store_true',
        help='Use HTTPS instead of HTTP (default: HTTP)'
    )
    parser.add_argument(
        '--aerofly-port',
        type=int,
        default=DEFAULT_AEROFLY_PORT,
        help=f'Port to receive Aerofly data (default: {DEFAULT_AEROFLY_PORT})'
    )
    parser.add_argument(
        '--callsign',
        type=str,
        default=None,
        help='Your callsign (optional, sent via flight plan with highest precedence)'
    )
    parser.add_argument(
        '--use-fswidget',
        action='store_true',
        help='Use FSWidget TCP protocol instead of Aerofly UDP (default: UDP)'
    )
    parser.add_argument(
        '--fswidget-ip',
        type=str,
        default='localhost',
        help='IP address for FSWidget TCP connection (default: localhost)'
    )
    parser.add_argument(
        '--aviator-ledger',
        action='store_true',
        help='Write Aviator Ledger import CSV beside the executable / script dir (header on depart, row + timestamped name on land)'
    )
    parser.add_argument(
        '--departure-airport',
        type=str,
        default=None,
        help='Departure airport ICAO code (optional, e.g., LOWG)'
    )
    parser.add_argument(
        '--arrival-airport',
        type=str,
        default=None,
        help='Arrival airport ICAO code (optional, e.g., EDDF)'
    )
    parser.add_argument(
        '--airplane-model',
        type=str,
        default=None,
        help='Aircraft model (optional, e.g., Boeing 737-800)'
    )
    parser.add_argument(
        '--airplane-livery',
        type=str,
        default=None,
        help='Aircraft livery/airline (optional, e.g., United Airlines)'
    )
    parser.add_argument(
        '--cruise-altitude',
        type=str,
        default=None,
        help='Cruise altitude in feet or FL notation (optional, e.g., 30000, FL300, FL270). Default: 30000'
    )
    parser.add_argument(
        '--auto-detect',
        action='store_true',
        help='Automatically detect simulator IP address on local network (works for both UDP and FSWidget)'
    )
    parser.add_argument(
        '--flightplan-route',
        type=str,
        default=None,
        help='Flight plan route text (max 500 chars, alphanumeric + spaces only, optional). Example: "KSFO SID ROUTE STAR KLAX"'
    )
    parser.add_argument(
        '--squawk-code',
        type=str,
        default=None,
        help='Squawk code (4 digits, 0000-9999, optional). Defaults to "0000" if not provided or invalid.'
    )
    parser.add_argument(
        '--terminal-debug',
        metavar='CHANNELS',
        default=None,
        help=(
            'Filter [DEBUG ATC]/[DEBUG LEDGER]/[DEBUG REWINGER] lines. Comma-separated: atc, ledger, rewinger '
            '(alias client=atc). Use "none" for quiet (critical errors still print). '
            'Default: full debug; butter.json "terminal_debug" when unset; ABFLUG_TERMINAL_DEBUG env overrides JSON. '
            'CLI flags here override env for this process.'
        ),
    )
    parser.add_argument(
        '--quiet-terminal',
        action='store_true',
        help='Shortcut for --terminal-debug none',
    )
    args = parser.parse_args()
    
    from terminal_debug import apply_cli_terminal_debug

    apply_cli_terminal_debug(terminal_debug=args.terminal_debug, quiet_terminal=args.quiet_terminal)
    # Get API key from command line or config file
    api_key = args.api_key
    if not api_key:
        api_key = get_butter_api_key()
        if api_key:
            print("Using API key from config file")
        else:
            print("Error: No API key provided and config file not found or has no API key")
            print("Please provide --api-key or create butter.json/abflug.json with an 'api_key' field")
            return 1
    else:
        api_key = api_key.strip()
        if not api_key:
            print("Error: Empty API key provided")
            return 1
    
    # Get server URL from command line or config file
    server_url = args.server
    if not server_url:
        server_url = get_server_url()
        print(f"Using server URL from config file: {server_url}")
    else:
        server_url = server_url.strip()
        if not server_url:
            print("Error: Empty server URL provided")
            return 1
    
    # Parse cruise altitude (handle FL notation)
    cruise_altitude = None
    if args.cruise_altitude:
        cruise_alt_str = args.cruise_altitude.strip().upper()
        if cruise_alt_str.startswith("FL"):
            # FL notation: FL300 = 30000 ft, FL270 = 27000 ft, FL15 = 1500 ft
            try:
                fl_number = int(cruise_alt_str[2:])
                cruise_altitude = fl_number * 100
            except ValueError:
                print(f"Error: Invalid FL notation '{args.cruise_altitude}'. Use format FL### (e.g., FL300)")
                return 1
        else:
            # Direct feet value
            try:
                cruise_altitude = int(cruise_alt_str)
            except ValueError:
                print(f"Error: Invalid cruise altitude '{args.cruise_altitude}'. Must be a number or FL notation (e.g., 30000 or FL300)")
                return 1
    # Validate and pad squawk code if provided (API v2.2)
    squawk_code = None
    if args.squawk_code:
        squawk_code = AbflugClient.validate_and_pad_squawk_code(args.squawk_code)
        if squawk_code == "0000" and args.squawk_code.strip() != "0000":
            print(f"Warning: Invalid squawk code '{args.squawk_code}', defaulting to '0000'")
    
    # Sanitize flightplan route if provided (API v2.2)
    flightplan_route = None
    if args.flightplan_route:
        flightplan_route = AbflugClient.sanitize_flightplan_route(args.flightplan_route)
        if flightplan_route == "NO DATA" and args.flightplan_route.strip():
            print(f"Warning: Flight plan route sanitization resulted in 'NO DATA', original was: '{args.flightplan_route[:100]}...'")
    
    try:
        client = AbflugClient(
            api_key=api_key,
            server_url=server_url,
            aerofly_port=args.aerofly_port,
            use_https=args.use_https,
            callsign=args.callsign,
            use_fswidget=args.use_fswidget,
            fswidget_ip=args.fswidget_ip,
            aviator_ledger_export=args.aviator_ledger,
            departure_airport=args.departure_airport,
            arrival_airport=args.arrival_airport,
            airplane_model=args.airplane_model,
            airplane_livery=args.airplane_livery,
            cruise_altitude=cruise_altitude,
            auto_detect=args.auto_detect,
            flightplan_route=flightplan_route,
            squawk_code=squawk_code
        )
        client.start()
    except Exception as e:
        print(f"Fatal error: {e}")
        import traceback
        traceback.print_exc()
        return 1
    
    return 0


if __name__ == '__main__':
    exit(main())

