"""
AT Command Client for Fibocom FM350-GL modem using pyserial.
Thread-safe with exclusive port access via Lock.
"""

import os
import re
import threading
import time
import logging
from typing import Optional, Dict, Any, List

try:
    import serial
except ImportError:
    serial = None  # Will raise error when used

logger = logging.getLogger(__name__)


class ATClient:
    """
    Thread-safe AT command interface for Fibocom FM350-GL modem.
    Uses pyserial for serial communication.
    """

    DEFAULT_PORTS = ["/dev/ttyUSB1", "/dev/ttyUSB0", "/dev/ttyUSB2"]
    DEFAULT_BAUDRATE = 115200
    DEFAULT_TIMEOUT = 5

    def __init__(self, port: str = "/dev/ttyUSB1", baudrate: int = DEFAULT_BAUDRATE):
        if serial is None:
            raise ImportError("pyserial is required. Install with: pip install pyserial")
        self.port = port
        self.baudrate = baudrate
        self.lock = threading.Lock()
        self._serial: Optional[serial.Serial] = None

    def _open(self) -> bool:
        """Open serial port if not already open."""
        if self._serial and self._serial.is_open:
            return True
        try:
            self._serial = serial.Serial(
                port=self.port,
                baudrate=self.baudrate,
                timeout=1,
                write_timeout=1,
                bytesize=serial.EIGHTBITS,
                parity=serial.PARITY_NONE,
                stopbits=serial.STOPBITS_ONE,
            )
            # Disable echo
            self._send_raw("ATE0\r", timeout=2)
            return True
        except (serial.SerialException, OSError) as e:
            logger.warning(f"Failed to open {self.port}: {e}")
            self._serial = None
            return False

    def _close(self):
        """Close serial port."""
        if self._serial and self._serial.is_open:
            try:
                self._serial.close()
            except Exception:
                pass
        self._serial = None

    def _send_raw(self, cmd: str, timeout: float = DEFAULT_TIMEOUT) -> str:
        """
        Send raw AT command and read response. Must hold lock.
        Returns the response string (may contain OK, ERROR, or data).
        """
        if not self._serial or not self._serial.is_open:
            return ""

        # Clear input buffer
        self._serial.reset_input_buffer()

        # Send command
        if not cmd.endswith("\r"):
            cmd += "\r"
        self._serial.write(cmd.encode("utf-8"))
        self._serial.flush()

        # Read response until OK, ERROR, or timeout
        response_lines = []
        start = time.time()
        while (time.time() - start) < timeout:
            if self._serial.in_waiting > 0:
                line = self._serial.readline().decode("utf-8", errors="replace").strip()
                if line:
                    response_lines.append(line)
                    # Check for terminal responses
                    if line in ("OK", "ERROR") or line.startswith("+CME ERROR") or line.startswith("+CMS ERROR"):
                        break
            else:
                time.sleep(0.05)

        return "\n".join(response_lines)

    def send_command(self, cmd: str, timeout: float = DEFAULT_TIMEOUT) -> str:
        """
        Send AT command and return response. Thread-safe.
        """
        with self.lock:
            if not self._open():
                return ""
            try:
                return self._send_raw(cmd, timeout)
            except Exception as e:
                logger.error(f"AT command error: {e}")
                self._close()
                return ""

    def test(self) -> bool:
        """Test AT communication. Returns True if modem responds with OK."""
        response = self.send_command("AT", timeout=3)
        return "OK" in response

    def find_working_port(self) -> Optional[str]:
        """
        Try to find a working AT port from the default list.
        Updates self.port if found. Returns the working port or None.
        """
        for port in self.DEFAULT_PORTS:
            if not os.path.exists(port):
                continue
            old_port = self.port
            self.port = port
            with self.lock:
                self._close()
            if self.test():
                logger.info(f"Found working AT port: {port}")
                return port
            self.port = old_port
            with self.lock:
                self._close()
        return None

    def get_manufacturer(self) -> str:
        """Get modem manufacturer (AT+CGMI)."""
        response = self.send_command("AT+CGMI")
        for line in response.split("\n"):
            line = line.strip()
            if line and line not in ("OK", "AT+CGMI") and not line.startswith("+"):
                return line
        return ""

    def get_model(self) -> str:
        """Get modem model (AT+CGMM)."""
        response = self.send_command("AT+CGMM")
        for line in response.split("\n"):
            line = line.strip()
            if line and line not in ("OK", "AT+CGMM") and not line.startswith("+"):
                return line
        return ""

    def get_revision(self) -> str:
        """Get firmware revision (AT+CGMR)."""
        response = self.send_command("AT+CGMR")
        for line in response.split("\n"):
            line = line.strip()
            if line and line not in ("OK", "AT+CGMR", "ERROR") and not line.startswith("+"):
                return line
        return ""

    def get_imei(self) -> str:
        """Get IMEI (AT+CGSN)."""
        response = self.send_command("AT+CGSN")
        match = re.search(r"\d{15}", response)
        return match.group(0) if match else ""

    def get_iccid(self) -> str:
        """Get SIM ICCID (AT+CCID)."""
        response = self.send_command("AT+CCID")
        match = re.search(r"\+CCID:\s*\"?(\d+)\"?", response)
        if match:
            return match.group(1)
        # Try alternate format
        for line in response.split("\n"):
            line = line.strip()
            if line and re.match(r"^\d{18,22}$", line):
                return line
        return ""

    def get_signal_csq(self) -> Optional[int]:
        """
        Get signal strength as CSQ value (AT+CSQ).
        CSQ 0-31 = signal, 99 = unknown.
        Returns None if unavailable.
        """
        response = self.send_command("AT+CSQ")
        match = re.search(r"\+CSQ:\s*(\d+)", response)
        if match:
            csq = int(match.group(1))
            return csq if csq <= 31 else None
        return None

    def get_registration_status(self) -> Dict[str, str]:
        """
        Get network registration status (AT+CREG?, AT+CEREG?).
        Returns dict with 'creg' and 'cereg' status strings.
        """
        result = {"creg": "unknown", "cereg": "unknown"}

        # Parse registration: ,1 = home, ,5 = roaming, ,2 = searching, ,0 = not registered
        def parse_reg(response: str) -> str:
            if ",1" in response:
                return "registered_home"
            elif ",5" in response:
                return "registered_roaming"
            elif ",2" in response:
                return "searching"
            elif ",0" in response:
                return "not_registered"
            return "unknown"

        creg_resp = self.send_command("AT+CREG?")
        cereg_resp = self.send_command("AT+CEREG?")

        if "+CREG:" in creg_resp:
            result["creg"] = parse_reg(creg_resp)
        if "+CEREG:" in cereg_resp:
            result["cereg"] = parse_reg(cereg_resp)

        return result

    def get_operator(self) -> str:
        """Get current operator name (AT+COPS?)."""
        response = self.send_command("AT+COPS?")
        # Format: +COPS: 0,0,"Vodafone",7
        match = re.search(r'"([^"]+)"', response)
        return match.group(1) if match else ""

    def get_usb_mode(self) -> Optional[int]:
        """Get USB mode (AT+GTUSBMODE?). 40 = RNDIS, 41 = extended."""
        response = self.send_command("AT+GTUSBMODE?")
        match = re.search(r"\+GTUSBMODE:\s*(\d+)", response)
        return int(match.group(1)) if match else None

    def get_pdp_address(self, cid: int = 1) -> Optional[str]:
        """Get IP address assigned to PDP context (AT+CGPADDR=cid)."""
        response = self.send_command(f"AT+CGPADDR={cid}")
        match = re.search(r"(\d+\.\d+\.\d+\.\d+)", response)
        return match.group(1) if match else None

    def get_connection_params(self, cid: int = 1) -> Dict[str, Any]:
        """
        Get connection dynamic parameters (AT+CGCONTRDP=cid).
        Returns dict with 'ip', 'dns1', 'dns2', etc.
        """
        response = self.send_command(f"AT+CGCONTRDP={cid}", timeout=5)
        result = {"ip": None, "dns1": None, "dns2": None}

        # Extract all IP addresses from response
        ips = re.findall(r"(\d+\.\d+\.\d+\.\d+)", response)
        if ips:
            # First IP is usually the assigned IP, rest are DNS
            result["ip"] = ips[0]
            if len(ips) > 1:
                result["dns1"] = ips[1]
            if len(ips) > 2:
                result["dns2"] = ips[2]

        return result

    def configure_apn(self, apn: str, cid: int = 1) -> bool:
        """Configure APN for PDP context (AT+CGDCONT)."""
        response = self.send_command(f'AT+CGDCONT={cid},"IP","{apn}"', timeout=5)
        return "OK" in response

    def activate_pdp(self, cid: int = 1) -> bool:
        """Activate PDP context (AT+CGACT=1,cid)."""
        response = self.send_command(f"AT+CGACT=1,{cid}", timeout=10)
        return "OK" in response or "CGEV" in response

    def deactivate_pdp(self, cid: int = 1) -> bool:
        """Deactivate PDP context (AT+CGACT=0,cid)."""
        response = self.send_command(f"AT+CGACT=0,{cid}", timeout=5)
        return "OK" in response

    def reset_modem(self) -> bool:
        """Reset modem (AT+CFUN=1,1)."""
        response = self.send_command("AT+CFUN=1,1", timeout=5)
        self._close()
        return "OK" in response

    def get_full_status(self) -> Dict[str, Any]:
        """
        Get complete modem status. Replaces modem-status-at.sh functionality.
        """
        reg = self.get_registration_status()
        return {
            "at_port": self.port,
            "manufacturer": self.get_manufacturer(),
            "model": self.get_model(),
            "revision": self.get_revision(),
            "imei": self.get_imei(),
            "signal_csq": self.get_signal_csq(),
            "creg_status": reg["creg"],
            "cereg_status": reg["cereg"],
            "usb_mode": self.get_usb_mode(),
            "iccid": self.get_iccid(),
            "operator": self.get_operator(),
            "modem_ip": self.get_pdp_address(1),
        }

    def close(self):
        """Close serial connection."""
        with self.lock:
            self._close()

    def __del__(self):
        self.close()
