# -*- coding: utf-8 -*-
# hub/ticket/services/serial_service.py

"""Serial transport-сервис Ticket с подтверждением состояний кнопок."""

from __future__ import annotations

import threading
import time
from typing import Any

from PySide6.QtCore import QObject

from error_logger import log_exception

from domain import TicketConnectionStatus
from domain.ticket_constants import HARDWARE_SIGNAL_INITIALIZE
from .base_service import BaseService

try:
    import serial
    from serial.tools import list_ports
except ImportError:
    serial = None
    list_ports = None
SERIAL_PORT = "COM21"
SERIAL_BAUDRATE = 9600
SERIAL_TIMEOUT = 0.1
def probe_serial_port(port: str) -> bool:
    """Проверить доступность COM-порта без запуска transport-потока."""
    if serial is None or list_ports is None:
        return False
    try:
        available_ports = {item.device for item in list_ports.comports()}
    except Exception as exc:
        log_exception(__name__, "probe_serial_port", exc)
        return False
    if port not in available_ports:
        return False
    serial_port = None
    try:
        serial_port = serial.Serial(port=port, baudrate=SERIAL_BAUDRATE, timeout=SERIAL_TIMEOUT)
        return True
    except Exception as exc:
        log_exception(__name__, "probe_serial_port.serial_open", exc)
        return False
    finally:
        if serial_port is not None and serial_port.is_open:
            serial_port.close()
