# target_simulator/core/serial_communicator.py

"""
Handles all serial communication with the target device.
"""
import time

try:
    import serial
    import serial.tools.list_ports
except ImportError as e:
    # Provide a clearer error message if pyserial is not installed in the
    # interpreter used to run the application. This will fail fast with a
    # helpful instruction to the user about how to resolve the issue.
    raise RuntimeError(
        "Missing dependency: 'pyserial' is required. Install it with `python -m pip install pyserial`"
    ) from e
from typing import List, Optional, Dict, Any

from .communicator_interface import CommunicatorInterface
from .models import Scenario
from target_simulator.core import command_builder
from target_simulator.utils.logger import get_logger

# Constants for serial communication - using numeric values to avoid import issues
SERIAL_PARAMS = {
    "baudrates": [9600, 19200, 38400, 57600, 115200],
    "bytesizes": [7, 8],  # SEVENBITS=7, EIGHTBITS=8
    "parities": ['N', 'E', 'O'],  # NONE, EVEN, ODD
    "stopbits": [1, 2],  # ONE, TWO
}


class SerialCommunicator(CommunicatorInterface):
    """A class to manage and abstract serial port communication."""

    def __init__(self):
        self.serial_instance: Optional[serial.Serial] = None
        self.logger = get_logger(__name__)
        self.config: Optional[Dict[str, Any]] = None

    def connect(self, config: Dict[str, Any]) -> bool:
        """Open and configure the serial port described by `config`.

        Expected keys in `config`: 'port' (required), optional 'baudrate',
        'bytesize', 'parity' and 'stopbits'. Returns True on successful open
        and False on any failure. Existing open connections are closed first.
        """
        if self.is_open:
            self.logger.warning("Already connected. Disconnecting first.")
            self.disconnect()

        self.config = config
        port = self.config.get("port")
        if not port:
            self.logger.error("Connection failed: Port not specified in config.")
            return False

        # Conversione parametri stringa -> costanti pyserial
        bytesize_map = {"8": serial.EIGHTBITS, "7": serial.SEVENBITS}
        parity_map = {
            "N": serial.PARITY_NONE,
            "E": serial.PARITY_EVEN,
            "O": serial.PARITY_ODD,
        }
        stopbits_map = {"1": serial.STOPBITS_ONE, "2": serial.STOPBITS_TWO}

        bytesize = bytesize_map.get(
            str(self.config.get("bytesize", "8")), serial.EIGHTBITS
        )
        parity = parity_map.get(str(self.config.get("parity", "N")), serial.PARITY_NONE)
        stopbits = stopbits_map.get(
            str(self.config.get("stopbits", "1")), serial.STOPBITS_ONE
        )

        try:
            self.logger.info(f"Attempting to connect to {port} with config: {config}")
            self.serial_instance = serial.Serial(
                port=port,
                baudrate=int(self.config.get("baudrate", 115200)),
                bytesize=bytesize,
                parity=parity,
                stopbits=stopbits,
                timeout=1.0,
            )
            self.logger.info(f"Successfully connected to {port}.")
            return True
        except (serial.SerialException, ValueError) as e:
            self.logger.error(f"Failed to connect to {port}. Error: {e}")
            self.serial_instance = None
            return False

    def disconnect(self) -> None:
        """Close the serial port and clear stored configuration.

        Safe to call when not connected.
        """
        if self.serial_instance and self.serial_instance.is_open:
            port_name = self.serial_instance.port
            self.serial_instance.close()
            self.logger.info(f"Disconnected from {port_name}.")
        self.serial_instance = None
        self.config = None

    @property
    def is_open(self) -> bool:
        """Return True when the serial port is currently open."""
        return self.serial_instance is not None and self.serial_instance.is_open

    def _send_single_command(self, command: str) -> bool:
        """Internal helper to send one command string."""
        if not self.is_open:
            self.logger.error("Cannot send command: Serial port is not open.")
            return False
        try:
            full_command = command + "\r\n"
            self.serial_instance.write(full_command.encode("utf-8"))
            self.logger.debug(f"Sent: {command!r}")
            return True
        except serial.SerialException as e:
            self.logger.error(f"Failed to send command. Error: {e}")
            self.disconnect()
            return False

    def send_scenario(self, scenario: Scenario) -> bool:
        """Send a Scenario over the serial link using tgtinit commands.

        The method attempts to pause the remote, latch A/C data, send
        individual tgtinit commands for each target and then unlatch/continue.
        Returns True on success or False if any command failed.
        """
        self.logger.info(f"Atomically sending scenario '{scenario.name}' via Serial...")

        commands = [command_builder.build_pause(), command_builder.build_aclatch()]

        for target in scenario.get_all_targets():
            commands.append(command_builder.build_tgtinit(target))

        commands.extend(
            [command_builder.build_acunlatch(), command_builder.build_continue()]
        )

        for command in commands:
            if not self._send_single_command(command):
                self.logger.error(
                    f"Failed to send command '{command}'. Aborting scenario send."
                )
                # Attempt to unlatch and continue to leave the system in a good state
                self._send_single_command(command_builder.build_acunlatch())
                self._send_single_command(command_builder.build_continue())
                return False
            time.sleep(0.05)  # Small delay between commands

        self.logger.info("Finished sending scenario.")
        return True

    @staticmethod
    def test_connection(config: Dict[str, Any]) -> bool:
        """Statically test whether the provided serial port config can be opened.

        Attempts to open and immediately close the port. Returns True when the
        port can be opened with the requested settings.
        """
        port = config.get("port")
        if not port:
            return False
        try:
            test_serial = serial.Serial(
                port=port,
                baudrate=int(config.get("baudrate", 115200)),
                bytesize=config.get("bytesize", serial.EIGHTBITS),
                parity=config.get("parity", serial.PARITY_NONE),
                stopbits=config.get("stopbits", serial.STOPBITS_ONE),
                timeout=1.0,
            )
            test_serial.close()
            return True
        except (serial.SerialException, ValueError):
            return False

    @staticmethod
    def list_available_ports() -> List[str]:
        """Return a list of available serial port device names on the host."""
        ports = serial.tools.list_ports.comports()
        return [port.device for port in ports]

    def send_commands(self, commands: List[str]) -> bool:
        """Sends a batch of commands for live updates."""
        if not self.is_open:
            self.logger.error("Cannot send commands: Serial port is not open.")
            return False

        for command in commands:
            if not self._send_single_command(command):
                self.logger.error(
                    f"Failed to send command '{command}'. Aborting batch send."
                )
                return False
            time.sleep(0.05)  # Small delay between commands

        self.logger.debug(f"Successfully sent a batch of {len(commands)} commands.")
        return True
