# target_simulator/core/serial_communicator.py

"""
Handles all serial communication with the target device.
"""
import time
import serial
import serial.tools.list_ports
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
SERIAL_PARAMS = {
    "baudrates": [9600, 19200, 38400, 57600, 115200],
    "bytesizes": [serial.SEVENBITS, serial.EIGHTBITS],
    "parities": [serial.PARITY_NONE, serial.PARITY_EVEN, serial.PARITY_ODD],
    "stopbits": [serial.STOPBITS_ONE, serial.STOPBITS_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:
        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:
        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 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:
        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:
        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]:
        ports = serial.tools.list_ports.comports()
        return [port.device for port in ports]
