import logging
import time
import threading
from typing import Any, TYPE_CHECKING

import serial
from serial.tools import list_ports

from .base import SMUBase

if TYPE_CHECKING:
    from . import SMUScript, SMUScriptCallable

_LOG = logging.getLogger(__name__)

class SMU(SMUBase):
    def __init__(self, port: str, baudrate: int = 115200, timeout: float | None = 1.0):
        self.__port = port
        self.__baudrate = baudrate
        self.__timeout = timeout
        self._serial = None
        self._connect_attempts: int = 0
        self._last_connect_time: float = 0.0
        self.max_backoff: float = 3.0
        self.initial_backoff: float = 0.1

        self._serial_lock = threading.Lock()

        try:
            self.connect()
        except Exception:
            _LOG.exception("Initial connect failed for %s", port)
            self._serial = None

    @property
    def port(self) -> str:
        return self.__port

    def connect(self) -> None:
        with self._serial_lock:
            try:
                self._serial = serial.Serial(port=self.__port, baudrate=self.__baudrate, timeout=self.__timeout)
                self._connect_attempts = 0
                self._last_connect_time = time.monotonic()
                _LOG.debug("Connected to SMU on %s", self.__port)
            except Exception as e:
                # _LOG.exception("Failed to open serial port %s", self.__port)
                self._serial = None
                raise IOError(f"Could not open port {self.__port}: {e}") from e

    def disconnect(self) -> None:
        with self._serial_lock:
            if self._serial is not None:
                try:
                    self._serial.close()
                except Exception:
                    pass
                    # _LOG.exception("Error while closing serial port")
                finally:
                    self._serial = None
                    _LOG.debug("Disconnected from %s", self.__port)

    def reconnect(self) -> None:
        # with self._serial_lock: # removed because of deadlock with self.connect()
        backoff = self.initial_backoff
        attempts = 0
        while True:
            try:
                self.connect()
                return
            except IOError:
                attempts += 1
                if backoff > self.max_backoff:
                    raise IOError(f"Failed to reconnect after {attempts} attempts") from None
                # _LOG.warning("Reconnect attempt %d failed, backing off %.2fs", attempts, backoff)
                time.sleep(backoff)
                backoff *= 2

    def __write(self, command: str) -> None:
        # with self._serial_lock:
        if self._serial is None:
            raise IOError("Serial not connected")
        self._serial.write((command + "\n").encode())

    def __read(self, command: str) -> str:
        # with self._serial_lock:
        if self._serial is None:
            raise IOError("Serial not connected")
        self.__write(command)
        raw = self._serial.readline()
        if isinstance(raw, bytes):
            raw = raw.decode(errors='ignore')
        return str(raw).strip()

    def set_voltage(self, v: float) -> None:
        with self._serial_lock:
            self.__write(f":SOUR:VOLT {float(v)}")

    def set_current(self, i: float) -> None:
        with self._serial_lock:
            self.__write(f":SOUR:CURR {float(i)}")

    def voltage_limit(self) -> float:
        with self._serial_lock:
            return float(self.__read(":SOUR:VOLT:LIM?"))

    def current_limit(self) -> float:
        with self._serial_lock:
            return float(self.__read(":SOUR:CURR:LIM?"))

    def measure_voltage(self) -> float:
        with self._serial_lock:
            return float(self.__read(":MEAS:VOLT?"))

    def measure_current(self) -> float:
        with self._serial_lock:
            return float(self.__read(":MEAS:CURR?"))

    def run_script(self, script: SMUScriptCallable, *args, **kwargs) -> Any | list | dict:
        return script(self, *args, **kwargs)
    
    @staticmethod
    def discover_scripts(scripts_dir: str) -> list[SMUScript]:
        return []

    @staticmethod
    def discover_ports() -> dict[str, str]:
        """
        :return: A list of port-devices (e.g., ['COM3', '/dev/ttyUSB0'])
        """
        ports = list_ports.comports()
        return {port.device: port.description for port in ports}
