"""Professional WiFi Configuration GUI — Network management via SCPI.

Provides a comprehensive tkinter interface for configuring WiFi settings on
CircuitPython devices over USB serial or WiFi/TCP.

Features:
- WiFi enable/disable control
- Network list display with SSID, security, RSSI, channel
- Add/remove fallback networks
- Primary network configuration
- Connection status monitoring (IP, MAC, RSSI, channel)
- Configuration save/load
- Network scanning
- CIRCUITPY mount status warning
- SCPI command console
- Auto-connect with USB/WiFi selection
- Dark / Light theme toggle

IMPORTANT: CIRCUITPY must be unmounted for configuration changes to persist!
Use: diskutil unmount /Volumes/CIRCUITPY (macOS) before adding networks.

(c) 2026 Prof. Flavio ABREU ARAUJO. All rights reserved.
"""

from __future__ import annotations

import argparse
import contextlib
import re
import threading
import time
import tkinter as tk
from tkinter import messagebox, ttk

from neng_scpi_tools.connection_panel import ConnectionPanel  # type: ignore
from neng_scpi_tools.myserial.scpi_universal import SCPIUniversal
from neng_scpi_tools.scpi_client import format_response  # type: ignore

# Global debug flag
_DEBUG_MODE = False


def debug_log(msg: str) -> None:
    """Print debug message if debug mode is enabled."""
    if _DEBUG_MODE:
        print(f"DEBUG: {msg}", flush=True)


def strip_ansi_codes(text: str) -> str:
    """Remove ANSI color codes from text for GUI display."""
    ansi_escape = re.compile(r"\x1b\[[0-9;]*m")
    return ansi_escape.sub("", text)


# ===================================================================
# Theme / Colours
# ===================================================================
THEMES = {
    "dark": {
        "bg": "#2b2b2b",
        "fg": "#ffffff",
        "panel_bg": "#333333",
        "field_bg": "#3c3c3c",
        "console_bg": "#1e1e1e",
        "accent": "#0078d4",
        "success": "#107c10",
        "error": "#d83b01",
        "warning": "#ff8c00",
        "muted": "#888888",
        "value_fg": "#00ccff",
        "btn_bg": "#3c3c3c",
        "btn_active": "#505050",
        "green_btn": "#107c10",
        "green_active": "#0e6b0e",
        "red_btn": "#d83b01",
        "red_active": "#b83200",
        "accent_btn": "#0078d4",
        "accent_active": "#005fa3",
        "yellow_btn": "#ff8c00",
        "yellow_active": "#d97700",
        "list_bg": "#2e2e2e",
        "list_select": "#0078d4",
    },
    "light": {
        "bg": "#f0f0f0",
        "fg": "#1e1e1e",
        "panel_bg": "#e8e8e8",
        "field_bg": "#ffffff",
        "console_bg": "#ffffff",
        "accent": "#0063b1",
        "success": "#0b7a0b",
        "error": "#c50f1f",
        "warning": "#c87d0a",
        "muted": "#666666",
        "value_fg": "#005a9e",
        "btn_bg": "#dcdcdc",
        "btn_active": "#c0c0c0",
        "green_btn": "#0b7a0b",
        "green_active": "#096609",
        "red_btn": "#c50f1f",
        "red_active": "#a00d19",
        "accent_btn": "#0063b1",
        "accent_active": "#004e8c",
        "yellow_btn": "#c87d0a",
        "yellow_active": "#a66a08",
        "list_bg": "#fafafa",
        "list_select": "#0063b1",
    },
}

# Active theme (default dark)
_current_theme = "dark"


def _t() -> dict:
    """Return current theme dict."""
    return THEMES[_current_theme]


