#!/usr/bin/env python3
"""
SCPI Client — Interactive SCPI terminal for TMP117 PID Controller.

Send SCPI commands via USB serial or WiFi/TCP. Supports single-command
and interactive modes with tab-completion, command history, pretty-printed
responses, and OPC synchronization.

Usage (installed via pip):
    # Interactive mode
    scpi-client

    # Single command
    scpi-client "*IDN?"
    scpi-client ":SENS:TEMP?"

    # WiFi/TCP mode
    scpi-client -w 192.168.1.100 "*IDN?"
    scpi-client -w 192.168.1.100  # interactive over WiFi

    # Specify serial port
    scpi-client -p /dev/cu.usbmodem1101 "*IDN?"

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

from __future__ import annotations

import argparse
import contextlib
import os
import re
import socket
import sys
import time

# Conditional import of termios (Unix-only)
try:
    import termios
except ImportError:
    # On Windows, termios doesn't exist. Create a placeholder exception class.
    class termios:  # type: ignore
        class error(OSError):
            """Placeholder for termios.error on Windows"""

            pass


import serial  # type: ignore
import serial.tools.list_ports  # type: ignore

# ---------------------------------------------------------------------------
# ANSI helpers
# ---------------------------------------------------------------------------
CYAN = "\033[96m"
YELLOW = "\033[93m"
GREEN = "\033[92m"
MAGENTA = "\033[95m"
RED = "\033[91m"
BLUE = "\033[94m"
RESET = "\033[0m"
BOLD = "\033[1m"
DIM = "\033[2m"

DEFAULT_SCPI_PORT = 5025

# Try to import readline for history / tab-completion
try:
    import readline

    READLINE_AVAILABLE = True
    USING_LIBEDIT = "libedit" in (readline.__doc__ or "")
except ImportError:
    READLINE_AVAILABLE = False
    USING_LIBEDIT = False

HISTORY_FILE = os.path.expanduser("~/.scpi_send_history")


# ===================================================================
# TCP Socket wrapper (mirrors serial.Serial interface)
# ===================================================================
class TCPSocket:
    """TCP socket wrapper that mimics ``serial.Serial`` for SCPI over WiFi."""

    def __init__(self, host: str, port: int = DEFAULT_SCPI_PORT, timeout: float = 2.0):
        self.host = host
        self.tcp_port = port
        self.timeout = timeout
        self.sock: socket.socket | None = None
        self._buffer = b""

    # -- connection --------------------------------------------------------
    def connect(self):
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.sock.settimeout(self.timeout)
        self.sock.connect((self.host, self.tcp_port))

    def close(self):
        if self.sock:
            with contextlib.suppress(Exception):
                self.sock.close()
            self.sock = None

    # -- write / read ------------------------------------------------------
    def write(self, data, sync=True):
        if isinstance(data, str):
            data = data.encode("utf-8")
        if self.sock:
            try:
                self.sock.sendall(data)
            except (BrokenPipeError, ConnectionResetError, OSError) as e:
                raise ConnectionError(f"Connection lost: {e}") from e

    def read(self, size: int = 1) -> bytes:
        if not self.sock:
            return b""
        try:
            if len(self._buffer) >= size:
                data, self._buffer = self._buffer[:size], self._buffer[size:]
                return data
            data = self._buffer
            self._buffer = b""
            with contextlib.suppress(socket.timeout):
                data += self.sock.recv(size - len(data))
            return data
        except (ConnectionResetError, BrokenPipeError, OSError) as e:
            raise ConnectionError(f"Connection lost while reading: {e}") from e

    def readline(self) -> bytes:
        if not self.sock:
            return b""
        try:
            while b"\n" not in self._buffer:
                try:
                    chunk = self.sock.recv(1024)
                    if not chunk:
                        break
                    self._buffer += chunk
                except socket.timeout:
                    break
            if b"\n" in self._buffer:
                line, self._buffer = self._buffer.split(b"\n", 1)
                return line + b"\n"
            line, self._buffer = self._buffer, b""
            return line
        except (ConnectionResetError, BrokenPipeError, OSError) as e:
            raise ConnectionError(f"Connection lost while reading: {e}") from e

    # -- buffer management -------------------------------------------------
    def reset_input_buffer(self):
        self._buffer = b""
        if self.sock:
            self.sock.setblocking(False)
            try:
                while True:
                    chunk = self.sock.recv(1024)
                    if not chunk:
                        break
            except (OSError, BlockingIOError):
                pass
            finally:
                self.sock.setblocking(True)
                self.sock.settimeout(self.timeout)

    def reset_output_buffer(self):
        pass

    def flush(self):
        pass

    @property
    def in_waiting(self) -> int:
        if self._buffer:
            return len(self._buffer)
        if not self.sock:
            return 0
        self.sock.setblocking(False)
        try:
            chunk = self.sock.recv(1024, socket.MSG_PEEK)
            return len(chunk)
        except (OSError, BlockingIOError):
            return 0
        finally:
            self.sock.setblocking(True)
            self.sock.settimeout(self.timeout)


# ===================================================================
# Utility functions
# ===================================================================
def find_circuitpython_port() -> str | None:
    """Auto-detect CircuitPython serial port."""
    ports = serial.tools.list_ports.comports()
    for p in ports:
        if any(kw in p.description.lower() for kw in ("circuitpython", "arduino", "esp32", "nano")):
            return p.device
        if p.manufacturer and "arduino" in p.manufacturer.lower():
            return p.device
    if ports:
        print(f"\n{YELLOW}Available serial ports:{RESET}")
        for i, p in enumerate(ports, 1):
            print(f"  {CYAN}{i}.{RESET} {BOLD}{p.device}{RESET} - {p.description}")
    return None


def parse_wifi_address(addr_str: str) -> tuple[str, int]:
    """Parse ``"host"`` or ``"host:port"`` into *(host, port)* tuple."""
    if ":" in addr_str:
        host, port_str = addr_str.rsplit(":", 1)
        try:
            port = int(port_str)
        except ValueError:
            print(f"Warning: Invalid port '{port_str}', using default {DEFAULT_SCPI_PORT}")
            port = DEFAULT_SCPI_PORT
    else:
        host = addr_str
        port = DEFAULT_SCPI_PORT
    return host, port


# ===================================================================
# Pretty-print helpers
# ===================================================================
def format_response(command: str, response: str, pretty: bool = True) -> str:
    """Pretty-format known SCPI responses for human readability."""
    if not pretty or not response:
        return response

    cmd = command.strip().upper()

    # Detect SCPI error codes (format: -xxx or -xxx,Message)
    if response.strip().startswith("-") and ("," in response or response.strip()[1:].isdigit()):
        try:
            parts = response.strip().split(",", 1)
            error_code = int(parts[0])
            error_msg = parts[1] if len(parts) > 1 else "Unknown error"

            # Common SCPI error codes
            if error_code == -113:
                return f"{RED}✗ Error:{RESET} Undefined header (command not recognized)"
            elif error_code == -109:
                return f"{RED}✗ Error:{RESET} Missing parameter"
            elif error_code == -108:
                return f"{RED}✗ Error:{RESET} Parameter not allowed"
            elif error_code == -102:
                return f"{RED}✗ Error:{RESET} Syntax error"
            else:
                return f"{RED}✗ Error {error_code}:{RESET} {error_msg}"
        except (ValueError, IndexError):
            pass

    # --- Special error handling ---
    # Read-only filesystem error (common when CIRCUITPY is mounted in DEBUG mode)
    if "Read-only filesystem" in response or "CIRCUITPY mounted" in response:
        if cmd.startswith(":SYST:USB:MODE"):
            return (
                f"{RED}✗ USB mode change failed:{RESET} {YELLOW}Filesystem is read-only{RESET}\n"
                f"{DIM}Note: CIRCUITPY is mounted. USB mode can only be changed in usb_config.txt file directly in CIRCUITPY drive.{RESET}"
            )
        else:
            return (
                f"{RED}✗ Write failed:{RESET} {YELLOW}Filesystem is read-only{RESET}\n"
                f"{DIM}Note: CIRCUITPY is mounted (DEBUG mode).{RESET}"
            )

    # --- PID Status (:PID:STAT?) ---
    if cmd in (":PID:STAT?", ":PID:STATUS?"):
        try:
            parts = response.split(",")
            if len(parts) >= 7:
                en, sp, err, out = int(parts[0]), float(parts[1]), float(parts[2]), float(parts[3])
                p, i, d = float(parts[4]), float(parts[5]), float(parts[6])
                si = f"{GREEN}●{RESET}" if en else f"{DIM}○{RESET}"
                st = f"{GREEN}ENABLED{RESET}" if en else f"{DIM}DISABLED{RESET}"
                oc = RED if abs(out) > 0.8 else YELLOW if abs(out) > 0.5 else GREEN
                ob = "█" * int(abs(out) * 10)
                od = "HEAT" if out > 0 else "COOL" if out < 0 else "OFF"
                ec = RED if abs(err) > 2 else YELLOW if abs(err) > 0.5 else GREEN
                r = f"\n{BOLD}{CYAN}PID Controller Status:{RESET}\n"
                r += f"  {si} Status:      {st}\n"
                r += f"  🎯 Setpoint:    {MAGENTA}{sp:6.2f}°C{RESET}\n"
                r += f"  ⚠️  Error:       {ec}{err:+6.2f}°C{RESET}\n"
                r += f"  ⚡ Output:      {oc}{out:+6.2f}{RESET} ({od}) {oc}{ob}{RESET}\n"
                r += f"  📊 PID Terms:\n     P: {p:+7.4f}\n     I: {i:+7.4f}\n     D: {d:+7.4f}"
                return r
        except (ValueError, IndexError):
            pass

    # --- PID Telemetry (:PID:TELE?) ---
    elif cmd in (":PID:TELE?", ":PID:TELEMETRY?"):
        try:
            parts = response.split(",")
            if len(parts) >= 7:
                run, sens = int(parts[0]), int(parts[1])
                tmp, sp, out = float(parts[2]), float(parts[3]), float(parts[4])
                lc, ramp = int(parts[5]), int(parts[6])
                si = f"{GREEN}●{RESET}" if run else f"{DIM}○{RESET}"
                st = f"{GREEN}RUNNING{RESET}" if run else f"{DIM}STOPPED{RESET}"
                oc = RED if abs(out) > 0.8 else YELLOW if abs(out) > 0.5 else GREEN
                ob = "█" * int(abs(out) * 10)
                od = "HEAT" if out > 0 else "COOL" if out < 0 else "OFF"
                er = tmp - sp
                ec = RED if abs(er) > 2 else YELLOW if abs(er) > 0.5 else GREEN
                r = f"\n{BOLD}{CYAN}PID Controller Telemetry:{RESET}\n"
                r += f"  {si} Status:      {st}\n"
                r += f"  🔢 Sensor:       #{sens}\n"
                r += f"  🌡️  Temperature:  {CYAN}{tmp:7.3f}°C{RESET}\n"
                r += f"  🎯 Setpoint:     {MAGENTA}{sp:7.3f}°C{RESET}\n"
                r += f"  ⚠️  Error:        {ec}{er:+7.3f}°C{RESET}\n"
                r += f"  ⚡ Output:       {oc}{out:+7.3f}{RESET} ({od}) {oc}{ob}{RESET}\n"
                r += f"  🔄 Loop count:   {lc}\n"
                r += (
                    f"  🔥 Ramping:      {YELLOW}ACTIVE{RESET}"
                    if ramp
                    else f"  🔥 Ramping:      {DIM}INACTIVE{RESET}"
                )
                return r
        except (ValueError, IndexError):
            pass

    # --- Ramp Status (:PID:RAMP:STAT?) ---
    elif cmd in (":PID:RAMP:STAT?", ":PID:RAMP:STATUS?"):
        try:
            parts = response.split(",")
            if len(parts) >= 9:
                act, ramp, hold, comp = int(parts[0]), int(parts[1]), int(parts[2]), int(parts[3])
                st_t, tgt, csp = float(parts[4]), float(parts[5]), float(parts[6])
                rate, prog = float(parts[7]), float(parts[8])
                if comp:
                    si, st = f"{GREEN}✓{RESET}", f"{GREEN}COMPLETED{RESET}"
                elif hold:
                    si, st = f"{YELLOW}⏸{RESET}", f"{YELLOW}HOLDING{RESET}"
                elif ramp:
                    si, st = f"{YELLOW}▶{RESET}", f"{YELLOW}RAMPING{RESET}"
                elif act:
                    si, st = f"{CYAN}●{RESET}", f"{CYAN}ACTIVE{RESET}"
                else:
                    si, st = f"{DIM}■{RESET}", f"{DIM}IDLE{RESET}"
                r = f"\n{BOLD}{CYAN}Temperature Ramp Status:{RESET}\n  {si} Phase:      {st}\n"
                if act:
                    r += f"  🎯 Target:     {MAGENTA}{tgt:6.2f}°C{RESET}\n"
                    r += f"  📍 Current SP: {CYAN}{csp:6.2f}°C{RESET}\n"
                    r += f"  📈 Rate:       {CYAN}{rate:6.2f}°C/min{RESET}\n"
                    if rate > 0 and ramp:
                        td = abs(tgt - st_t)
                        total = (td / rate) * 60.0
                        el = (prog / 100.0) * total
                        rem = total - el
                        r += f"  ⏱️  Elapsed:    {el:6.1f}s\n  ⏳ Remaining:  {rem:6.1f}s\n"
                    bw = 20
                    fi = int((prog / 100.0) * bw)
                    bar = "█" * fi + "░" * (bw - fi)
                    r += f"  Progress: [{CYAN}{bar}{RESET}] {prog:5.1f}%"
                else:
                    r += f"  {DIM}No active ramp{RESET}"
                return r
        except (ValueError, IndexError) as e:
            return f"{YELLOW}⚠ Ramp Status Parse Error:{RESET}\n{response}\n{DIM}Error: {e}{RESET}"

    # --- WiFi Comprehensive Status (:WIFI:STAT?) ---
    elif cmd == ":WIFI:STAT?":
        try:
            # Parse pipe-separated key:value format
            # Format: HW:1|EN:1|CONN:1|SSID:MyNet|IP:192.168.1.100|RSSI:-45|CH:6|MAC:AA:BB:CC|HOST:tmp117|AUTO:1|FB:2
            data = {}
            for part in response.split("|"):
                if ":" in part:
                    key, value = part.split(":", 1)
                    data[key] = value

            # Check hardware availability
            if data.get("HW") == "0":
                return f"{RED}WiFi Hardware: NOT AVAILABLE{RESET}"

            # Build pretty output
            enabled = data.get("EN") == "1"
            connected = data.get("CONN") == "1"

            # Status indicators
            if connected:
                si = f"{GREEN}📶{RESET}"
                st = f"{GREEN}CONNECTED{RESET}"
            elif enabled:
                si = f"{YELLOW}📡{RESET}"
                st = f"{YELLOW}ENABLED, NOT CONNECTED{RESET}"
            else:
                si = f"{DIM}📵{RESET}"
                st = f"{DIM}DISABLED{RESET}"

            r = f"\n{BOLD}{CYAN}┌─── WiFi Status ───┐{RESET}\n"
            r += f"  {si} Status:      {st}\n"

            # Connection details
            if connected:
                ssid = data.get("SSID", "Unknown")
                ip = data.get("IP", "Unknown")
                r += f"  🌐 SSID:        {CYAN}{ssid}{RESET}\n"
                r += f"  📍 IP Address:  {MAGENTA}{ip}{RESET}\n"

                # Signal strength (if available)
                if "RSSI" in data:
                    try:
                        rssi = int(data["RSSI"])
                        if rssi > -50:
                            sig_color, sig_qual = GREEN, "Excellent"
                        elif rssi > -60:
                            sig_color, sig_qual = GREEN, "Good"
                        elif rssi > -70:
                            sig_color, sig_qual = YELLOW, "Fair"
                        else:
                            sig_color, sig_qual = RED, "Weak"
                        r += f"  📊 Signal:      {sig_color}{sig_qual}{RESET} ({rssi} dBm)\n"
                    except ValueError:
                        pass

                # Channel (if available)
                if "CH" in data:
                    r += f"  📡 Channel:     {CYAN}{data['CH']}{RESET}\n"
            else:
                # Show configured SSID if not connected
                cfg_ssid = data.get("CFG", "None")
                if cfg_ssid != "None":
                    r += f"  🌐 Configured:  {DIM}{cfg_ssid}{RESET}\n"

            # Device identity
            mac = data.get("MAC", "Unknown")
            hostname = data.get("HOST", "Unknown")
            r += f"  🔖 MAC:         {CYAN}{mac}{RESET}\n"
            r += f"  💻 Hostname:    {CYAN}{hostname}{RESET}\n"

            # Configuration
            auto = data.get("AUTO") == "1"
            auto_str = f"{GREEN}Yes{RESET}" if auto else f"{DIM}No{RESET}"
            r += f"  🔄 Auto-Connect: {auto_str}\n"

            # Fallback networks
            fb_count = data.get("FB", "0")
            r += f"  📋 Fallback Networks: {CYAN}{fb_count}{RESET}\n"

            r += f"{BOLD}{CYAN}└───────────────────┘{RESET}"
            return r
        except (ValueError, IndexError, KeyError):
            pass

    # --- WiFi Status (:WIFI:STATUS?) ---
    elif cmd == ":WIFI:STATUS?":
        try:
            parts = response.split(",")
            if len(parts) >= 4:
                conn = int(parts[0])
                ssid, ip, rssi = parts[1], parts[2], int(parts[3])
                si = f"{GREEN}📶{RESET}" if conn else f"{DIM}📵{RESET}"
                st = f"{GREEN}CONNECTED{RESET}" if conn else f"{DIM}DISCONNECTED{RESET}"
                if rssi > -50:
                    sig = f"{GREEN}Excellent{RESET} ({rssi} dBm)"
                elif rssi > -60:
                    sig = f"{GREEN}Good{RESET} ({rssi} dBm)"
                elif rssi > -70:
                    sig = f"{YELLOW}Fair{RESET} ({rssi} dBm)"
                else:
                    sig = f"{RED}Weak{RESET} ({rssi} dBm)"
                r = f"\n{BOLD}{CYAN}WiFi Status:{RESET}\n  {si} Status:  {st}\n"
                if conn:
                    r += f"  🌐 Network:  {CYAN}{ssid}{RESET}\n"
                    r += f"  📍 Address:  {MAGENTA}{ip}{RESET}\n"
                    r += f"  📊 Signal:   {sig}"
                return r
        except (ValueError, IndexError):
            pass

    # --- WiFi Scan Results ---
    elif cmd in (":WIFI:SCAN?", ":WIFI:NETWORKS?"):
        try:
            # Format: SSID1,RSSI1,Channel1,AuthMode1;SSID2,RSSI2,Channel2,AuthMode2;...
            networks = response.strip().split(";")
            if networks and networks[0]:
                r = f"\n{BOLD}{CYAN}Available WiFi Networks:{RESET}\n{DIM}{'─' * 60}{RESET}\n"
                for idx, net in enumerate(networks, 1):
                    parts = net.split(",", 3)
                    if len(parts) >= 3:
                        ssid = parts[0].strip()
                        try:
                            rssi = int(parts[1].strip())
                        except ValueError:
                            rssi = 0
                        try:
                            channel = int(parts[2].strip())
                        except ValueError:
                            channel = 0
                        auth = parts[3].strip() if len(parts) > 3 else "Unknown"

                        # Signal strength color and quality
                        if rssi > -50:
                            sig_color = GREEN
                            sig_bar = "█████ Excellent"
                        elif rssi > -60:
                            sig_color = GREEN
                            sig_bar = "████░ Good"
                        elif rssi > -70:
                            sig_color = YELLOW
                            sig_bar = "███░░ Fair"
                        elif rssi > -80:
                            sig_color = YELLOW
                            sig_bar = "██░░░ Weak"
                        else:
                            sig_color = RED
                            sig_bar = "█░░░░ Very Weak"

                        # Security indicators
                        auth_clean = (
                            auth.replace("[", "").replace("]", "").replace("wifi.AuthMode.", "")
                        )
                        if "WPA2" in auth_clean:
                            sec_icon = f"{GREEN}🔒{RESET}"
                            sec_level = "Secure (WPA2)"
                        elif "WPA" in auth_clean:
                            sec_icon = f"{YELLOW}🔓{RESET}"
                            sec_level = "WPA"
                        else:
                            sec_icon = f"{RED}⚠️{RESET}"
                            sec_level = "Open"

                        r += f"  {idx:2d}. {CYAN}{ssid:<30}{RESET} {sec_icon} {sec_level:<20}\n"
                        r += f"      Channel: {channel:2d}  Signal: {sig_color}{sig_bar}{RESET} ({rssi} dBm)\n"

                r += f"{DIM}{'─' * 60}{RESET}"
                return r
        except (ValueError, IndexError, AttributeError):
            pass

    # --- Sensor temperatures ---
    elif cmd in (":PID:TEMPS?", ":PID:TEMP:ALL?"):
        try:
            temps = [float(t) for t in response.split(",")]
            r = f"\n{BOLD}{CYAN}Sensor Temperatures:{RESET}\n"
            for idx, t in enumerate(temps, 1):
                tc = RED if t > 40 else YELLOW if t > 30 else CYAN
                r += f"  🌡️  Sensor {idx}: {tc}{t:6.2f}°C{RESET}\n"
            return r.rstrip()
        except (ValueError, IndexError):
            pass

    # --- Simulation status ---
    elif cmd in (":SIM:STAT?", ":SIM:STATUS?"):
        try:
            pairs = {}
            for part in response.split(","):
                if ":" in part:
                    k, v = part.split(":", 1)
                    pairs[k] = float(v)
            if "ACTUAL" in pairs and "TEC" in pairs and "AMBIENT" in pairs:
                ac, tc, am = pairs["ACTUAL"], pairs["TEC"], pairs["AMBIENT"]
                tcc = RED if abs(tc) > 0.8 else YELLOW if abs(tc) > 0.5 else GREEN
                td = "HEATING" if tc > 0 else "COOLING" if tc < 0 else "OFF"
                tb = "█" * int(abs(tc) * 10)
                r = f"\n{BOLD}{CYAN}Thermal Simulation:{RESET}\n"
                r += f"  🌡️  Actual:   {CYAN}{ac:6.2f}°C{RESET}\n"
                r += f"  🌍 Ambient:  {DIM}{am:6.2f}°C{RESET}\n"
                r += f"  ⚡ TEC:      {tcc}{tc:+6.2f}{RESET} ({td}) {tcc}{tb}{RESET}"
                return r
        except (ValueError, KeyError):
            pass

    # --- :SENS:ALL? ---
    elif cmd in (":SENS:ALL?", ":SENS:TEMP:ALL?"):
        try:
            values = [float(t) for t in response.split(",")]
            if len(values) < 2:
                return response  # Not enough data, return as-is

            # First value is timestamp in seconds, rest are sensor temperatures
            timestamp = values[0]
            temps = values[1:]

            r = f"\n{BOLD}{CYAN}All Sensors:{RESET}\n"
            r += f"  ⏱️  Timestamp:  {DIM}{timestamp:.2f}s{RESET}\n"
            for idx, t in enumerate(temps, 1):
                tc = RED if t > 40 else YELLOW if t > 30 else CYAN
                r += f"  🌡️  Sensor {idx}: {tc}{t:6.2f}°C{RESET}\n"
            return r.rstrip()
        except (ValueError, IndexError):
            pass

    # --- Synchronized sensor measurement ---
    elif cmd.startswith(":SENS:SYNC"):
        try:
            # Format: timestamp,T1,T2,skew_ms,DIFF
            parts = response.split(",")
            if len(parts) >= 5:
                timestamp = float(parts[0])
                t1 = float(parts[1])
                t2 = float(parts[2])
                skew_ms = float(parts[3])
                diff = float(parts[4])

                # Color code temperatures
                t1c = RED if t1 > 40 else YELLOW if t1 > 30 else CYAN
                t2c = RED if t2 > 40 else YELLOW if t2 > 30 else CYAN
                diffc = RED if abs(diff) > 1.0 else YELLOW if abs(diff) > 0.1 else GREEN

                r = f"\n{BOLD}{CYAN}Synchronized Sensor Reading:{RESET}\n"
                r += f"  ⏱️  Timestamp:  {DIM}{timestamp:.3f}s{RESET}\n"
                r += f"  🌡️  Sensor 1:   {t1c}{t1:.4f}°C{RESET}\n"
                r += f"  🌡️  Sensor 2:   {t2c}{t2:.4f}°C{RESET}\n"
                r += f"  ⚡ Skew:       {DIM}{skew_ms:.3f}ms{RESET}\n"
                r += f"  📊 Difference: {diffc}{diff:+.4f}°C{RESET} {DIM}(T1 - T2){RESET}"
                return r
        except (ValueError, IndexError):
            pass

    # --- Single temperature ---
    elif cmd in (":SENS:TEMP?", ":MEAS:TEMP?", ":TEMP?") or (
        cmd.startswith(":SENS") and cmd.endswith(":TEMP?")
    ):
        try:
            t = float(response.strip())
            tc = RED if t > 40 else YELLOW if t > 30 else CYAN
            return f"🌡️  {tc}{t:.2f}°C{RESET}"
        except ValueError:
            pass

    # --- TEC Output ---
    elif cmd in (":TEC:OUTP?", ":TEC:OUTPUT?"):
        try:
            o = float(response.strip())
            oc = RED if abs(o) > 0.8 else YELLOW if abs(o) > 0.5 else GREEN
            ob = "█" * int(abs(o) * 10)
            od = "HEATING" if o > 0 else "COOLING" if o < 0 else "OFF"
            r = f"⚡ TEC Output: {oc}{o:+6.2f}{RESET} ({od})"
            if ob:
                r += f" {oc}{ob}{RESET}"
            return r
        except ValueError:
            pass

    # --- PID Tuning ---
    elif cmd == ":PID:TUNE?":
        try:
            parts = response.split(",")
            if len(parts) >= 3:
                kp, ki, kd = float(parts[0]), float(parts[1]), float(parts[2])
                r = f"\n{BOLD}{CYAN}PID Tuning Parameters:{RESET}\n"
                r += f"  Kp: {GREEN}{kp:.6f}{RESET}\n"
                r += f"  Ki: {GREEN}{ki:.6f}{RESET}\n"
                r += f"  Kd: {GREEN}{kd:.6f}{RESET}"
                return r
        except (ValueError, IndexError):
            pass

    # --- Simple one-liners ---
    elif cmd in (":PID:SETP?", ":PID:SETPOINT?"):
        try:
            return f"🎯 Setpoint: {MAGENTA}{float(response.strip()):.2f}°C{RESET}"
        except ValueError:
            pass
    elif cmd in (":PID:ENAB?", ":PID:ENABLE?"):
        try:
            en = int(response.strip())
            return (
                f"⚡ PID Control: {GREEN}ENABLED{RESET}"
                if en
                else f"⚡ PID Control: {DIM}DISABLED{RESET}"
            )
        except ValueError:
            pass
    elif cmd in (":PID:SENS?", ":PID:SENSOR?"):
        try:
            return f"🔢 Selected Sensor: #{CYAN}{int(response.strip())}{RESET}"
        except ValueError:
            pass
    elif cmd in (":PID:SENS:COUN?", ":PID:SENS:COUNT?", ":PID:SENSOR:COUNT?", ":SENS:COUNT?"):
        try:
            return f"🔢 Sensor Count: {CYAN}{int(response.strip())}{RESET}"
        except ValueError:
            pass
    elif cmd == ":PID:RAMP?":
        try:
            return (
                f"🔥 Ramp: {YELLOW}ACTIVE{RESET}"
                if int(response.strip())
                else f"🔥 Ramp: {DIM}INACTIVE{RESET}"
            )
        except ValueError:
            pass
    elif cmd == ":WIFI:IP?":
        return f"📍 IP Address: {MAGENTA}{response.strip()}{RESET}"
    elif cmd == ":WIFI:MAC?":
        return f"🔖 MAC Address: {CYAN}{response.strip()}{RESET}"
    elif cmd == ":WIFI:SSID?":
        return f"🌐 SSID: {CYAN}{response.strip()}{RESET}"
    elif cmd == ":WIFI:RSSI?":
        try:
            rssi = int(response.strip())
            if rssi > -50:
                sig = f"{GREEN}Excellent{RESET}"
            elif rssi > -60:
                sig = f"{GREEN}Good{RESET}"
            elif rssi > -70:
                sig = f"{YELLOW}Fair{RESET}"
            else:
                sig = f"{RED}Weak{RESET}"
            return f"📊 Signal Strength: {sig} ({rssi} dBm)"
        except ValueError:
            pass
    elif cmd in (":WIFI:CONNECTED?",):
        try:
            c = int(response.strip())
            return (
                f"📶 WiFi: {GREEN}CONNECTED{RESET}" if c else f"📵 WiFi: {DIM}DISCONNECTED{RESET}"
            )
        except ValueError:
            pass
    elif cmd in (":WIFI:ENAB?", ":WIFI:ENABLE?"):
        try:
            e = int(response.strip())
            return f"📶 WiFi: {GREEN}ENABLED{RESET}" if e else f"📶 WiFi: {DIM}DISABLED{RESET}"
        except ValueError:
            pass
    elif cmd == ":WIFI:AVAILABLE?":
        try:
            a = int(response.strip())
            return (
                f"📶 WiFi Hardware: {GREEN}AVAILABLE{RESET}"
                if a
                else f"📶 WiFi Hardware: {RED}NOT AVAILABLE{RESET}"
            )
        except ValueError:
            pass
    elif cmd == ":WIFI:NETWORK:LIST?":
        # Format: SSID1,SSID2,SSID3 or NONE
        if response.strip().upper() == "NONE":
            return f"{DIM}No fallback networks configured{RESET}"
        try:
            networks = [n.strip() for n in response.split(",") if n.strip()]
            if not networks:
                return f"{DIM}No fallback networks configured{RESET}"
            r = f"\n{BOLD}{CYAN}Configured Fallback Networks:{RESET}\n"
            for idx, ssid in enumerate(networks, 1):
                r += f"  {idx}. {CYAN}{ssid}{RESET}\n"
            return r.rstrip()
        except Exception:
            pass
    elif cmd.startswith(":WIFI:NETWORK:ADD"):
        # Handle WiFi network add responses (may include debug info)
        resp = response.strip()
        # Check last non-empty line (response may have debug print statements first)
        last_line = [line for line in resp.split("\n") if line.strip()][-1] if resp else ""
        if last_line.startswith("WARNING"):
            # Show warning message (e.g., added to memory but not saved to disk)
            return f"{YELLOW}⚠{RESET} {last_line}"
        elif last_line.startswith("OK"):
            # Extract SSID from command (format: :WIFI:NETWORK:ADD ssid,password)
            parts = cmd.split(None, 1)
            if len(parts) > 1 and "," in parts[1]:
                ssid = parts[1].split(",")[0].strip()
                # Check for debug info in response
                if "[DEBUG:" in last_line:
                    # Extract debug info
                    debug_match = re.search(r"\[DEBUG:.*?\]", last_line)
                    if debug_match:
                        debug_info = debug_match.group(0)
                        return f"{GREEN}✓{RESET} Fallback network added: {CYAN}{ssid}{RESET}\n{DIM}{debug_info}{RESET}"
                return f"{GREEN}✓{RESET} Fallback network added: {CYAN}{ssid}{RESET}"
            return f"{GREEN}✓{RESET} Fallback network added"
        elif last_line.startswith("ERROR"):
            # Show error with debug info if present
            return f"{RED}✗{RESET} {last_line}"
        # Fall through to show raw response
    elif cmd.startswith(":WIFI:NETWORK:REM"):
        # Handle WiFi network remove responses
        resp = response.strip()
        # Check last non-empty line (response may have debug print statements first)
        last_line = [line for line in resp.split("\n") if line.strip()][-1] if resp else ""
        if last_line.startswith("WARNING"):
            # Show warning message (e.g., removed from memory but not saved to disk)
            return f"{YELLOW}⚠{RESET} {last_line}"
        elif last_line.startswith("OK"):
            # Extract SSID from command if present
            parts = cmd.split(None, 1)
            ssid = parts[1] if len(parts) > 1 else ""
            if ssid:
                return f"{GREEN}✓{RESET} Fallback network removed: {CYAN}{ssid}{RESET}"
            return f"{GREEN}✓{RESET} Fallback network removed"
        elif last_line.startswith("ERROR"):
            return f"{RED}✗{RESET} {last_line}"
    elif cmd == ":WIFI:CONFIG:SAVE":
        # Handle WiFi config save responses
        resp = response.strip()
        if resp.upper().startswith("OK"):
            return f"{GREEN}✓{RESET} WiFi configuration {GREEN}saved{RESET}"
        else:
            return f"{RED}✗{RESET} {resp}"
    elif cmd == ":WIFI:CONFIG:LOAD":
        # Handle WiFi config load responses
        resp = response.strip()
        if resp.upper().startswith("OK"):
            return f"{GREEN}✓{RESET} WiFi configuration {GREEN}reloaded{RESET}"
        else:
            return f"{RED}✗{RESET} {resp}"
    elif cmd == ":TEC:STOP":
        rl = response.strip().lower()
        if "ok" in rl or rl == "0":
            return f"🛑 TEC: {RED}STOPPED{RESET} (output set to 0)"
        return f"🛑 TEC Stop: {response}"
    elif cmd in (":TEC:LIM?", ":TEC:LIMIT?", ":TEC:LIMITS?"):
        try:
            parts = response.split(",")
            if len(parts) >= 2:
                return f"🔒 TEC Limits: {CYAN}{float(parts[0]):+.2f}{RESET} to {CYAN}{float(parts[1]):+.2f}{RESET}"
        except (ValueError, IndexError):
            pass
    elif cmd.startswith(":SIM:RES"):
        rl = response.strip().lower()
        return (
            f"🔄 Simulation: {GREEN}RESET{RESET}"
            if "ok" in rl
            else f"🔄 Simulation Reset: {response}"
        )
    elif cmd.startswith(":SYST:LOG"):
        if response.strip():
            r = f"\n{BOLD}{CYAN}System Log:{RESET}\n{DIM}{'─' * 60}{RESET}\n"

            # Response may have concatenated log entries like: "[8.38s] INFO:   - cmd1;[8.38s] INFO:   - cmd2;..."
            # Split by timestamp pattern: look for patterns like "[N.NNs] LEVEL:"
            log_text = response.strip()

            # First, try to split by explicit newlines (if firmware already includes them)
            lines = log_text.split("\n")

            # Check if we have concatenated entries (multiple timestamps within lines)
            # This can happen if: (a) single line with multiple entries, OR (b) multi-line with concatenated entries
            has_concatenated = any(line.count("[") > 1 for line in lines)

            if has_concatenated:
                # We have concatenated entries within one or more lines
                # Split each line by timestamp pattern, then flatten the result
                split_pattern = r"(?=\[\d+\.\d+s\]\s+(INFO|ERROR|WARNING|DEBUG|WARN)\s*:)"
                all_entries = []
                for line in lines:
                    if "[" in line:
                        # This line has log entries, split it
                        entries = re.split(split_pattern, line)
                        all_entries.extend([e.strip() for e in entries if e.strip() and "[" in e])
                lines = all_entries

            for line in lines:
                if not line.strip():
                    continue

                # Color code based on log level
                if "[ERROR]" in line or "ERROR" in line.upper():
                    r += f"{RED}{line}{RESET}\n"
                elif "[WARNING]" in line or "WARN" in line.upper():
                    r += f"{YELLOW}{line}{RESET}\n"
                elif "[INFO]" in line:
                    r += f"{CYAN}{line}{RESET}\n"
                elif "[DEBUG]" in line:
                    r += f"{DIM}{line}{RESET}\n"
                else:
                    r += f"{line}\n"

            r += f"{DIM}{'─' * 60}{RESET}"
            return r
        return f"{DIM}(no log entries){RESET}"
    elif cmd.startswith(":SYST:CLIENTS"):
        if response.strip():
            r = f"\n{BOLD}{CYAN}TCP SCPI Clients:{RESET}\n{DIM}{'─' * 60}{RESET}\n"

            # Response format: count,IP1:Port1,status1;IP2:Port2,status2;...
            # or: count\nIP1:Port1,status1\nIP2:Port2,status2\n...
            lines = response.strip().split("\n")
            first_line = lines[0]

            try:
                # Try to parse count from first line
                if first_line.startswith("ERROR"):
                    return f"{RED}✗ {first_line}{RESET}"

                parts = first_line.split(",")
                try:
                    count = int(parts[0])
                except ValueError:
                    # If first part is not an int, treat the whole response as one line
                    count = len(lines)
                    lines = [response.strip()]

                if count == 0:
                    r += f"  {DIM}No clients connected{RESET}\n"
                else:
                    r += f"  {CYAN}{count} client{'' if count == 1 else 's'} connected:{RESET}\n\n"

                    client_list = []
                    # Prefer clients from subsequent lines (avoid treating the first line as a client)
                    for line in lines[1:] if len(lines) > 1 and count > 0 else []:
                        if ":" in line and ("," in line or ";" not in line):
                            client_list.append(line.strip())
                        elif ";" in line:
                            # Handle semicolon-separated format
                            for client in line.split(";"):
                                if client.strip():
                                    client_list.append(client.strip())

                    # If still empty and count > 0, parse remainder of the first line after the first comma
                    if not client_list and count > 0 and "," in first_line:
                        remainder = first_line.split(",", 1)[1].strip()
                        if remainder:
                            if ";" in remainder:
                                client_list.extend(
                                    [c.strip() for c in remainder.split(";") if c.strip()]
                                )
                            else:
                                client_list.append(remainder)

                    for idx, client in enumerate(client_list[:count], 1):
                        # Parse client info: IP:Port,status or just IP:Port
                        if "," in client:
                            addr, status = client.split(",", 1)
                            status_str = status.strip()
                        else:
                            addr = client
                            status_str = "connected"

                        # Format address
                        if ":" in addr:
                            ip, port = addr.rsplit(":", 1)
                            try:
                                port_num = int(port)
                            except ValueError:
                                port_num = port
                        else:
                            ip = addr
                            port_num = "?"

                        # Status indicator
                        if status_str.lower() in ("active", "connected", "ok", "1"):
                            status_icon = f"{GREEN}●{RESET}"
                            status_text = f"{GREEN}Connected{RESET}"
                        elif status_str.lower() in ("idle", "0"):
                            status_icon = f"{YELLOW}●{RESET}"
                            status_text = f"{YELLOW}Idle{RESET}"
                        else:
                            status_icon = f"{DIM}●{RESET}"
                            status_text = f"{DIM}{status_str}{RESET}"

                        r += f"  {idx}. {status_icon} {MAGENTA}{ip}{RESET}:{CYAN}{port_num}{RESET} {status_text}\n"

                r += f"{DIM}{'─' * 60}{RESET}"
                return r
            except (ValueError, IndexError) as e:
                return (
                    f"{YELLOW}⚠ Client List Parse Error:{RESET}\n{response}\n{DIM}Error: {e}{RESET}"
                )
        return f"{DIM}(no clients connected){RESET}"
    elif cmd in (":SYST:CAL:SAVE",):
        rl = response.strip().lower()
        return (
            f"💾 Calibration: {GREEN}SAVED{RESET}"
            if ("ok" in rl or "saved" in rl)
            else f"💾 Calibration Save: {response}"
        )
    elif cmd in (":SYST:CAL:LOAD",):
        rl = response.strip().lower()
        return (
            f"📂 Calibration: {GREEN}LOADED{RESET}"
            if ("ok" in rl or "loaded" in rl)
            else f"📂 Calibration Load: {response}"
        )
    elif cmd in (":SYST:CAL:DEL", ":SYST:CAL:DELETE"):
        rl = response.strip().lower()
        return (
            f"🗑️  Calibration: {YELLOW}DELETED{RESET}"
            if ("ok" in rl or "deleted" in rl)
            else f"🗑️  Calibration Delete: {response}"
        )
    elif cmd in (":SYST:MODE?",):
        mode = response.strip().upper()
        if mode == "SIM":
            return f"🔧 System Mode: {YELLOW}SIMULATION{RESET}"
        elif mode == "REAL":
            return f"🔧 System Mode: {GREEN}REAL HARDWARE{RESET}"
        return f"🔧 System Mode: {CYAN}{mode}{RESET}"
    elif cmd.upper().startswith(":SYST:MODE") and not cmd.endswith("?"):
        # Handle mode switch command responses
        resp = response.strip()
        # Check if it's an error code (format: "code,message")
        if resp.startswith("-") or ("," in resp and resp.split(",")[0].lstrip("-").isdigit()):
            # Parse SCPI error
            try:
                parts = resp.split(",", 1)
                code = int(parts[0])
                msg = parts[1] if len(parts) > 1 else "Unknown error"
                if code == -109:
                    return f"{RED}✗ Mode switch failed:{RESET} {YELLOW}Missing or incorrect password{RESET}"
                else:
                    return f"{RED}✗ Mode switch failed:{RESET} {msg} (error {code})"
            except (ValueError, IndexError):
                return f"{RED}✗ Mode switch failed:{RESET} {resp}"
        # If it contains log messages with timestamps, it's a successful mode switch
        elif "[" in resp and "s]" in resp and "INFO" in resp:
            return f"{GREEN}✓{RESET} Mode switch in progress:\n{CYAN}{resp}{RESET}"
        # Otherwise, pass through as-is
        return resp
    # Note: :SYST:USB:MODE is handled by get_command_confirmation (no device response)
    elif cmd in (":SYST:RES", ":SYST:RESET", "*RST") or response == "RESET_COMMAND_SENT":
        return f"🔄 System: {YELLOW}RESETTING...{RESET}"
    elif cmd == "*IDN?":
        parts = response.strip().split(",")
        if len(parts) >= 4:
            r = f"\n{BOLD}{CYAN}Device Identification:{RESET}\n"
            r += f"  Manufacturer: {GREEN}{parts[0].strip()}{RESET}\n"
            r += f"  Model:        {GREEN}{parts[1].strip()}{RESET}\n"
            r += f"  Serial:       {MAGENTA}{parts[2].strip()}{RESET}\n"
            r += f"  Version:      {CYAN}{parts[3].strip()}{RESET}"
            return r
    elif cmd.startswith(":HELP") or cmd.startswith("HELP"):
        return f"\n{BOLD}{CYAN}Help:{RESET}\n{response}"

    # --- OTA Update Status (:SYST:UPD:STATUS?) ---
    elif cmd == ":SYST:UPD:STATUS?":
        try:
            # Parse comma-separated key:value format
            # Format: state:IDLE,authenticated:0,target:none,received:0,expected:0,progress:0.0%
            data = {}
            for part in response.split(","):
                if ":" in part:
                    key, value = part.split(":", 1)
                    data[key] = value.strip()

            state = data.get("state", "UNKNOWN")
            authenticated = data.get("authenticated", "0") == "1"
            target = data.get("target", "none")
            received = data.get("received", "0")
            expected = data.get("expected", "0")
            progress = data.get("progress", "0.0%")

            # State indicator and color
            if state == "IDLE":
                state_icon = f"{DIM}○{RESET}"
                state_str = f"{DIM}IDLE{RESET}"
            elif state == "AUTHENTICATED":
                state_icon = f"{CYAN}●{RESET}"
                state_str = f"{CYAN}AUTHENTICATED{RESET}"
            elif state == "RECEIVING":
                state_icon = f"{YELLOW}▶{RESET}"
                state_str = f"{YELLOW}RECEIVING{RESET}"
            elif state == "VERIFYING":
                state_icon = f"{YELLOW}⏳{RESET}"
                state_str = f"{YELLOW}VERIFYING{RESET}"
            elif state == "COMMITTED":
                state_icon = f"{GREEN}✓{RESET}"
                state_str = f"{GREEN}COMMITTED{RESET}"
            elif state == "FAILED":
                state_icon = f"{RED}✗{RESET}"
                state_str = f"{RED}FAILED{RESET}"
            else:
                state_icon = "?"
                state_str = state

            # Authentication status
            auth_icon = f"{GREEN}✓{RESET}" if authenticated else f"{DIM}✗{RESET}"
            auth_str = f"{GREEN}Yes{RESET}" if authenticated else f"{DIM}No{RESET}"

            # Format output
            r = f"\n{BOLD}{CYAN}OTA Update Status:{RESET}\n"
            r += f"  {state_icon} State:         {state_str}\n"
            r += f"  {auth_icon} Authenticated: {auth_str}\n"
            r += f"  📄 Target:        {MAGENTA}{target}{RESET}\n"
            r += f"  📥 Received:      {CYAN}{received}{RESET} bytes\n"
            r += f"  📊 Expected:      {CYAN}{expected}{RESET} bytes\n"
            r += f"  ⏱️  Progress:      {YELLOW}{progress}{RESET}"

            return r
        except (ValueError, IndexError, KeyError):
            pass

    return response


# ===================================================================
# Command helpers
# ===================================================================
def get_command_confirmation(command: str) -> str:
    """Descriptive confirmation for write (non-query) commands."""
    cu = command.strip().upper()
    parts = cu.split()
    base = parts[0] if parts else cu
    param = " ".join(parts[1:]) if len(parts) > 1 else ""

    # Detect malformed commands (ending with colon indicates incomplete command)
    if base.endswith(":") and not base.startswith("*"):
        return (
            f"{YELLOW}⚠{RESET} Incomplete command: {DIM}{base}{RESET} (missing subcommand or query)"
        )

    if base.startswith(":TEC:OUTP") or base.startswith(":TEC:OUTPUT"):
        if param:
            try:
                v = float(param)
                d = "HEATING" if v > 0 else "COOLING" if v < 0 else "OFF"
                vc = RED if abs(v) > 0.7 else YELLOW if abs(v) > 0.3 else GREEN
                return f"{GREEN}✓{RESET} TEC output set to {vc}{v:+.2f}{RESET} {DIM}({d}){RESET}"
            except ValueError:
                return f"{GREEN}✓{RESET} TEC output set to {CYAN}{param}{RESET}"
        return f"{GREEN}✓{RESET} TEC output configured"
    elif base == ":TEC:STOP":
        return f"{GREEN}✓{RESET} TEC stopped {DIM}(output set to 0){RESET}"
    elif base.startswith(":PID:ENAB"):
        if param in ("1", "ON"):
            return f"{GREEN}✓{RESET} PID control {GREEN}enabled{RESET}"
        elif param in ("0", "OFF"):
            return f"{GREEN}✓{RESET} PID control {YELLOW}disabled{RESET}"
        return f"{GREEN}✓{RESET} PID enable state changed"
    elif base.startswith(":PID:SETP"):
        return (
            f"{GREEN}✓{RESET} PID setpoint set to {MAGENTA}{param}°C{RESET}"
            if param
            else f"{GREEN}✓{RESET} PID setpoint configured"
        )
    elif base == ":PID:KP":
        return (
            f"{GREEN}✓{RESET} Kp set to {CYAN}{param}{RESET}"
            if param
            else f"{GREEN}✓{RESET} Kp configured"
        )
    elif base == ":PID:KI":
        return (
            f"{GREEN}✓{RESET} Ki set to {CYAN}{param}{RESET}"
            if param
            else f"{GREEN}✓{RESET} Ki configured"
        )
    elif base == ":PID:KD":
        return (
            f"{GREEN}✓{RESET} Kd set to {CYAN}{param}{RESET}"
            if param
            else f"{GREEN}✓{RESET} Kd configured"
        )
    elif base.startswith(":PID:TUNE"):
        return (
            f"{GREEN}✓{RESET} PID tuning set: {CYAN}{param}{RESET}"
            if param
            else f"{GREEN}✓{RESET} PID tuning configured"
        )
    elif base.startswith(":PID:RES"):
        return f"{GREEN}✓{RESET} PID controller reset {DIM}(integral term cleared){RESET}"
    elif base.startswith(":PID:SENS") and not base.endswith("?"):
        return (
            f"{GREEN}✓{RESET} PID sensor {CYAN}#{param}{RESET} selected"
            if param
            else f"{GREEN}✓{RESET} PID sensor selected"
        )
    elif base.startswith(":PID:RAMP") and not base.endswith("?"):
        if ":STOP" in base or ":CANC" in base:
            return f"{GREEN}✓{RESET} Temperature ramp {YELLOW}stopped{RESET}"
        if param:
            rp = param.split(",")
            if len(rp) >= 2:
                return f"{GREEN}✓{RESET} Ramp started: target {MAGENTA}{rp[0]}°C{RESET} at {CYAN}{rp[1]}°C/min{RESET} {DIM}(PID auto-enabled){RESET}"
            return f"{GREEN}✓{RESET} Ramp configured: {CYAN}{param}{RESET} {DIM}(PID auto-enabled){RESET}"
        return f"{GREEN}✓{RESET} Temperature ramp configured {DIM}(PID auto-enabled){RESET}"
    elif base.startswith(":SIM:RES"):
        ti = f" at {CYAN}{param}°C{RESET}" if param else f" to {DIM}ambient{RESET}"
        return f"{GREEN}✓{RESET} Thermal simulation reset{ti}"
    elif base.startswith(":SYST:CAL:SAVE"):
        return f"{GREEN}✓{RESET} Calibration {GREEN}saved{RESET}"
    elif base.startswith(":SYST:CAL:LOAD"):
        return f"{GREEN}✓{RESET} Calibration {GREEN}loaded{RESET}"
    elif base.startswith(":SYST:CAL:DEL"):
        return f"{GREEN}✓{RESET} Calibration file {YELLOW}deleted{RESET}"
    elif base.startswith(":SYST:USB:MODE"):
        mode_part = param.split()[0].upper() if param else ""
        return f"{GREEN}✓{RESET} USB mode set to {CYAN}{mode_part or '?'}{RESET} {DIM}(reboot required){RESET}"
    elif base.startswith(":SYST:MODE"):
        # Extract mode from param (e.g., "SIM password" or "REAL password")
        mode_part = param.split()[0].upper() if param else ""
        if mode_part == "SIM":
            return f"{GREEN}✓{RESET} System mode confirmed: {YELLOW}SIMULATION{RESET}"
        elif mode_part == "REAL":
            return f"{GREEN}✓{RESET} System mode confirmed: {GREEN}REAL HARDWARE{RESET}"
        return f"{GREEN}✓{RESET} System mode changed"
    elif base.startswith(":SYST:RES"):
        return f"{GREEN}✓{RESET} System reset {YELLOW}initiated{RESET}"
    elif base == "*RST":
        return f"{GREEN}✓{RESET} Device reset {DIM}(status cleared, sensors reset){RESET}"
    elif base == "*CLS":
        return f"{GREEN}✓{RESET} Status registers cleared"
    elif base == "*OPC":
        return f"{GREEN}✓{RESET} Operation complete flag set"
    elif base.startswith(":WIFI:ENAB"):
        if param in ("1", "ON"):
            return f"{GREEN}✓{RESET} WiFi {GREEN}enabled{RESET}"
        elif param in ("0", "OFF"):
            return f"{GREEN}✓{RESET} WiFi {YELLOW}disabled{RESET}"
        return f"{GREEN}✓{RESET} WiFi state changed"
    elif base.startswith(":WIFI:CONN"):
        return f"{GREEN}✓{RESET} WiFi connection initiated"
    elif base.startswith(":WIFI:DISC"):
        return f"{GREEN}✓{RESET} WiFi {YELLOW}disconnected{RESET}"
    # Note: :WIFI:NETWORK:ADD and :WIFI:NETWORK:REMOVE are handled in format_response()
    # to properly show WARNING/ERROR messages with debug info
    elif base.startswith(":WIFI:CONFIG:SAVE"):
        return f"{GREEN}✓{RESET} WiFi configuration {GREEN}saved{RESET}"
    elif base.startswith(":WIFI:CONFIG:LOAD"):
        return f"{GREEN}✓{RESET} WiFi configuration {GREEN}reloaded{RESET}"
    elif base.startswith(":WIFI:SSID") and not base.endswith("?"):
        if param:
            return f"{GREEN}✓{RESET} WiFi SSID set to: {CYAN}{param}{RESET}"
        return f"{GREEN}✓{RESET} WiFi SSID configured"
    elif base.startswith(":WIFI:HOSTNAME") and not base.endswith("?"):
        if param:
            return f"{GREEN}✓{RESET} Hostname set to: {CYAN}{param}{RESET}"
        return f"{GREEN}✓{RESET} Hostname configured"
    elif base.startswith(":WIFI:PASSWORD") and not base.endswith("?"):
        return f"{GREEN}✓{RESET} WiFi password {DIM}updated{RESET} (not shown for security)"
    elif base.startswith(":WIFI:AUTOCONN") and not base.endswith("?"):
        if param in ("1", "ON"):
            return f"{GREEN}✓{RESET} WiFi auto-connect {GREEN}enabled{RESET}"
        elif param in ("0", "OFF"):
            return f"{GREEN}✓{RESET} WiFi auto-connect {YELLOW}disabled{RESET}"
        return f"{GREEN}✓{RESET} WiFi auto-connect configured"
    return f"{GREEN}✓{RESET} Command completed"


def send_scpi_command(
    ser, command: str, timeout: float = 2.0, retry_count: int = 3, sync_mode: bool = True
) -> str | None:
    """Send an SCPI command with optional OPC synchronization, return response."""
    cmd = command.strip()
    is_query = cmd.endswith("?")
    is_reset_command = cmd.upper() in (":SYST:RES", "*RST")
    is_mode_command = cmd.upper().startswith(":SYST:MODE") and not is_query

    # Some commands take longer to execute, use extended timeout
    if cmd.upper().startswith(":WIFI:SCAN"):
        timeout = 8.0  # WiFi scan can take up to 5-7 seconds
    elif is_mode_command:
        timeout = 6.0  # Mode switching can take time (sensor initialization)

    if sync_mode and not is_query and not cmd.startswith("*") and not is_reset_command:
        cmd_to_send = f"{cmd};*OPC?\n"
    else:
        cmd_to_send = cmd + "\n" if not cmd.endswith("\n") else cmd

    for attempt in range(retry_count):
        # Skip buffer reset for reset commands - they need to be sent immediately
        if not is_reset_command:
            try:
                ser.reset_input_buffer()
            except (BrokenPipeError, ConnectionResetError, ConnectionError, OSError, termios.error):
                # Connection lost or device disconnected, cannot continue
                return "ERROR: Connection lost (device disconnected)"

        try:
            ser.write(cmd_to_send.encode("utf-8"))
            ser.flush()
        except (BrokenPipeError, ConnectionResetError, ConnectionError, OSError) as e:
            # Connection lost while writing
            if attempt == retry_count - 1:
                return f"ERROR: Connection lost while sending command: {e}"
            time.sleep(0.5)
            continue

        # For reset commands, the device will disconnect immediately
        # Return a special message without waiting for response
        if is_reset_command:
            time.sleep(0.2)  # Brief wait for command to be processed
            return "RESET_COMMAND_SENT"

        start = time.time()
        lines: list[str] = []

        while (time.time() - start) < timeout:
            try:
                if ser.in_waiting > 0:
                    try:
                        line = ser.readline().decode("utf-8", errors="replace").strip()
                        if not line:
                            continue
                        if line == cmd or line == cmd_to_send.strip():
                            continue
                        # Special handling for system logs which may span multiple readline() calls
                        if cmd.startswith(":SYST:LOG"):
                            # For logs, collect data and check if more is coming
                            lines.append(line)
                            time.sleep(0.05)
                            if ser.in_waiting > 0:
                                # More data coming, continue reading
                                continue
                            else:
                                # No more data, return everything collected
                                return "\n".join(lines) if len(lines) > 1 else line
                        # Special handling for :SYST:MODE commands - check for errors
                        elif is_mode_command:
                            lines.append(line)
                            # Check if this is an error response
                            if line.startswith("-") or "error" in line.lower():
                                return line
                            # Check if this is an OPC response
                            if line == "1":
                                # Query for errors to see if mode switch actually failed
                                time.sleep(0.1)
                                try:
                                    ser.write(b":SYST:ERR?\n")
                                    time.sleep(0.1)
                                    if ser.in_waiting > 0:
                                        err_resp = (
                                            ser.readline().decode("utf-8", errors="replace").strip()
                                        )
                                        # SCPI error format: "error_code,error_message"
                                        if err_resp and not err_resp.startswith("0,"):
                                            # There was an error, return it instead of success message
                                            return err_resp
                                except Exception:
                                    pass
                                # No error, but might have log messages
                                time.sleep(0.5)  # Wait for potential log messages
                                if ser.in_waiting > 0:
                                    # Collect log messages (filter out internal responses)
                                    while ser.in_waiting > 0:
                                        try:
                                            msg = (
                                                ser.readline()
                                                .decode("utf-8", errors="replace")
                                                .strip()
                                            )
                                            # Filter out OPC response, error query responses, and empty lines
                                            if (
                                                msg
                                                and msg != "1"
                                                and not msg.startswith(":SYST:ERR")
                                                and not msg.startswith("0,")
                                            ):
                                                lines.append(msg)
                                            time.sleep(0.05)
                                        except Exception:
                                            break
                                    # Return collected log messages (skip the first "1" if present)
                                    log_messages = [line for line in lines if line != "1"]
                                    if len(log_messages) > 1:
                                        return "\n".join(log_messages[1:])
                                    elif log_messages:
                                        return log_messages[0]
                                    else:
                                        return get_command_confirmation(cmd)
                                else:
                                    return get_command_confirmation(cmd)
                            # For log messages from mode switch, keep collecting
                            time.sleep(0.2)
                            if ser.in_waiting > 0:
                                continue
                            else:
                                return "\n".join(lines)
                        elif is_query:
                            # For other queries, return immediately after first response
                            return line
                        else:
                            # For commands, check sync mode
                            lines.append(line)
                            if sync_mode:
                                if line == "1":
                                    return get_command_confirmation(cmd)
                                elif line.startswith("-") or "error" in line.lower():
                                    return line
                                elif line.startswith("OK") or line.startswith("ERROR"):
                                    # Device returned explicit OK/ERROR response (may include debug info)
                                    return line
                            else:
                                time.sleep(0.1)
                                if not ser.in_waiting:
                                    return line if lines else get_command_confirmation(cmd)
                    except (BrokenPipeError, ConnectionResetError, ConnectionError, OSError) as e:
                        # Connection lost while reading
                        if attempt == retry_count - 1:
                            return f"ERROR: Connection lost while reading response: {e}"
                        break
            except (BrokenPipeError, ConnectionResetError, ConnectionError, OSError) as e:
                # Connection lost while checking buffer
                if attempt == retry_count - 1:
                    return f"ERROR: Connection lost: {e}"
                break

            time.sleep(0.01)

        if lines:
            return "\n".join(lines)
        if attempt < retry_count - 1:
            time.sleep(0.2)

    return None


# ===================================================================
# Help text
# ===================================================================
def print_help(device_type: str | None = None) -> None:
    """Print common SCPI commands, tailored to the connected instrument."""
    device_label = {
        "relaybank16": "RelayBank16",
        "tmp117": "TMP117",
    }.get(device_type or "", "")
    header = f"Common SCPI Commands — {device_label}" if device_label else "Common SCPI Commands"
    print(f"\n{BOLD}{CYAN}{header}:{RESET}")

    print(f"\n  {BOLD}{YELLOW}Standard Commands (IEEE-488.2):{RESET}")
    print(f"    {GREEN}*IDN?{RESET}           - Get device identification")
    print(f"    {GREEN}*RST{RESET}            - Reset device")
    print(f"    {GREEN}*CLS{RESET}            - Clear status")
    print(f"    {GREEN}*OPC?{RESET}           - Operation complete query")

    if device_type == "relaybank16":
        print(f"\n  {BOLD}{YELLOW}Route — Bulk Relay Control:{RESET}")
        print(f"    {GREEN}:ROUT:CLOS <n[,n,...]>{RESET}  - Close (activate) one or more relays")
        print(f"    {GREEN}:ROUT:OPEN <n[,n,...]>{RESET}  - Open (deactivate) one or more relays")
        print(f"    {GREEN}:ROUT:MASK <0xHHHH>{RESET}     - Set all 16 relays from hex bitmask")
        print(f"    {GREEN}:ROUT:MASK?{RESET}             - Query relay states as 16-bit hex")
        print(f"    {GREEN}:ROUT:MASK:ALL?{RESET}         - Query relay states as 16-char binary")
        print(f"    {GREEN}:ROUT:TEST [dur]{RESET}        - Sequence-test all relays (dur s each)")

        print(
            f"\n  {BOLD}{YELLOW}Individual Relay Control ({DIM}n = 1..16{RESET}{BOLD}{YELLOW}):{RESET}"
        )
        print(f"    {GREEN}:RELA<n>:STAT ON|OFF{RESET}    - Activate / deactivate relay n")
        print(f"    {GREEN}:RELA<n>:STAT?{RESET}          - Query relay n state (ON/OFF)")
        print(f"    {GREEN}:RELA<n>:PULS [dur]{RESET}     - Pulse relay n (optional duration, s)")
        print(f"    {GREEN}:RELA:PULS:DUR <s>{RESET}      - Set default pulse duration (s)")
        print(f"    {GREEN}:RELA:PULS:DUR?{RESET}         - Query default pulse duration")

        print(f"\n  {BOLD}{YELLOW}Diagnostics:{RESET}")
        print(f"    {GREEN}:DIAG:PING?{RESET}             - Connectivity check (returns PONG)")

        print(f"\n  {BOLD}{YELLOW}System:{RESET}")
        print(f"    {GREEN}:SYST:ERR?{RESET}              - Get last error")
        print(f"    {GREEN}:SYST:CLIENTS?{RESET}          - Get connected TCP clients")
        print(f"    {GREEN}:SYST:RES{RESET}               - Reboot device")
        print(f"    {GREEN}:SYST:VERS?{RESET}             - Query firmware version")

    else:
        # TMP117 / generic sections
        print(f"\n  {BOLD}{YELLOW}Temperature Measurement:{RESET}")
        print(f"    {GREEN}:SENS:TEMP?{RESET}     - Read current temperature")
        print(f"    {GREEN}:SENS:ALL?{RESET}      - Read all sensors (with timestamp)")
        print(f"    {GREEN}:SENS:SYNC? [n]{RESET} - Synchronized reading (n samples, default 8)")
        print(f"    {GREEN}:SENS:COUNT?{RESET}    - Get sensor count")

        print(f"\n  {BOLD}{YELLOW}PID Control:{RESET}")
        print(f"    {GREEN}:PID:SENS <n>{RESET}   - Select sensor (0-3)")
        print(f"    {GREEN}:PID:SETP <t>{RESET}   - Set target temperature (°C)")
        print(f"    {GREEN}:PID:ENAB <0|1>{RESET} - Enable/disable PID")
        print(f"    {GREEN}:PID:STAT?{RESET}      - Get PID status")
        print(f"    {GREEN}:PID:TELE?{RESET}      - Get telemetry")
        print(f"    {GREEN}:PID:TEMPS?{RESET}     - Get all sensor temperatures")

        print(f"\n  {BOLD}{YELLOW}Temperature Ramps:{RESET}")
        print(f"    {GREEN}:PID:RAMP <targ>,<rate>[,<unit>][,<hold>]{RESET}  - Start ramp")
        print(f"    {DIM}            unit: MIN (default) or SEC{RESET}")
        print(f"    {DIM}            hold: hold time at target (sec){RESET}")
        print(f"    {GREEN}:PID:RAMP:STAT?{RESET}                         - Detailed ramp status")
        print(f"    {GREEN}:PID:RAMP:STOP{RESET}                          - Stop current ramp")

        print(f"\n  {BOLD}{YELLOW}PID Tuning:{RESET}")
        print(f"    {GREEN}:PID:KP <val>{RESET}   - Set proportional gain")
        print(f"    {GREEN}:PID:KI <val>{RESET}   - Set integral gain")
        print(f"    {GREEN}:PID:KD <val>{RESET}   - Set derivative gain")
        print(f"    {GREEN}:PID:TUNE?{RESET}      - Query all tuning parameters")
        print(f"    {GREEN}:PID:RES{RESET}        - Reset PID (clear integral term)")

        print(f"\n  {BOLD}{YELLOW}TEC Control:{RESET}")
        print(f"    {GREEN}:TEC:OUTP <-1..1>{RESET} - Set TEC output (normalized)")
        print(f"    {GREEN}:TEC:OUTP?{RESET}        - Query TEC output")
        print(f"    {GREEN}:TEC:STOP{RESET}         - Stop TEC (set to 0)")

        print(f"\n  {BOLD}{YELLOW}System:{RESET}")
        print(f"    {GREEN}:SYST:MODE?{RESET}             - Query current mode (SIM/REAL)")
        print(f"    {GREEN}:SYST:MODE <m> <pw>{RESET}     - Switch mode (SIM/REAL, password)")
        print(
            f"    {GREEN}:SYST:USB:MODE <m> <pw>{RESET} - Set USB mode (PROD/DEV/DEBUG, password)"
        )
        print(f"    {GREEN}:SYST:ERR?{RESET}              - Get last error")
        print(f"    {GREEN}:SYST:LOG? [n]{RESET}          - Get system log (last n lines)")
        print(f"    {GREEN}:SYST:CLIENTS?{RESET}          - Get connected TCP clients")

    # End of device-specific sections
    print(f"\n  {BOLD}{YELLOW}WiFi (if available):{RESET}")
    print(f"    {GREEN}:WIFI:STAT?{RESET}              - Comprehensive WiFi status")
    print(f"    {GREEN}:WIFI:STATUS?{RESET}            - WiFi connection status (compact)")
    print(f"    {GREEN}:WIFI:CONNECTED?{RESET}         - Check if connected (1/0)")
    print(f"    {GREEN}:WIFI:IP?{RESET}                - Get IP address")
    print(f"    {GREEN}:WIFI:SCAN?{RESET}              - Scan for available networks")
    print(f"    {GREEN}:WIFI:NETWORK:ADD <ssid>,<pw>{RESET} - Add fallback network")
    print(f"    {DIM}      Example: :WIFI:NETWORK:ADD MyWiFi,password123{RESET}")
    print(f"    {GREEN}:WIFI:NETWORK:REMOVE <ssid>{RESET}  - Remove fallback network")
    print(f"    {GREEN}:WIFI:NETWORK:LIST?{RESET}          - List configured networks")
    print(f"    {GREEN}:WIFI:CONFIG:SAVE{RESET}            - Save WiFi configuration")

    print(f"\n  {BOLD}{YELLOW}Local Commands:{RESET}")
    print(f"    {MAGENTA}help{RESET}         - Show this help")
    print(f"    {MAGENTA}list{RESET}         - List tab-completion commands")
    print(f"    {MAGENTA}sync/nosync{RESET}  - Toggle OPC synchronization")
    print(f"    {MAGENTA}pretty/raw_response{RESET} - Toggle pretty formatting")
    print(f"    {MAGENTA}status{RESET}       - Show current settings")
    print(f"    {MAGENTA}raw{RESET}          - Monitor raw serial data (5 s)")
    print(f"    {MAGENTA}exit{RESET}         - Exit interactive mode")

    print(f"\n  {BOLD}{CYAN}OPC Synchronization:{RESET}")
    print(f"    {DIM}When enabled (default), non-query commands wait for *OPC?{RESET}")
    print(f"    {DIM}response to ensure completion before returning.{RESET}")
    print()


# ===================================================================
# Interactive REPL
# ===================================================================
# Comprehensive tab-completion list
SCPI_COMMANDS = [
    "*IDN?",
    "*RST",
    "*CLS",
    "*OPC?",
    "*OPC",
    "*ESR?",
    "*ESE",
    "*ESE?",
    "*STB?",
    "*SRE",
    "*SRE?",
    "*TST?",
    "*WAI",
    ":SENS:TEMP?",
    ":SENS:ALL?",
    ":SENS:RAW?",
    ":SENS:SER?",
    ":SENS:MEAS?",
    ":SENS:INIT",
    ":SENS:RESET",
    ":SENS:SYNC?",
    ":SENS:SYNC? 1",
    ":SENS:SYNC? 8",
    ":SENS:SYNC? 32",
    ":SENS:SYNC? 64",
    ":SENS:COUNT?",
    ":SENS1:TEMP?",
    ":SENS1:RAW?",
    ":SENS1:SER?",
    ":SENS1:MEAS?",
    ":SENS2:TEMP?",
    ":SENS2:RAW?",
    ":SENS2:SER?",
    ":SENS2:MEAS?",
    ":SENS3:TEMP?",
    ":SENS3:RAW?",
    ":SENS3:SER?",
    ":SENS3:MEAS?",
    ":SENS4:TEMP?",
    ":SENS4:RAW?",
    ":SENS4:SER?",
    ":SENS4:MEAS?",
    ":CONF:HIGH",
    ":CONF:HIGH?",
    ":CONF:LOW",
    ":CONF:LOW?",
    ":CONF:TEMP:OFFS",
    ":CONF:TEMP:OFFS?",
    ":CONF:ALRT:MODE",
    ":CONF:ALRT:MODE?",
    ":CONF:MEAS:AVG",
    ":CONF:MEAS:AVG?",
    ":CONF:MEAS:DEL",
    ":CONF:MEAS:DEL?",
    ":CONF:MEAS:MODE",
    ":CONF:MEAS:MODE?",
    ":PID:SENS",
    ":PID:SENS?",
    ":PID:SENSOR",
    ":PID:SENSOR?",
    ":PID:SENS:COUN?",
    ":PID:SENS:COUNT?",
    ":PID:SENSOR:COUNT?",
    ":PID:SETP",
    ":PID:SETP?",
    ":PID:SETPOINT",
    ":PID:SETPOINT?",
    ":PID:ENAB",
    ":PID:ENAB?",
    ":PID:ENABLE",
    ":PID:ENABLE?",
    ":PID:ENAB ON",
    ":PID:ENAB OFF",
    ":PID:ENAB 1",
    ":PID:ENAB 0",
    ":PID:STAT?",
    ":PID:STATUS?",
    ":PID:TELE?",
    ":PID:TELEMETRY?",
    ":PID:TEMPS?",
    ":PID:TEMP:ALL?",
    ":PID:KP",
    ":PID:KP?",
    ":PID:KI",
    ":PID:KI?",
    ":PID:KD",
    ":PID:KD?",
    ":PID:TUNE",
    ":PID:TUNE?",
    ":PID:RES",
    ":PID:RESET",
    ":PID:RAMP",
    ":PID:RAMP?",
    ":PID:RAMP:STOP",
    ":PID:RAMP:ABORT",
    ":PID:RAMP:STAT?",
    ":PID:RAMP:STATUS?",
    ":TEC:OUTP",
    ":TEC:OUTP?",
    ":TEC:OUTPUT",
    ":TEC:OUTPUT?",
    ":TEC:STOP",
    ":SIM:STAT?",
    ":SIM:STATUS?",
    ":SIM:RES",
    ":SIM:RESET",
    ":SYST:MODE?",
    ":SYST:MODE SIM",
    ":SYST:MODE REAL",
    ":SYST:ERR?",
    ":SYST:ERROR?",
    ":SYST:VERS?",
    ":SYST:VERSION?",
    ":SYST:USB:MODE?",
    ":SYST:USB:MODE PROD",
    ":SYST:USB:MODE DEV",
    ":SYST:USB:MODE DEBUG",
    ":SYST:RES",
    ":SYST:CAL:SAVE",
    ":SYST:CAL:LOAD",
    ":SYST:CAL:DEL",
    ":SYST:LOG?",
    ":SYST:LOG? 50",
    ":SYST:LOG? 100",
    ":SYST:CLIENTS?",
    ":WIFI:ENAB",
    ":WIFI:ENAB?",
    ":WIFI:ENABLE",
    ":WIFI:ENABLE?",
    ":WIFI:CONNECTED?",
    ":WIFI:CONNECT",
    ":WIFI:DISCONNECT",
    ":WIFI:IP?",
    ":WIFI:MAC?",
    ":WIFI:SSID?",
    ":WIFI:RSSI?",
    ":WIFI:STATUS?",
    ":WIFI:STAT?",
    ":WIFI:SCAN?",
    ":WIFI:CHANNEL?",
    ":WIFI:SSID",
    ":WIFI:PASSWORD",
    ":WIFI:PASS",
    ":WIFI:HOSTNAME",
    ":WIFI:HOSTNAME?",
    ":WIFI:AUTOCONNECT",
    ":WIFI:AUTOCONNECT?",
    ":WIFI:AVAILABLE?",
    ":WIFI:CONFIG:SAVE",
    ":WIFI:CONFIG:LOAD",
    ":WIFI:NETWORK:ADD",
    ":WIFI:NETWORK:REMOVE",
    ":WIFI:NETWORK:LIST?",
    "help",
    "exit",
    "quit",
    "q",
    "raw",
    "list",
    "commands",
    "sync",
    "nosync",
    "pretty",
    "raw_response",
    "status",
]

# RelayBank16-specific command list
_RELA_N = [str(n) for n in range(1, 17)]
SCPI_COMMANDS_RELAYBANK16 = [
    # IEEE-488.2
    "*IDN?",
    "*RST",
    "*CLS",
    "*OPC?",
    "*OPC",
    "*ESR?",
    "*ESE",
    "*ESE?",
    "*STB?",
    "*SRE",
    "*SRE?",
    "*TST?",
    "*WAI",
    # Route (bulk relay control)
    ":ROUT:CLOS",
    ":ROUT:OPEN",
    ":ROUT:MASK",
    ":ROUT:MASK?",
    ":ROUT:MASK:ALL?",
    ":ROUT:TEST",
    # Individual relays RELA1-RELA16
    *[f":RELA{n}:STAT ON" for n in _RELA_N],
    *[f":RELA{n}:STAT OFF" for n in _RELA_N],
    *[f":RELA{n}:STAT?" for n in _RELA_N],
    *[f":RELA{n}:PULS" for n in _RELA_N],
    # Pulse duration
    ":RELA:PULS:DUR",
    ":RELA:PULS:DUR?",
    # Diagnostics
    ":DIAG:PING?",
    # System
    ":SYST:ERR?",
    ":SYST:CLIENTS?",
    ":SYST:RES",
    ":SYST:VERS?",
    # WiFi
    ":WIFI:ENAB",
    ":WIFI:ENAB?",
    ":WIFI:ENABLE",
    ":WIFI:ENABLE?",
    ":WIFI:CONNECTED?",
    ":WIFI:CONNECT",
    ":WIFI:DISCONNECT",
    ":WIFI:IP?",
    ":WIFI:MAC?",
    ":WIFI:SSID?",
    ":WIFI:RSSI?",
    ":WIFI:STATUS?",
    ":WIFI:STAT?",
    ":WIFI:SCAN?",
    ":WIFI:CHANNEL?",
    ":WIFI:SSID",
    ":WIFI:PASSWORD",
    ":WIFI:HOSTNAME",
    ":WIFI:HOSTNAME?",
    ":WIFI:AUTOCONNECT",
    ":WIFI:AUTOCONNECT?",
    ":WIFI:AVAILABLE?",
    ":WIFI:CONFIG:SAVE",
    ":WIFI:CONFIG:LOAD",
    ":WIFI:NETWORK:ADD",
    ":WIFI:NETWORK:REMOVE",
    ":WIFI:NETWORK:LIST?",
    # Local
    "help",
    "exit",
    "quit",
    "q",
    "raw",
    "list",
    "commands",
    "sync",
    "nosync",
    "pretty",
    "raw_response",
    "status",
]


def _detect_device_type(idn_str: str) -> str:
    """Return a short device-type token from an *IDN? response string."""
    idn_up = idn_str.upper()
    if "RELAYBANK" in idn_up:
        return "relaybank16"
    if "TMP117" in idn_up:
        return "tmp117"
    return "generic"


def _commands_for_device(device_type: str) -> list:
    """Return the appropriate tab-completion command list for a device type."""
    if device_type == "relaybank16":
        return SCPI_COMMANDS_RELAYBANK16
    return SCPI_COMMANDS  # TMP117 / generic


def interactive_mode(
    ser,
    *,
    sync_mode: bool = True,
    connection_info: dict | None = None,
    pretty_print: bool = True,
    commands: list | None = None,
    device_type: str | None = None,
) -> None:
    """Interactive SCPI prompt with history, tab-completion, and pretty output."""
    cur_sync = sync_mode
    cur_pretty = pretty_print
    _cmds = commands if commands is not None else SCPI_COMMANDS

    if connection_info is None:
        if hasattr(ser, "port") and hasattr(ser, "baudrate"):
            connection_info = {"type": "serial", "port": ser.port, "baudrate": ser.baudrate}
        elif hasattr(ser, "host") and hasattr(ser, "tcp_port"):
            connection_info = {"type": "tcp", "host": ser.host, "tcp_port": ser.tcp_port}

    # readline setup
    if READLINE_AVAILABLE:

        def completer(text, state):
            opts = [c for c in _cmds if c.upper().startswith(text.upper())] if text else list(_cmds)
            return opts[state] if state < len(opts) else None

        readline.set_completer(completer)
        if USING_LIBEDIT:
            readline.parse_and_bind("bind ^I rl_complete")
        else:
            readline.parse_and_bind("tab: complete")
        readline.set_completer_delims(" \t\n")
        try:
            readline.read_history_file(HISTORY_FILE)
            readline.set_history_length(500)
        except (FileNotFoundError, Exception):
            pass

    print("\n" + "=" * 60)
    print(f"{BOLD}{CYAN}Interactive SCPI Mode (© 2026 Prof. Flavio ABREU ARAUJO){RESET}")
    print("=" * 60)
    if device_type:
        _device_label = {"relaybank16": "RelayBank16", "tmp117": "TMP117"}.get(device_type, "")
        if _device_label:
            print(f"{BOLD}Device:{RESET}     {GREEN}{_device_label}{RESET}")
    if connection_info:
        if connection_info.get("type") == "tcp":
            print(
                f"{BOLD}Connection:{RESET} {GREEN}TCP{RESET} {connection_info.get('host')}:{connection_info.get('tcp_port')}"
            )
        else:
            print(
                f"{BOLD}Connection:{RESET} {GREEN}Serial{RESET} {connection_info.get('port')} @ {connection_info.get('baudrate')} baud"
            )
    print(f"{DIM}Type SCPI commands and press Enter.{RESET}")
    print(
        f"{DIM}Commands: 'help', 'list', 'sync', 'nosync', 'pretty', 'raw_response', 'status', 'exit'{RESET}"
    )
    if READLINE_AVAILABLE:
        print(f"{DIM}Use ↑/↓ arrows for history, Tab for completion.{RESET}")
    sync_str = f"{GREEN}ON{RESET}" if cur_sync else f"{YELLOW}OFF{RESET}"
    pretty_str = f"{GREEN}ON{RESET}" if cur_pretty else f"{YELLOW}OFF{RESET}"
    print(f"{BOLD}OPC Sync Mode:{RESET} {sync_str}  {BOLD}Pretty Print:{RESET} {pretty_str}")
    print("=" * 60 + "\n")

    try:
        while True:
            try:
                # drain buffer before prompt (may fail if device disconnected)
                try:
                    if hasattr(ser, "in_waiting") and hasattr(ser, "reset_input_buffer"):
                        time.sleep(0.05)
                        if ser.in_waiting > 0:
                            ser.read(ser.in_waiting)
                        ser.reset_input_buffer()
                except (OSError, termios.error):
                    # Device disconnected (e.g., after reset)
                    print(f"\n{YELLOW}Device disconnected.{RESET}")
                    print(f"{YELLOW}Run 'scpi-client' again to reconnect.{RESET}\n")
                    break

                command = input("SCPI> ").strip()
                if command:
                    print(f"\033[1A\033[2K{BOLD}{MAGENTA}SCPI>{RESET} {CYAN}{command}{RESET}")
                if not command:
                    continue

                cl = command.lower()
                if cl in ("exit", "quit", "q"):
                    print(f"{YELLOW}Exiting...{RESET}")
                    break
                if cl == "help":
                    print_help(device_type)
                    continue
                if cl == "raw":
                    print(f"\n{CYAN}Listening for raw serial data (5 seconds)...{RESET}")
                    print("-" * 60)
                    start = time.time()
                    while time.time() - start < 5:
                        if ser.in_waiting > 0:
                            try:
                                data = ser.read(ser.in_waiting)
                                print(data.decode("utf-8", errors="replace"), end="", flush=True)
                            except Exception:
                                pass
                        time.sleep(0.01)
                    print("\n" + "-" * 60)
                    ser.reset_input_buffer()
                    continue
                if cl in ("list", "commands"):
                    print(f"\n{BOLD}{CYAN}Available SCPI commands for tab completion:{RESET}")
                    print("-" * 60)
                    groups: dict[str, list[str]] = {}
                    for c in sorted(_cmds):
                        if c.startswith("*"):
                            key = "IEEE-488.2"
                        elif c.startswith(":"):
                            key = c.split(":")[1] if ":" in c[1:] else "Other"
                        else:
                            key = "Local"
                        groups.setdefault(key, []).append(c)
                    for grp in sorted(groups):
                        print(f"\n  {BOLD}{YELLOW}{grp}:{RESET}")
                        cmds = groups[grp]
                        cw = max(len(x) for x in cmds) + 2
                        cols = 80 // cw or 1
                        for i in range(0, len(cmds), cols):
                            row = cmds[i : i + cols]
                            print(
                                f"    {GREEN}"
                                + f"{RESET}  {GREEN}".join(x.ljust(cw - 2) for x in row)
                                + RESET
                            )
                    print()
                    continue
                if cl == "sync":
                    cur_sync = True
                    print(f"{GREEN}OPC synchronization: ON{RESET}")
                    continue
                if cl == "nosync":
                    cur_sync = False
                    print(f"{YELLOW}OPC synchronization: OFF{RESET}")
                    continue
                if cl == "pretty":
                    cur_pretty = True
                    print(f"{GREEN}Pretty print mode: ON{RESET}")
                    continue
                if cl == "raw_response":
                    cur_pretty = False
                    print(f"{YELLOW}Pretty print mode: OFF{RESET}")
                    continue
                if cl == "status":
                    ss = f"{GREEN}ON{RESET}" if cur_sync else f"{YELLOW}OFF{RESET}"
                    ps = f"{GREEN}ON{RESET}" if cur_pretty else f"{YELLOW}OFF{RESET}"
                    print(f"\n{BOLD}Current Settings:{RESET}")
                    print(f"  {BOLD}OPC Sync Mode:{RESET} {ss}")
                    print(f"  {BOLD}Pretty Print:{RESET}  {ps}")
                    if connection_info:
                        if connection_info.get("type") == "tcp":
                            print(f"  {BOLD}Connection:{RESET}    {GREEN}TCP{RESET}")
                            print(f"  {BOLD}Host:{RESET}          {connection_info.get('host')}")
                            print(
                                f"  {BOLD}Port:{RESET}          {connection_info.get('tcp_port')}"
                            )
                        else:
                            print(f"  {BOLD}Connection:{RESET}    {GREEN}Serial{RESET}")
                            print(f"  {BOLD}Serial Port:{RESET}   {connection_info.get('port')}")
                            print(
                                f"  {BOLD}Baudrate:{RESET}      {connection_info.get('baudrate')}"
                            )
                    continue

                # send the command
                # print(f"{BLUE}→{RESET} {CYAN}{command}{RESET}")
                is_reset_command = command.strip().upper() in (":SYST:RES", ":SYST:RES?", "*RST")
                try:
                    response = send_scpi_command(ser, command, sync_mode=cur_sync)
                    if response:
                        # Check for reset command special response
                        if response == "RESET_COMMAND_SENT":
                            formatted = format_response(command, response, pretty=cur_pretty)
                            print(f"{YELLOW}←{RESET} {formatted}")
                            print(f"\n{GREEN}{BOLD}Device reset initiated.{RESET}")
                            print(
                                f"{YELLOW}The device is restarting. Connection will be closed.{RESET}"
                            )
                            print(f"{YELLOW}Run 'scpi-client' again to reconnect.{RESET}\n")
                            break
                        # Check for critical connection errors
                        elif (
                            "Connection lost" in response
                            or "broken pipe" in response.lower()
                            or "device disconnected" in response.lower()
                        ):
                            print(f"{RED}←{RESET} {RED}{response}{RESET}")
                            if is_reset_command:
                                print(f"\n{GREEN}{BOLD}Device reset complete.{RESET}")
                                print(
                                    f"{YELLOW}The device is restarting. Connection closed.{RESET}"
                                )
                                print(f"{YELLOW}Run 'scpi-client' again to reconnect.{RESET}\n")
                                break
                            else:
                                print(f"\n{RED}{BOLD}Connection lost!{RESET}")
                                print(
                                    f"{YELLOW}The device connection was interrupted. Please reconnect or exit.{RESET}\n"
                                )
                        else:
                            # Always format responses (including errors) for better presentation
                            formatted = format_response(command, response, pretty=cur_pretty)
                            # Determine color based on content
                            if (
                                "✗" in formatted
                                or formatted.startswith("-")
                                or "error" in formatted.lower()
                                and "ERROR:" in response
                            ):
                                # Error message
                                if "\n" in formatted:
                                    print(f"{RED}←{RESET}{formatted}")
                                else:
                                    print(f"{RED}←{RESET} {formatted}")
                            elif "\n" in formatted:
                                print(f"{GREEN}←{RESET}{formatted}")
                            else:
                                print(f"{GREEN}←{RESET} {YELLOW}{formatted}{RESET}")
                    else:
                        print(
                            f"{RED}←{RESET} {DIM}(no response – try 'raw' to see device output){RESET}"
                        )
                except (
                    BrokenPipeError,
                    ConnectionResetError,
                    ConnectionError,
                    OSError,
                    termios.error,
                ) as e:
                    if is_reset_command:
                        print(f"{GREEN}{BOLD}✓{RESET} {GREEN}Device reset complete.{RESET}")
                        print(f"{YELLOW}The device is restarting. Connection closed.{RESET}")
                        print(f"{YELLOW}Run 'scpi-client' again to reconnect.{RESET}\n")
                        break
                    else:
                        print(f"{RED}←{RESET} {RED}{BOLD}ERROR: Connection lost!{RESET}")
                        print(f"{RED}  {e}{RESET}")
                        print(
                            f"{YELLOW}The device connection was interrupted. Please reconnect or exit.{RESET}\n"
                        )
                print()

            except KeyboardInterrupt:
                print(f"\n\n{YELLOW}Interrupted by user. Exiting...{RESET}")
                break
            except EOFError:
                print(f"\n{YELLOW}EOF received. Exiting...{RESET}")
                break
    finally:
        if READLINE_AVAILABLE:
            with contextlib.suppress(Exception):
                readline.write_history_file(HISTORY_FILE)


# ===================================================================
# main() entry-point
# ===================================================================
def main() -> int:
    """CLI entry-point for the ``scpi-client`` command."""
    from . import __version__

    parser = argparse.ArgumentParser(
        description="Send SCPI commands to TMP117 PID Controller",
        epilog=(
            "Examples:\n"
            '  %(prog)s "*IDN?"\n'
            '  %(prog)s ":SENS:TEMP?"\n'
            '  %(prog)s --no-sync ":PID:SETP 25"\n'
            '  %(prog)s -w 192.168.1.100 "*IDN?"   (WiFi)\n'
            "  %(prog)s  (interactive mode)"
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    parser.add_argument("-V", "--version", action="version", version=f"%(prog)s {__version__}")
    parser.add_argument("command", nargs="?", help="SCPI command (omit for interactive mode)")

    conn = parser.add_mutually_exclusive_group()
    conn.add_argument("-p", "--port", help="Serial port path (auto-detects if not specified)")
    conn.add_argument(
        "-w",
        "--wifi",
        nargs="?",
        const="AUTO",
        metavar="HOST[:PORT]",
        help=f"WiFi/TCP connection (auto-detects if HOST not specified, default port: {DEFAULT_SCPI_PORT})",
    )

    parser.add_argument(
        "-b", "--baud", type=int, default=115200, help="Baud rate (default: 115200)"
    )
    parser.add_argument(
        "-t", "--timeout", type=float, default=2.0, help="Timeout in seconds (default: 2.0)"
    )
    parser.add_argument("--no-sync", action="store_true", help="Disable OPC synchronization")
    parser.add_argument("--no-pretty", action="store_true", help="Disable pretty printing")

    args = parser.parse_args()
    use_wifi = args.wifi is not None

    # ---- open connection ------------------------------------------------
    if use_wifi:
        # Auto-detect device if --wifi specified without HOST
        if args.wifi == "AUTO":
            from .device_scanner import get_all_local_networks, scan_network

            print(f"{CYAN}Auto-detecting device on network...{RESET}")
            networks = get_all_local_networks()
            if not networks:
                print(f"{RED}✗ Could not auto-detect local networks{RESET}")
                print(f"{YELLOW}  Specify host with --wifi HOST{RESET}")
                return 1

            if len(networks) > 1:
                print(f"{DIM}  Scanning {len(networks)} networks...{RESET}")

            devices = []
            for network in networks:
                network_devices = scan_network(network=network, verbose=False)
                devices.extend(network_devices)

            if not devices:
                print(f"{RED}✗ No devices found on any network{RESET}")
                print(f"{YELLOW}  Please check device is powered on and connected{RESET}")
                return 1

            if len(devices) == 1:
                host = devices[0][0]
                port = DEFAULT_SCPI_PORT
                print(f"{GREEN}✓ Found device: {BOLD}{host}{RESET}")
            else:
                print(f"{GREEN}✓ Found {len(devices)} devices:{RESET}")
                for idx, (ip, idn) in enumerate(devices, 1):
                    print(f"{DIM}  [{idx}] {ip:15s} - {idn}{RESET}")

                while True:
                    try:
                        choice = input(f"\n{CYAN}Select device [1-{len(devices)}]: {RESET}").strip()
                        idx = int(choice)
                        if 1 <= idx <= len(devices):
                            host = devices[idx - 1][0]
                            port = DEFAULT_SCPI_PORT
                            print(f"{GREEN}✓ Selected: {BOLD}{host}{RESET}")
                            break
                        else:
                            print(f"{YELLOW}  Invalid choice. Enter 1-{len(devices)}{RESET}")
                    except (ValueError, KeyboardInterrupt, EOFError):
                        print(f"\n{RED}✗ Selection cancelled{RESET}")
                        return 1
        else:
            host, port = parse_wifi_address(args.wifi)

        print(f"{CYAN}Connecting to {BOLD}{host}:{port}{RESET}{CYAN} via TCP...{RESET}")
        try:
            ser = TCPSocket(host, port, timeout=args.timeout)
            ser.connect()
            print(f"{GREEN}✓ Connected to {BOLD}{host}:{port}{RESET}")
            time.sleep(0.2)
        except OSError as e:
            print(f"\n{RED}✗ Error connecting to {host}:{port}: {e}{RESET}")
            if e.errno == 61 or "Connection refused" in str(e):
                print(f"\n{YELLOW}Connection refused – possible causes:{RESET}")
                print(f"  {DIM}1. TCP SCPI server not enabled in firmware{RESET}")
                print(f"  {DIM}2. Device not reachable at this IP{RESET}")
                print(f"  {DIM}3. Firewall blocking the connection{RESET}")
            return 1
        except Exception as e:
            print(f"\n{RED}✗ Unexpected error: {e}{RESET}")
            return 1
    else:
        if args.port:
            port_path = args.port
            print(f"{CYAN}Using specified port: {BOLD}{port_path}{RESET}")
        else:
            print(f"{CYAN}Auto-detecting CircuitPython device...{RESET}")
            port_path = find_circuitpython_port()
            if not port_path:
                print(f"\n{RED}✗ Could not auto-detect CircuitPython device.{RESET}")
                print(f"{YELLOW}Specify port with --port or use --wifi HOST.{RESET}")
                return 1
            print(f"{GREEN}✓ Found device on: {BOLD}{port_path}{RESET}")

        try:
            ser = serial.Serial(
                port=port_path,
                baudrate=args.baud,
                timeout=args.timeout,
                write_timeout=args.timeout,
            )
            print(f"{DIM}Waiting for device to initialize...{RESET}")
            time.sleep(1.5)

            # drain startup messages
            startup: list[str] = []
            while ser.in_waiting > 0:
                try:
                    line = ser.readline().decode("utf-8", errors="replace").strip()
                    if line:
                        startup.append(line)
                except Exception:
                    break
                time.sleep(0.01)
            if startup:
                print(f"\n{CYAN}Device startup messages:{RESET}")
                for ln in startup[-10:]:
                    print(f"  {DIM}{ln}{RESET}")
                print()
            ser.reset_input_buffer()
            ser.reset_output_buffer()
        except serial.SerialException as e:
            print(f"\n{RED}✗ Error opening serial port: {e}{RESET}")
            return 1

    # ---- detect device type (best-effort, interactive mode only) -----------
    device_type = "generic"
    _cmds = SCPI_COMMANDS
    if not args.command:
        try:
            idn = send_scpi_command(ser, "*IDN?", args.timeout, sync_mode=False)
            if idn and not idn.startswith("ERROR"):
                device_type = _detect_device_type(idn)
                _cmds = _commands_for_device(device_type)
                _dev_label = {"relaybank16": "RelayBank16", "tmp117": "TMP117"}.get(device_type, "")
                if _dev_label:
                    print(f"{GREEN}✓ Device identified: {BOLD}{_dev_label}{RESET}")
        except Exception:
            pass

    # ---- send command(s) ------------------------------------------------
    try:
        if args.command:
            print(f"\n{CYAN}Sending:{RESET} {BOLD}{args.command}{RESET}")
            response = send_scpi_command(
                ser, args.command, args.timeout, sync_mode=not args.no_sync
            )
            if response:
                if response.startswith("ERROR:") or response.startswith("-"):
                    print(f"{RED}Error:{RESET} {RED}{response}{RESET}")
                    return 1
                formatted = format_response(args.command, response, pretty=not args.no_pretty)
                if "\n" in formatted:
                    print(f"{GREEN}Response:{RESET}{formatted}")
                else:
                    print(f"{GREEN}Response:{RESET} {YELLOW}{formatted}{RESET}")
            else:
                print(f"{RED}(no response){RESET}")
                return 1
        else:
            ci = (
                {"type": "tcp", "host": host, "tcp_port": port}
                if use_wifi
                else {"type": "serial", "port": port_path, "baudrate": args.baud}
            )
            interactive_mode(
                ser,
                sync_mode=not args.no_sync,
                connection_info=ci,
                pretty_print=not args.no_pretty,
                commands=_cmds,
                device_type=device_type,
            )
    finally:
        ser.close()

    return 0


if __name__ == "__main__":
    sys.exit(main())