class SerialService(BaseService):
    """Transport-сервис чтения и записи пакетов COM-порта."""

    def __init__(
        self,
        port: str = SERIAL_PORT,
        baudrate: int = SERIAL_BAUDRATE,
        timeout: float = SERIAL_TIMEOUT,
        resend_interval_sec: float = 1.0,
        reconnect_delay_sec: float = 1.0,
        max_connect_attempts: int = 3,
        parent: QObject | None = None,
    ):
        super().__init__(parent)
        self._port_name = port
        self._baudrate = baudrate
        self._timeout = timeout
        self._resend_interval_sec = resend_interval_sec
        self._reconnect_delay_sec = reconnect_delay_sec
        self._max_connect_attempts = max_connect_attempts
        self._lock = threading.Lock()
        self._stop_event = threading.Event()
        self._thread: threading.Thread | None = None
        self._serial_port: Any | None = None
        self._buffer = bytearray()
        self._initialized_buttons: set[int] = set()
        self._pending_states: dict[int, int] = {}
        self._retry_deadlines: dict[int, float] = {}

    def start(self) -> None:
        if self.is_running():
            return
        if serial is None:
            message = "pyserial недоступен, serial transport не может быть запущен."
            self.error_occurred.emit(message)
            self._set_connection_status(TicketConnectionStatus.ERROR, message)
            self._set_button_initialization(False, 0)
            return
        self._stop_event.clear()
        self._thread = threading.Thread(target=self._run_loop, name="TicketSerialService", daemon=True)
        self._thread.start()

    def stop(self) -> None:
        self._stop_event.set()
        self._close_port("Порт закрыт")
        if self._thread is not None:
            self._thread.join(timeout=1.0)
        self._thread = None

    def is_running(self) -> bool:
        return self._thread is not None and self._thread.is_alive()

    def set_button_state(self, button_id: int, state_code: int) -> None:
        super().set_button_state(button_id, state_code)
        with self._lock:
            button_id = int(button_id)
            state_code = int(state_code)
            self._pending_states[button_id] = state_code
            serial_port = self._serial_port
            port_open = serial_port is not None and serial_port.is_open
            is_initialized = button_id in self._initialized_buttons
        if port_open:
            self._send_state(button_id, state_code, schedule_retry=is_initialized)

    def remove_button_state(self, button_id: int) -> None:
        super().remove_button_state(button_id)
        with self._lock:
            button_id = int(button_id)
            self._pending_states.pop(button_id, None)
            self._retry_deadlines.pop(button_id, None)

    def reset_button_states(self) -> None:
        super().reset_button_states()
        with self._lock:
            self._pending_states.clear()
            self._retry_deadlines.clear()

    def _run_loop(self) -> None:
        attempts = 0
        while not self._stop_event.is_set():
            if not self._is_port_open():
                if self._connect_port():
                    attempts = 0
                    self._flush_known_states()
                else:
                    attempts += 1
                    if attempts >= self._max_connect_attempts:
                        try:
                            self.error_occurred.emit(
                                f"Не удалось подключиться к {self._port_name}."
                            )
                            self.port_disconnected.emit()
                        except RuntimeError:
                            pass
                        return
                    self._stop_event.wait(self._reconnect_delay_sec)
                    continue
            try:
                self._read_available_packets()
                self._retry_pending_states_if_needed()
            except Exception as exc:
                log_exception(__name__, "SerialService._run_loop", exc)
                try:
                    self.port_disconnected.emit()
                except RuntimeError:
                    return
                self._close_port("Порт закрыт")
                self._stop_event.wait(self._reconnect_delay_sec)
                continue
            self._stop_event.wait(0.05)

    def _connect_port(self) -> bool:
        try:
            serial_port = serial.Serial(
                port=self._port_name,
                baudrate=self._baudrate,
                timeout=self._timeout,
                write_timeout=0.5,
                inter_byte_timeout=0.005,
            )
            serial_port.reset_input_buffer()
            serial_port.reset_output_buffer()
        except Exception as exc:
            log_exception(__name__, "SerialService._connect_port", exc)
            self._set_connection_status(
                TicketConnectionStatus.ERROR,
                f"Ошибка подключения к {self._port_name}: {exc}",
            )
            self._set_button_initialization(False, 0)
            return False
        with self._lock:
            self._serial_port = serial_port
            self._buffer.clear()
        self._set_connection_status(
            TicketConnectionStatus.CONNECTED,
            f"{self._port_name} ({self._baudrate} бод)",
        )
        self._set_button_initialization(False, 0)
        return True

    def _read_available_packets(self) -> None:
        with self._lock:
            serial_port = self._serial_port
        if serial_port is None or not serial_port.is_open:
            return
        data_available = getattr(serial_port, "in_waiting", 0)
        if data_available <= 0:
            return
        payload = serial_port.read(data_available)
        if not payload:
            return
        with self._lock:
            self._buffer.extend(payload)
        self._process_buffer()

    def _process_buffer(self) -> None:
        while True:
            with self._lock:
                if len(self._buffer) < 4:
                    return
                packet_index = self._find_packet_start()
                if packet_index is None:
                    if len(self._buffer) > 100:
                        self._buffer.clear()
                    return
                button_id = self._buffer[packet_index]
                hardware_state = self._buffer[packet_index + 1]
                del self._buffer[: packet_index + 4]
            self._handle_packet(button_id, hardware_state)

    def _find_packet_start(self) -> int | None:
        buffer_length = len(self._buffer)
        for index in range(buffer_length - 3):
            if self._buffer[index + 2] == 0x0D and self._buffer[index + 3] == 0x0A:
                return index
        return None

    def _handle_packet(self, button_id: int, hardware_state: int) -> None:
        if not 1 <= button_id <= 8:
            return
        if hardware_state == HARDWARE_SIGNAL_INITIALIZE:
            self._handle_initialization_request(button_id)
            return
        if hardware_state == 0xAA:
            self._handle_confirmation(button_id)
            return
        if hardware_state not in (0, 1, 2, 3):
            self.error_occurred.emit(
                f"Неизвестное аппаратное состояние: {hardware_state:02X}"
            )
            return
        self.action_triggered.emit(
            {
                "event": "advance",
                "button_id": button_id,
                "hardware_state": hardware_state,
                "current_state_code": self._get_button_state(button_id),
            }
        )

    def _handle_initialization_request(self, button_id: int) -> None:
        with self._lock:
            self._initialized_buttons.add(button_id)
            button_count = len(self._initialized_buttons)
        self._set_button_initialization(True, button_count)
        self._send_state(button_id, self._get_button_state(button_id))

    def _handle_confirmation(self, button_id: int) -> None:
        with self._lock:
            self._pending_states.pop(button_id, None)
            self._retry_deadlines.pop(button_id, None)

    def _retry_pending_states_if_needed(self) -> None:
        now = time.monotonic()
        with self._lock:
            pending_items = list(self._retry_deadlines.items())
        for button_id, retry_deadline in pending_items:
            if retry_deadline <= now:
                self._send_state(button_id, self._get_button_state(button_id))

    def _flush_known_states(self) -> None:
        """Отправить все известные состояния кнопок однократно (без retry)."""
        for button_id, state_code in list(self._button_states.items()):
            self._send_state(button_id, state_code, schedule_retry=False)

    def _send_state(self, button_id: int, state_code: int, *, schedule_retry: bool = True) -> bool:
        packet = bytes([int(button_id), int(state_code) & 0xFF, 0x0D, 0x0A])
        try:
            with self._lock:
                serial_port = self._serial_port
                if serial_port is None or not serial_port.is_open:
                    return False
                serial_port.write(packet)
                serial_port.flush()
                if schedule_retry:
                    self._pending_states[int(button_id)] = int(state_code)
                    self._retry_deadlines[int(button_id)] = time.monotonic() + self._resend_interval_sec
        except Exception as exc:
            log_exception(__name__, "SerialService._send_state", exc)
            try:
                self.port_disconnected.emit()
            except RuntimeError:
                return False
            self._close_port("Порт закрыт")
            return False
        return True

    def _close_port(self, message: str) -> None:
        with self._lock:
            serial_port = self._serial_port
            self._serial_port = None
            self._buffer.clear()
            self._initialized_buttons.clear()
            self._pending_states.clear()
            self._retry_deadlines.clear()
        if serial_port is not None and serial_port.is_open:
            try:
                serial_port.close()
            except Exception as exc:
                log_exception(__name__, "SerialService._close_port", exc)
        self._set_button_initialization(False, 0)
        self._set_connection_status(TicketConnectionStatus.DISCONNECTED, message)

    def _is_port_open(self) -> bool:
        with self._lock:
            serial_port = self._serial_port
            return serial_port is not None and serial_port.is_open