class WiFiConfigGUI:
    """Main WiFi Configuration GUI application."""

    # ------------------------------------------------------------------ init
    def __init__(self, root: tk.Tk) -> None:
        self.root = root
        self.root.title("WiFi Configuration (© 2026 Prof. Flavio ABREU ARAUJO)")
        self.root.geometry("900x800")
        self.root.resizable(True, True)
        self.root.configure(bg=_t()["bg"])

        # State
        self.instr: SCPIUniversal | None = None
        self.connected = False
        self._scpi_lock = threading.Lock()  # Serialize all SCPI I/O
        self._refresh_timer_id: str | None = None
        self._refresh_in_progress = False  # Prevent concurrent status refreshes

        # SCPI console history
        self._scpi_history: list[str] = []
        self._scpi_history_idx = 0

        # Network list data
        self.network_data: list[dict] = []  # Scanned networks
        self.fallback_networks: list[str] = []  # Configured fallback networks

        # ----- ttk theme -----
        self._apply_ttk_theme()

        # ----- bottom bar: status + theme toggle (pack FIRST to reserve space) -----
        bottom_bar = ttk.Frame(root)
        bottom_bar.pack(side=tk.BOTTOM, fill=tk.X, padx=6, pady=(0, 4))

        self._status_var = tk.StringVar(value="Disconnected")
        ttk.Label(
            bottom_bar,
            textvariable=self._status_var,
            anchor=tk.W,
            font=("Arial", 9),
            foreground=_t()["muted"],
        ).pack(side=tk.LEFT, fill=tk.X, expand=True)

        self.theme_btn = ttk.Button(bottom_bar, text="☀ Light", command=self._toggle_theme, width=9)
        self.theme_btn.pack(side=tk.RIGHT)

        # ----- layout: left panel | right panel -----
        content = ttk.Frame(root)
        content.pack(fill=tk.BOTH, expand=True, padx=6, pady=6)

        left_panel = ttk.Frame(content, width=450)
        left_panel.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 6))

        right_panel = ttk.Frame(content, width=400)
        right_panel.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True)
        right_panel.pack_propagate(False)

        # ===== LEFT PANEL =====
        self._build_connection_panel(left_panel)
        self._build_wifi_status_panel(left_panel)
        self._build_wifi_control_panel(left_panel)
        self._build_network_scan_panel(left_panel)

        # ===== RIGHT PANEL =====
        self._build_network_config_panel(right_panel)
        self._build_config_actions_panel(right_panel)
        self._build_scpi_console(right_panel)

        # protocol handler
        self.root.protocol("WM_DELETE_WINDOW", self._on_close)

    # ----------------------------------------------------------------
    #  TTK theme helpers
    # ----------------------------------------------------------------
    def _apply_ttk_theme(self) -> None:
        """Apply current theme colours to ttk styles."""
        t = _t()
        style = ttk.Style()
        style.theme_use("clam")
        style.configure(".", background=t["bg"], foreground=t["fg"], fieldbackground=t["field_bg"])
        style.configure("TLabel", background=t["bg"], foreground=t["fg"])
        style.configure("TFrame", background=t["bg"])
        style.configure("TLabelframe", background=t["bg"], foreground=t["fg"])
        style.configure("TLabelframe.Label", background=t["bg"], foreground=t["accent"])
        style.configure("TButton", background=t["btn_bg"], foreground=t["fg"], padding=4)
        style.configure("TEntry", fieldbackground=t["field_bg"], foreground=t["fg"])
        style.map("TButton", background=[("active", t["btn_active"])])
        style.configure("Green.TButton", background=t["green_btn"], foreground="#ffffff")
        style.map("Green.TButton", background=[("active", t["green_active"])])
        style.configure("Red.TButton", background=t["red_btn"], foreground="#ffffff")
        style.map("Red.TButton", background=[("active", t["red_active"])])
        style.configure("Accent.TButton", background=t["accent_btn"], foreground="#ffffff")
        style.map("Accent.TButton", background=[("active", t["accent_active"])])
        style.configure("Yellow.TButton", background=t["yellow_btn"], foreground="#ffffff")
        style.map("Yellow.TButton", background=[("active", t["yellow_active"])])
        style.configure(
            "Control.TLabel", background=t["bg"], foreground=t["fg"], font=("Arial", 10)
        )
        style.configure(
            "Value.TLabel",
            background=t["bg"],
            foreground=t["value_fg"],
            font=("Courier", 11, "bold"),
        )
        style.configure(
            "Small.TLabel", background=t["bg"], foreground=t["muted"], font=("Arial", 8)
        )

    # ================================================================
    #  UI builders
    # ================================================================
    def _build_connection_panel(self, parent: ttk.Frame) -> None:
        self.conn_panel = ConnectionPanel(
            parent,
            default="USB",
            label="Mode:",
            on_connected=self._on_panel_connected,
            on_disconnected=self._on_panel_disconnected,
            on_log=self._console_log,
        )
        self.conn_panel.pack(fill=tk.X, pady=(0, 6))

    def _build_wifi_status_panel(self, parent: ttk.Frame) -> None:
        frm = ttk.LabelFrame(parent, text="WiFi Status", padding=6)
        frm.pack(fill=tk.X, pady=(0, 6))

        labels = [
            ("Available:", "available", "---"),
            ("Enabled:", "enabled", "---"),
            ("Connected:", "connected", "---"),
            ("SSID:", "ssid", "---"),
            ("IP Address:", "ip", "---"),
            ("MAC Address:", "mac", "---"),
            ("RSSI:", "rssi", "---"),
            ("Channel:", "channel", "---"),
        ]
        self._wifi_status_labels: dict[str, tk.StringVar] = {}
        for i, (lbl, key, default) in enumerate(labels):
            ttk.Label(frm, text=lbl, style="Control.TLabel").grid(
                row=i, column=0, sticky=tk.W, pady=1
            )
            var = tk.StringVar(value=default)
            self._wifi_status_labels[key] = var
            ttk.Label(frm, textvariable=var, style="Value.TLabel").grid(
                row=i, column=1, sticky=tk.E, padx=4
            )
        frm.columnconfigure(1, weight=1)

    def _build_wifi_control_panel(self, parent: ttk.Frame) -> None:
        frm = ttk.LabelFrame(parent, text="WiFi Control", padding=6)
        frm.pack(fill=tk.X, pady=(0, 6))

        # Enable / Disable
        btn_row = ttk.Frame(frm)
        btn_row.pack(fill=tk.X, pady=(0, 4))
        self.wifi_enable_btn = ttk.Button(
            btn_row, text="▶ Enable WiFi", command=self._wifi_enable, style="Green.TButton"
        )
        self.wifi_enable_btn.pack(side=tk.LEFT, expand=True, fill=tk.X, padx=(0, 2))
        self.wifi_disable_btn = ttk.Button(
            btn_row, text="⏹ Disable WiFi", command=self._wifi_disable, style="Red.TButton"
        )
        self.wifi_disable_btn.pack(side=tk.LEFT, expand=True, fill=tk.X, padx=(2, 0))

        # Connect / Disconnect
        btn_row2 = ttk.Frame(frm)
        btn_row2.pack(fill=tk.X, pady=(0, 4))
        self.wifi_connect_btn = ttk.Button(
            btn_row2, text="🔗 Connect", command=self._wifi_connect, style="Accent.TButton"
        )
        self.wifi_connect_btn.pack(side=tk.LEFT, expand=True, fill=tk.X, padx=(0, 2))
        self.wifi_disconnect_btn = ttk.Button(
            btn_row2, text="🔌 Disconnect", command=self._wifi_disconnect, style="Red.TButton"
        )
        self.wifi_disconnect_btn.pack(side=tk.LEFT, expand=True, fill=tk.X, padx=(2, 0))

        # Refresh status
        ttk.Button(
            frm, text="🔄 Refresh Status", command=self._refresh_wifi_status, style="Accent.TButton"
        ).pack(fill=tk.X)

    def _build_network_scan_panel(self, parent: ttk.Frame) -> None:
        frm = ttk.LabelFrame(parent, text="Available Networks", padding=6)
        frm.pack(fill=tk.BOTH, expand=True, pady=(0, 6))

        # Scan button
        ttk.Button(
            frm, text="🔍 Scan Networks", command=self._scan_networks, style="Accent.TButton"
        ).pack(fill=tk.X, pady=(0, 4))

        # Network list with scrollbar
        list_frame = ttk.Frame(frm)
        list_frame.pack(fill=tk.BOTH, expand=True)

        scrollbar = ttk.Scrollbar(list_frame)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)

        self.network_listbox = tk.Listbox(
            list_frame,
            yscrollcommand=scrollbar.set,
            bg=_t()["list_bg"],
            fg=_t()["fg"],
            font=("Courier", 9),
            selectbackground=_t()["list_select"],
            selectforeground="#ffffff",
            height=10,
        )
        self.network_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        scrollbar.config(command=self.network_listbox.yview)
        self.network_listbox.bind("<Double-Button-1>", self._on_network_double_click)

        # Add button
        ttk.Button(
            frm,
            text="➕ Add Selected to Fallback",
            command=self._add_selected_network,
            style="Green.TButton",
        ).pack(fill=tk.X, pady=(4, 0))

    def _build_network_config_panel(self, parent: ttk.Frame) -> None:
        frm = ttk.LabelFrame(parent, text="Network Configuration", padding=6)
        frm.pack(fill=tk.BOTH, expand=True, pady=(0, 6))

        # Primary network
        primary_frm = ttk.LabelFrame(frm, text="Primary Network", padding=4)
        primary_frm.pack(fill=tk.X, pady=(0, 6))

        ttk.Label(primary_frm, text="SSID:", style="Control.TLabel").grid(
            row=0, column=0, sticky=tk.W, pady=2
        )
        self.primary_ssid_var = tk.StringVar()
        ttk.Entry(primary_frm, textvariable=self.primary_ssid_var, width=25).grid(
            row=0, column=1, sticky=tk.EW, padx=4
        )

        ttk.Label(primary_frm, text="Password:", style="Control.TLabel").grid(
            row=1, column=0, sticky=tk.W, pady=2
        )
        self.primary_password_var = tk.StringVar()
        ttk.Entry(primary_frm, textvariable=self.primary_password_var, show="*", width=25).grid(
            row=1, column=1, sticky=tk.EW, padx=4
        )

        btn_row = ttk.Frame(primary_frm)
        btn_row.grid(row=2, column=0, columnspan=2, pady=(4, 0), sticky=tk.EW)
        ttk.Button(
            btn_row, text="📤 Set Primary", command=self._set_primary_network, style="Green.TButton"
        ).pack(side=tk.LEFT, expand=True, fill=tk.X, padx=(0, 2))
        ttk.Button(
            btn_row,
            text="📥 Load from File",
            command=self._read_primary_network,
            style="Accent.TButton",
        ).pack(side=tk.LEFT, expand=True, fill=tk.X, padx=(2, 0))

        primary_frm.columnconfigure(1, weight=1)

        # Fallback networks
        fallback_frm = ttk.LabelFrame(frm, text="Fallback Networks", padding=4)
        fallback_frm.pack(fill=tk.BOTH, expand=True, pady=(0, 6))

        # List with scrollbar
        list_frame = ttk.Frame(fallback_frm)
        list_frame.pack(fill=tk.BOTH, expand=True)

        scrollbar = ttk.Scrollbar(list_frame)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)

        self.fallback_listbox = tk.Listbox(
            list_frame,
            yscrollcommand=scrollbar.set,
            bg=_t()["list_bg"],
            fg=_t()["fg"],
            font=("Courier", 10),
            selectbackground=_t()["list_select"],
            selectforeground="#ffffff",
            height=8,
        )
        self.fallback_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        scrollbar.config(command=self.fallback_listbox.yview)
        self.fallback_listbox.bind("<Double-Button-1>", self._on_fallback_double_click)

        # Buttons
        btn_row2 = ttk.Frame(fallback_frm)
        btn_row2.pack(fill=tk.X, pady=(4, 0))
        ttk.Button(
            btn_row2, text="🔄 Refresh", command=self._refresh_fallback_list, style="Accent.TButton"
        ).pack(side=tk.LEFT, expand=True, fill=tk.X, padx=(0, 2))
        ttk.Button(
            btn_row2, text="🗑 Remove", command=self._remove_fallback_network, style="Red.TButton"
        ).pack(side=tk.LEFT, expand=True, fill=tk.X, padx=(2, 0))

        # Manual add
        manual_frm = ttk.Frame(fallback_frm)
        manual_frm.pack(fill=tk.X, pady=(4, 0))
        ttk.Label(manual_frm, text="SSID:", style="Small.TLabel").pack(side=tk.LEFT)
        self.manual_ssid_var = tk.StringVar()
        ttk.Entry(manual_frm, textvariable=self.manual_ssid_var, width=12).pack(
            side=tk.LEFT, padx=2
        )
        ttk.Label(manual_frm, text="Password:", style="Small.TLabel").pack(
            side=tk.LEFT, padx=(4, 0)
        )
        self.manual_password_var = tk.StringVar()
        ttk.Entry(manual_frm, textvariable=self.manual_password_var, show="*", width=12).pack(
            side=tk.LEFT, padx=2
        )
        ttk.Button(
            fallback_frm,
            text="➕ Add Manually",
            command=self._add_manual_network,
            style="Green.TButton",
        ).pack(fill=tk.X, pady=(2, 0))

    def _build_config_actions_panel(self, parent: ttk.Frame) -> None:
        frm = ttk.LabelFrame(parent, text="Configuration", padding=6)
        frm.pack(fill=tk.X, pady=(0, 6))

        # Warning label for CIRCUITPY mount
        # self.warning_label = ttk.Label(
        #     frm,
        #     text="⚠ Unmount CIRCUITPY before saving!\n(diskutil unmount /Volumes/CIRCUITPY)",
        #     style="Small.TLabel",
        #     foreground=_t()["warning"],
        #     justify=tk.CENTER,
        # )
        # self.warning_label.pack(fill=tk.X, pady=(0, 4))

        # Save / Load
        btn_row = ttk.Frame(frm)
        btn_row.pack(fill=tk.X)
        ttk.Button(
            btn_row, text="💾 Save Config", command=self._save_config, style="Yellow.TButton"
        ).pack(side=tk.LEFT, expand=True, fill=tk.X, padx=(0, 2))
        ttk.Button(
            btn_row, text="📂 Load Config", command=self._load_config, style="Accent.TButton"
        ).pack(side=tk.LEFT, expand=True, fill=tk.X, padx=(2, 0))

    def _build_scpi_console(self, parent: ttk.Frame) -> None:
        frm = ttk.LabelFrame(parent, text="SCPI Console", padding=4)
        frm.pack(fill=tk.BOTH, expand=True)

        self.console_text = tk.Text(
            frm,
            height=10,
            bg=_t()["console_bg"],
            fg=_t()["fg"],
            font=("Courier", 9),
            state=tk.DISABLED,
            wrap=tk.WORD,
        )
        self.console_text.pack(fill=tk.BOTH, expand=True, pady=(0, 4))

        entry_row = ttk.Frame(frm)
        entry_row.pack(fill=tk.X)
        self.scpi_entry = ttk.Entry(entry_row, width=30)
        self.scpi_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 4))
        self.scpi_entry.bind("<Return>", self._send_scpi)
        self.scpi_entry.bind("<Up>", self._scpi_hist_prev)
        self.scpi_entry.bind("<Down>", self._scpi_hist_next)
        ttk.Button(entry_row, text="Send", command=self._send_scpi, style="Accent.TButton").pack(
            side=tk.LEFT
        )

    # ================================================================
    #  Connection callbacks (called by ConnectionPanel)
    # ================================================================
    def _on_panel_connected(self, instr: SCPIUniversal, idn: str) -> None:
        """Called from worker thread after successful connection."""
        self.instr = instr
        self.connected = True
        self._refresh_wifi_status()
        self._refresh_fallback_list()
        conn_type = instr.connection_type.upper()
        self.root.after(0, lambda: self._set_status(f"Connected ({conn_type}) — {idn}"))

    def _on_panel_disconnected(self) -> None:
        """Called on main thread after disconnection."""
        self.instr = None
        self.connected = False
        self._set_status("Disconnected")
        for key in self._wifi_status_labels:
            self._wifi_status_labels[key].set("---")

    # ================================================================
    #  WiFi actions
    # ================================================================
    def _wifi_enable(self) -> None:
        debug_log("WiFi enable requested")
        self._do_async(":WIFI:ENABLE 1", "WiFi enabled")

    def _wifi_disable(self) -> None:
        debug_log("WiFi disable requested")
        self._do_async(":WIFI:ENABLE 0", "WiFi disabled")

    def _wifi_connect(self) -> None:
        debug_log("WiFi connect requested")
        self._do_async(":WIFI:CONNECT", "WiFi connecting…")
        # Schedule multiple status refreshes to catch connection progress
        debug_log("Scheduling status refreshes at 2s, 5s")
        self.root.after(2000, self._refresh_wifi_status)  # Early check
        self.root.after(7000, self._refresh_wifi_status)  # Mid check

    def _wifi_disconnect(self) -> None:
        debug_log("WiFi disconnect requested")
        self._do_async(":WIFI:DISCONNECT", "WiFi disconnected")
        self.root.after(500, self._refresh_wifi_status)

    def _refresh_wifi_status(self) -> None:
        """Query WiFi status and update display."""

        def _do():
            if not self.connected or not self.instr:
                return

            # Prevent concurrent refreshes - skip if already in progress
            if self._refresh_in_progress:
                debug_log("Status refresh already in progress, skipping")
                return

            self._refresh_in_progress = True
            debug_log("=== Starting WiFi status refresh ===")
            try:
                # Query availability separately (board-specific, not in status)
                available = self._query(":WIFI:AVAILABLE?")
                debug_log(f":WIFI:AVAILABLE? returned: '{available}'")

                # Initialize all fields
                enabled = "0"
                connected = "0"
                ssid = "---"
                ip = "---"
                rssi = "---"
                channel = "---"
                mac = "---"

                # Get comprehensive status from single query
                # Response formats: "CONNECTED,SSID,IP,RSSI,CHANNEL" or "ENABLED,NOT_CONNECTED" or "DISABLED" or "UNAVAILABLE"
                time.sleep(0.1)  # Delay to ensure previous response is cleared
                status_resp = self._query(":WIFI:STATUS?")
                debug_log(f":WIFI:STATUS? returned: '{status_resp}'")

                if status_resp:
                    if status_resp.startswith("CONNECTED,"):
                        # Parse: CONNECTED,SSID,IP,RSSI,CHANNEL
                        enabled = "1"
                        connected = "1"
                        parts = status_resp.split(",")
                        debug_log(f"Parsed into {len(parts)} parts: {parts}")

                        if len(parts) >= 5:
                            ssid = parts[1]
                            ip = parts[2]
                            rssi = parts[3]
                            channel = parts[4]
                            debug_log(
                                f"Extracted - SSID:{ssid}, IP:{ip}, RSSI:{rssi}, CH:{channel}"
                            )
                    elif status_resp == "ENABLED,NOT_CONNECTED":
                        enabled = "1"
                        connected = "0"
                        debug_log("WiFi enabled but not connected")
                    elif status_resp == "DISABLED":
                        enabled = "0"
                        connected = "0"
                        debug_log("WiFi disabled")
                    elif status_resp == "UNAVAILABLE":
                        enabled = "0"
                        connected = "0"
                        available = "0"
                        debug_log("WiFi unavailable")

                # Get MAC address (not in status response)
                time.sleep(0.1)  # Delay to ensure status response is cleared
                mac = self._query(":WIFI:MAC?")
                debug_log(f":WIFI:MAC? returned: '{mac}'")

                # Update labels
                self.root.after(
                    0,
                    lambda: self._wifi_status_labels["available"].set(
                        "Yes" if available == "1" else "No"
                    ),
                )
                self.root.after(
                    0,
                    lambda: self._wifi_status_labels["enabled"].set(
                        "Yes" if enabled == "1" else "No"
                    ),
                )
                self.root.after(
                    0,
                    lambda: self._wifi_status_labels["connected"].set(
                        "Yes" if connected == "1" else "No"
                    ),
                )
                # Update labels with parsed values
                debug_log(f"Setting labels - SSID:'{ssid}', IP:'{ip}', MAC:'{mac}'")
                self.root.after(0, lambda: self._wifi_status_labels["ssid"].set(ssid))
                self.root.after(0, lambda: self._wifi_status_labels["ip"].set(ip))
                self.root.after(0, lambda: self._wifi_status_labels["mac"].set(mac or "---"))
                self.root.after(
                    0,
                    lambda: self._wifi_status_labels["rssi"].set(
                        f"{rssi} dBm" if rssi != "---" and rssi != "None" else "---"
                    ),
                )
                self.root.after(0, lambda: self._wifi_status_labels["channel"].set(channel))

                self._console_log("✓ WiFi status refreshed")
            except Exception as e:
                self._console_log(f"❌ Status refresh failed: {e}")
            finally:
                # Always clear the flag when done
                self._refresh_in_progress = False

        threading.Thread(target=_do, daemon=True).start()

    def _scan_networks(self) -> None:
        """Scan for available WiFi networks."""
        self._console_log("🔍 Scanning networks…")
        debug_log("=== Starting network scan ===")

        def _do():
            if not self.connected or not self.instr:
                self._console_log("❌ Not connected")
                return

            try:
                resp = self._query(":WIFI:SCAN?")
                if not resp:
                    self._console_log("❌ No scan results")
                    return

                debug_log(f"Scan response length: {len(resp)} chars")
                # Parse scan results: SSID1,RSSI1,CH1,SEC1;SSID2,RSSI2,CH2,SEC2;...
                networks = []
                for network_str in resp.split(";"):
                    parts = network_str.split(",")
                    if len(parts) >= 4:
                        networks.append(
                            {
                                "ssid": parts[0].strip(),
                                "rssi": parts[1].strip(),
                                "channel": parts[2].strip(),
                                "security": parts[3].strip(),
                            }
                        )
                        debug_log(
                            f"  Network: {parts[0].strip()} (RSSI:{parts[1].strip()}, CH:{parts[2].strip()})"
                        )

                self.network_data = networks
                self.root.after(0, self._update_network_list)
                self._console_log(f"✓ Found {len(networks)} networks")
            except Exception as e:
                debug_log(f"Scan exception: {e}")
                self._console_log(f"❌ Scan failed: {e}")

        threading.Thread(target=_do, daemon=True).start()

    def _update_network_list(self) -> None:
        """Update network listbox with scan results."""
        self.network_listbox.delete(0, tk.END)
        for net in self.network_data:
            # Format: SSID (RSSI dBm, Ch X, Security)
            line = (
                f"{net['ssid']:25s} {net['rssi']:>4s}dBm Ch{net['channel']:>2s} {net['security']}"
            )
            self.network_listbox.insert(tk.END, line)

    def _on_network_double_click(self, event=None) -> None:
        """Add double-clicked network to fallback list."""
        debug_log("Network list double-click")
        self._add_selected_network()

    def _add_selected_network(self) -> None:
        """Add selected network from scan list to fallback networks."""
        selection = self.network_listbox.curselection()
        if not selection:
            debug_log("Add selected network: no selection")
            messagebox.showwarning("No Selection", "Please select a network from the list.")
            return

        idx = selection[0]
        if idx >= len(self.network_data):
            debug_log(f"Add selected network: invalid index {idx}")
            return

        network = self.network_data[idx]
        ssid = network["ssid"]
        debug_log(f"=== Adding selected network '{ssid}' to fallback ===")

        # Prompt for password
        password = self._prompt_password(ssid)
        if password is None:
            debug_log(f"Add network cancelled for '{ssid}'")
            return  # User cancelled

        debug_log(f"Password entered for '{ssid}', proceeding with add")
        self._add_network_to_device(ssid, password)

    def _prompt_password(self, ssid: str, title_prefix: str = "Password for") -> str | None:
        """Prompt user for WiFi password."""
        dialog = tk.Toplevel(self.root)
        dialog.title(f"{title_prefix} {ssid}")
        dialog.geometry("350x120")
        dialog.configure(bg=_t()["bg"])
        dialog.transient(self.root)
        dialog.wait_visibility()
        dialog.grab_set()

        result = {"password": None}

        ttk.Label(dialog, text=f"Enter password for '{ssid}':", style="Control.TLabel").pack(
            pady=(10, 5)
        )

        password_var = tk.StringVar()
        entry = ttk.Entry(dialog, textvariable=password_var, show="*", width=30)
        entry.pack(pady=5)
        entry.focus()

        def ok():
            result["password"] = password_var.get()
            dialog.destroy()

        def cancel():
            dialog.destroy()

        btn_frame = ttk.Frame(dialog)
        btn_frame.pack(pady=10)
        ttk.Button(btn_frame, text="OK", command=ok, style="Green.TButton", width=10).pack(
            side=tk.LEFT, padx=5
        )
        ttk.Button(btn_frame, text="Cancel", command=cancel, style="Red.TButton", width=10).pack(
            side=tk.LEFT, padx=5
        )

        entry.bind("<Return>", lambda e: ok())
        entry.bind("<Escape>", lambda e: cancel())

        dialog.wait_window()
        return result["password"]

    def _add_network_to_device(self, ssid: str, password: str) -> None:
        """Add network to device via SCPI."""
        debug_log(f"=== Adding network '{ssid}' to device ===")

        def _do():
            if not self.connected or not self.instr:
                self._console_log("❌ Not connected")
                return

            try:
                cmd = f":WIFI:NETWORK:ADD {ssid},{password}"
                debug_log(f"Sending command: {cmd[:30]}...")  # Truncate password
                self._write(cmd)
                time.sleep(0.1)
                # Query for response (non-query commands may return diagnostic messages)
                resp = self._query("*OPC?")
                debug_log(f"*OPC? response: {resp}")
                if resp == "1":
                    self._console_log(f"✓ Network '{ssid}' added to fallback list")
                    self.root.after(0, self._refresh_fallback_list)
                else:
                    self._console_log("⚠ Network add may have failed")
            except Exception as e:
                debug_log(f"Exception: {e}")
                self._console_log(f"❌ Failed to add network: {e}")

        threading.Thread(target=_do, daemon=True).start()

    def _add_manual_network(self) -> None:
        """Add network manually from entry fields."""
        ssid = self.manual_ssid_var.get().strip()
        password = self.manual_password_var.get().strip()

        debug_log(f"Manual network add requested: SSID='{ssid}'")

        if not ssid:
            debug_log("Manual add cancelled: no SSID provided")
            messagebox.showwarning("Missing SSID", "Please enter an SSID.")
            return

        self._add_network_to_device(ssid, password)
        self.manual_ssid_var.set("")
        self.manual_password_var.set("")

    def _refresh_fallback_list(self) -> None:
        """Query and display fallback network list."""
        debug_log("=== Refreshing fallback network list ===")

        def _do():
            if not self.connected or not self.instr:
                debug_log("Fallback refresh skipped: not connected")
                return

            try:
                resp = self._query(":WIFI:NETWORK:LIST?")
                if resp:
                    # Parse network list (comma-separated SSIDs)
                    networks = [n.strip() for n in resp.split(",") if n.strip()]
                    debug_log(f"Fallback networks retrieved: {len(networks)} networks")
                    for net in networks:
                        debug_log(f"  - {net}")
                    self.fallback_networks = networks
                    self.root.after(0, self._update_fallback_listbox)
                else:
                    debug_log("Fallback list empty")
                    self.fallback_networks = []
                    self.root.after(0, self._update_fallback_listbox)
            except Exception as e:
                debug_log(f"Fallback refresh exception: {e}")

        threading.Thread(target=_do, daemon=True).start()

    def _update_fallback_listbox(self) -> None:
        """Update fallback network listbox."""
        self.fallback_listbox.delete(0, tk.END)
        for ssid in self.fallback_networks:
            self.fallback_listbox.insert(tk.END, f"  • {ssid}")

    def _on_fallback_double_click(self, event=None) -> None:
        """Edit fallback network password on double-click."""
        selection = self.fallback_listbox.curselection()
        if not selection:
            debug_log("Fallback double-click: no selection")
            return

        idx = selection[0]
        if idx >= len(self.fallback_networks):
            debug_log(f"Fallback double-click: invalid index {idx}")
            return

        ssid = self.fallback_networks[idx]
        debug_log(f"=== Editing fallback network '{ssid}' ===")

        # Prompt for new password
        password = self._prompt_password(ssid, title_prefix="Update password for")
        if password is None:
            debug_log(f"Password update cancelled for '{ssid}'")
            return  # User cancelled

        debug_log(f"Password update confirmed for '{ssid}'")

        # Remove old entry and add updated one
        def _do():
            if not self.connected or not self.instr:
                self._console_log("❌ Not connected")
                debug_log("Update failed: not connected")
                return

            try:
                # Remove old network
                debug_log(f"Removing old entry for '{ssid}'")
                cmd = f":WIFI:NETWORK:REMOVE {ssid}"
                self._write(cmd)
                time.sleep(0.1)

                # Add with new password
                debug_log(f"Adding updated entry for '{ssid}'")
                cmd = f":WIFI:NETWORK:ADD {ssid},{password}"
                self._write(cmd)
                time.sleep(0.1)

                # Check for errors
                err = self._query(":SYST:ERR?")
                debug_log(f"Error check: {err}")
                if err.startswith("0,"):
                    self._console_log(f"✓ Password updated for network '{ssid}'")
                    debug_log(f"Password update successful for '{ssid}'")
                else:
                    self._console_log(f"⚠ Update may have failed: {err}")
                    debug_log(f"Password update failed: {err}")

                self.root.after(0, self._refresh_fallback_list)
            except Exception as e:
                debug_log(f"Exception during password update: {e}")
                self._console_log(f"❌ Failed to update network: {e}")

        threading.Thread(target=_do, daemon=True).start()

    def _remove_fallback_network(self) -> None:
        """Remove selected fallback network."""
        selection = self.fallback_listbox.curselection()
        if not selection:
            debug_log("Remove network: no selection")
            messagebox.showwarning("No Selection", "Please select a network to remove.")
            return

        idx = selection[0]
        if idx >= len(self.fallback_networks):
            debug_log(f"Remove network: invalid index {idx}")
            return

        ssid = self.fallback_networks[idx]
        debug_log(f"=== Removing fallback network '{ssid}' ===")

        def _do():
            if not self.connected or not self.instr:
                self._console_log("❌ Not connected")
                debug_log("Remove failed: not connected")
                return

            try:
                cmd = f":WIFI:NETWORK:REMOVE {ssid}"
                debug_log(f"Sending remove command for '{ssid}'")
                self._write(cmd)
                time.sleep(0.1)
                self._console_log(f"✓ Network '{ssid}' removed from fallback list")
                debug_log(f"Network '{ssid}' removed successfully")
                self.root.after(0, self._refresh_fallback_list)
            except Exception as e:
                debug_log(f"Remove exception: {e}")
                self._console_log(f"❌ Failed to remove network: {e}")

        threading.Thread(target=_do, daemon=True).start()

    def _set_primary_network(self) -> None:
        """Set primary network credentials and save to device."""
        ssid = self.primary_ssid_var.get().strip()
        password = self.primary_password_var.get().strip()

        if not ssid:
            messagebox.showwarning("Missing SSID", "Please enter an SSID for the primary network.")
            return

        debug_log(f"=== Setting primary network to '{ssid}' ===")

        def _do():
            if not self.connected or not self.instr:
                self._console_log("❌ Not connected")
                return

            try:
                self._console_log(f"→ Setting primary network to: '{ssid}'")

                # Set SSID (sync_mode in SCPIUniversal handles OPC automatically)
                self._write(f":WIFI:SSID {ssid}")
                time.sleep(0.1)

                # Check for errors
                err = self._query(":SYST:ERR?")
                if err and not err.startswith("0,"):
                    self._console_log(f"❌ SSID command error: {err}")
                    return

                # Verify SSID was set in memory
                check_ssid = self._query(":WIFI:SSID?")
                self._console_log(f"  SSID in memory: '{check_ssid}'")
                if check_ssid != ssid:
                    self._console_log(f"❌ SSID mismatch! Expected '{ssid}', got '{check_ssid}'")
                    self._console_log("⚠ Check for special characters or SSID too long")
                    return

                # Set password (sync_mode handles OPC automatically)
                self._write(f":WIFI:PASSWORD {password}")
                time.sleep(0.1)

                # Check for errors
                err = self._query(":SYST:ERR?")
                if err and not err.startswith("0,"):
                    self._console_log(f"❌ Password command error: {err}")
                    return

                # Save configuration to file (sync_mode handles OPC automatically)
                self._console_log("→ Saving configuration to file...")
                self._write(":WIFI:CONFIG:SAVE")
                time.sleep(0.2)

                # Check for errors
                err = self._query(":SYST:ERR?")
                if err and not err.startswith("0,"):
                    self._console_log(f"❌ Config save error: {err}")
                    return

                # Verify by reloading and checking
                self._console_log("→ Reloading from file to verify...")
                self._write(":WIFI:CONFIG:LOAD")
                time.sleep(0.2)
                saved_ssid = self._query(":WIFI:SSID?")
                self._console_log(f"  SSID from file: '{saved_ssid}'")

                if saved_ssid == ssid:
                    self._console_log(f"✓ Primary network '{ssid}' saved and verified")
                else:
                    self._console_log("❌ File save verification FAILED!")
                    self._console_log(f"   Expected: '{ssid}'")
                    self._console_log(f"   Got: '{saved_ssid}'")
                    self._console_log("⚠ Possible causes:")
                    self._console_log("   - Device filesystem is read-only")
                    self._console_log("   - Insufficient storage space")
                    self._console_log("   - File permissions issue")
                    self._console_log("   - Check device console for error messages")
            except Exception as e:
                self._console_log(f"❌ Failed to set primary network: {e}")

        threading.Thread(target=_do, daemon=True).start()

    def _read_primary_network(self) -> None:
        """Read primary network credentials from device configuration file."""
        debug_log("=== Reading primary network from file ===")

        def _do():
            if not self.connected or not self.instr:
                debug_log("Read primary network skipped: not connected")
                return

            try:
                # Load config from file to ensure we get the saved values
                debug_log("Loading config from file...")
                self._query(":WIFI:CONFIG:LOAD")  # consume the ACK response
                time.sleep(0.2)  # Give device time to load from file

                # Query the configured primary SSID
                ssid = self._query(":WIFI:SSID?")
                debug_log(f"Primary SSID from file: '{ssid}'")
                if ssid and ssid.upper() != "NONE" and ssid.strip():
                    self.root.after(0, lambda: self.primary_ssid_var.set(ssid))
                    self._console_log(f"✓ Primary network SSID loaded from file: {ssid}")
                else:
                    debug_log("No primary network configured")
                    self._console_log("⚠ No primary network configured in file")
                    self.root.after(0, lambda: self.primary_ssid_var.set(""))

                # Clear password field (cannot be read for security reasons)
                self.root.after(0, lambda: self.primary_password_var.set(""))
                self._console_log("ℹ Password not retrieved (not queryable for security)")
            except Exception as e:
                debug_log(f"Read primary network exception: {e}")
                self._console_log(f"❌ Failed to read primary network: {e}")

        threading.Thread(target=_do, daemon=True).start()

    def _save_config(self) -> None:
        """Save WiFi configuration to device."""
        debug_log("=== Saving configuration to device ===")
        self._do_async(":WIFI:CONFIG:SAVE", "Configuration saved to device")

    def _load_config(self) -> None:
        """Load WiFi configuration from device."""
        debug_log("=== Loading configuration from device ===")

        def _do():
            self._query(":WIFI:CONFIG:LOAD")  # consume ACK to keep buffer clean
            self._console_log("✓ Configuration loaded from device")

        threading.Thread(target=_do, daemon=True).start()
        self.root.after(500, self._refresh_wifi_status)
        self.root.after(500, self._refresh_fallback_list)

    # ================================================================
    #  SCPI console
    # ================================================================
    def _send_scpi(self, event=None) -> None:
        cmd = self.scpi_entry.get().strip()
        if not cmd:
            return
        self.scpi_entry.delete(0, tk.END)
        self._scpi_history.append(cmd)
        self._scpi_history_idx = len(self._scpi_history)
        self._console_log(f"→ {cmd}")

        # Mask passwords in debug log
        log_cmd = cmd if "PASSWORD" not in cmd.upper() else cmd.split()[0] + " ***"
        debug_log(f"=== SCPI Console command: {log_cmd} ===")

        def _do():
            is_query = cmd.endswith("?")
            is_reset_cmd = cmd.upper() in (":SYST:RES", "*RST")

            debug_log(f"Command type: query={is_query}, reset={is_reset_cmd}")

            if is_query:
                resp = self._query(cmd)
                if resp:
                    formatted = format_response(cmd, resp, pretty=True)
                    formatted_plain = strip_ansi_codes(formatted)
                    self._console_log(formatted_plain)
                    debug_log(f"Console query response: {resp[:100]}")
                else:
                    self._console_log("← (no response)")
                    debug_log("Console query: no response")
            elif is_reset_cmd:
                self._write(cmd)
                self._console_log("✓ Reset command sent — device restarting...")
                debug_log("Reset command sent")
            else:
                self._write(cmd)
                time.sleep(0.1)
                # Check for response (some commands return diagnostic messages)
                try:
                    resp = self._query("*OPC?")
                    if resp == "1":
                        self._console_log(f"✓ {cmd}")
                        debug_log("Console command completed (OPC=1)")
                except Exception as e:
                    self._console_log(f"✓ {cmd}")
                    debug_log(f"Console command OPC check exception: {e}")

        threading.Thread(target=_do, daemon=True).start()

    def _scpi_hist_prev(self, event=None) -> None:
        if self._scpi_history and self._scpi_history_idx > 0:
            self._scpi_history_idx -= 1
            self.scpi_entry.delete(0, tk.END)
            self.scpi_entry.insert(0, self._scpi_history[self._scpi_history_idx])

    def _scpi_hist_next(self, event=None) -> None:
        if self._scpi_history_idx < len(self._scpi_history) - 1:
            self._scpi_history_idx += 1
            self.scpi_entry.delete(0, tk.END)
            self.scpi_entry.insert(0, self._scpi_history[self._scpi_history_idx])
        else:
            self._scpi_history_idx = len(self._scpi_history)
            self.scpi_entry.delete(0, tk.END)

    # ================================================================
    #  Helpers
    # ================================================================
    def _query(self, cmd: str) -> str | None:
        """Thread-safe SCPI query. Returns stripped response or None."""
        debug_log(f"QUERY → {cmd}")
        try:
            with self._scpi_lock:
                if self.instr:
                    resp = self.instr.query(cmd)
                    result = resp.strip() if resp else None
                    debug_log(f"QUERY ← {result}")
                    return result
        except Exception as e:
            debug_log(f"QUERY ERROR: {e}")
            return None
        return None

    def _write(self, cmd: str) -> None:
        """Thread-safe SCPI write."""
        # Truncate password commands in debug log for security
        log_cmd = cmd if "PASSWORD" not in cmd.upper() else cmd.split()[0] + " ***"
        debug_log(f"WRITE → {log_cmd}")
        try:
            with self._scpi_lock:
                if self.instr:
                    self.instr.write(cmd)
                    debug_log("WRITE ✓")
        except Exception as e:
            debug_log(f"WRITE ERROR: {e}")
            self._console_log(f"Write error: {e}")

    def _do_async(self, cmd: str, success_msg: str) -> None:
        """Fire-and-forget SCPI write in a background thread."""
        debug_log(f"Async command: {cmd}")

        def _do():
            self._write(cmd)
            self._console_log(f"✓ {success_msg}")
            debug_log(f"Async complete: {success_msg}")

        threading.Thread(target=_do, daemon=True).start()

    def _console_log(self, msg: str) -> None:
        """Append a line to the SCPI console (thread-safe)."""

        def _add():
            self.console_text.config(state=tk.NORMAL)
            self.console_text.insert(tk.END, msg + "\n")
            self.console_text.see(tk.END)
            self.console_text.config(state=tk.DISABLED)

        self.root.after(0, _add)

    def _set_status(self, msg: str) -> None:
        self._status_var.set(msg)

    # ================================================================
    #  Theme toggle
    # ================================================================
    def _toggle_theme(self) -> None:
        """Switch between dark and light theme."""
        global _current_theme
        _current_theme = "light" if _current_theme == "dark" else "dark"
        t = _t()

        # Update button label
        self.theme_btn.config(text="🌙 Dark" if _current_theme == "light" else "☀ Light")

        # Re-apply ttk styles
        self._apply_ttk_theme()

        # Update root + console colours
        self.root.configure(bg=t["bg"])
        self.console_text.config(bg=t["console_bg"], fg=t["fg"])
        self.network_listbox.config(bg=t["list_bg"], fg=t["fg"], selectbackground=t["list_select"])
        self.fallback_listbox.config(bg=t["list_bg"], fg=t["fg"], selectbackground=t["list_select"])

    def _on_close(self) -> None:
        if self.instr:
            with contextlib.suppress(Exception):
                self.instr.close()
        self.root.destroy()


def main() -> None:
    """Entry point for WiFi configuration GUI."""
    global _DEBUG_MODE

    # Parse command-line arguments
    parser = argparse.ArgumentParser(
        description="WiFi Configuration GUI for CircuitPython devices",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  wifi-config-gui              # Normal mode
  wifi-config-gui --debug      # Debug mode (verbose logging)
  wifi-config-gui -d           # Debug mode (short form)
        """,
    )
    parser.add_argument(
        "-d",
        "--debug",
        action="store_true",
        help="Enable debug mode (verbose console logging)",
    )

    args = parser.parse_args()
    _DEBUG_MODE = args.debug

    if _DEBUG_MODE:
        print("=" * 60, flush=True)
        print("DEBUG MODE ENABLED - Verbose logging active", flush=True)
        print("=" * 60, flush=True)

    root = tk.Tk()
    WiFiConfigGUI(root)
    root.mainloop()


if __name__ == "__main__":
    main()
