import sys
import traceback
import warnings

import pymodbus
from PyQt5.QtWidgets import QMessageBox

if "--sim-serial" in sys.argv:
    from components.dummies.serial import serial
else:
    import serial

from pymodbus.constants import Endian
# from pymodbus.exceptions import ModbusIOException
from pymodbus.payload import BinaryPayloadBuilder, BinaryPayloadDecoder

if "--sim-modbus" not in sys.argv:
    from pymodbus.client import ModbusSerialClient as ModbusClient
else:
    from components.dummies.pymodbus import ModbusClient

from PyQt5.QtCore import QMutex, pyqtSignal

from .component import Component


class ModbusComponent(Component):
    modbus_error_signal = pyqtSignal(str)

    def __init__(self, config=None, name=None, period=1, lazy=True, paused=False, threaded=True, registers=None):
        super().__init__(config=config, name=name, period=period, lazy=lazy, paused=paused, threaded=threaded)
        self.registers = registers if registers is not None else {}
        self.lock = QMutex()

    def config_changed(self):
        try:
            # Read configuration values
            self.method = self.config[self.name].get("method", "rtu").lower()
            self.port = self.config[self.name]["port"]
            self.baudrate = int(self.config[self.name]["baudrate"])
            self.stopbits = getattr(serial, self.config[self.name].get("stopbits", "stopbits_one").upper())
            self.parity = getattr(serial, self.config[self.name].get("parity", "parity_none").upper())
            self.bytesize = getattr(serial, self.config[self.name].get("bytesize", "eightbits").upper())
            self.byteorder = getattr(Endian, self.config[self.name].get("byteorder", "Big").upper())
            self.wordorder = getattr(Endian, self.config[self.name].get("wordorder", "Little").upper())
            self.timeout = int(self.config[self.name].get("timeout", 1))

            # Lock the interaction to ensure thread safety
            self.lock.lock()
            try:
                # Initialize the Modbus client
                self.client = ModbusClient(
                    method=self.method,
                    port=self.port,
                    stopbits=self.stopbits,
                    bytesize=self.bytesize,
                    parity=self.parity,
                    baudrate=self.baudrate,
                    timeout=self.timeout,
                    strict=False,
                )
                if not self.client.connect():
                    raise ConnectionError(f"Cannot connect to Modbus on port {self.port}")

                if not self.client.is_socket_open():
                    raise ConnectionError(f"Connection socket not open on port {self.port}")

            finally:
                self.lock.unlock()
        except FileNotFoundError as e:
            error_message = f"Serial port error: {self.port} not found. Check your configuration."
            self.log.error(error_message, exc_info=True)
            self.modbus_error_signal.emit(error_message)
        except Exception as e:
            error_message = f"Configuration error: {str(e)}"
            self.log.error(error_message, exc_info=True)
            self.modbus_error_signal.emit(error_message)

    def _read(self, register, count=1, **kwargs):
        """Read holding registers with error handling."""
        self.lock.lock()
        try:
            read = self.client.read_holding_registers(register, count=count, **kwargs)
            if read.isError():
                raise ValueError(f"Modbus read error at register {register}")
            return read
        except pymodbus.exceptions.ConnectionException:
            error_message = f"Modbus read failed: Connection error at port {self.port}"
            self.log.error(error_message, exc_info=True)
            self.modbus_error_signal.emit(error_message)
            return None  # Return None to signal failure
        except Exception as e:
            error_message = f"Error reading Modbus register {register}: {str(e)}"
            self.log.error(error_message, exc_info=True)
            self.modbus_error_signal.emit(error_message)
            return None
        finally:
            self.lock.unlock()

    def _write(self, register, value, **kwargs):
        """Write to holding registers with error handling."""
        self.lock.lock()
        try:
            wrote = self.client.write_registers(register, value, skip_encode=True, **kwargs)

            # Check if the response indicates an error
            if wrote.isError():
                raise ValueError(f"Modbus write error at register {register}")
            return wrote
        except pymodbus.exceptions.ConnectionException as ce:
            error_message = f"Modbus write failed: Connection error at port {self.port}"
            self.log.error(error_message, exc_info=True)
            self.modbus_error_signal.emit(error_message)
        except Exception as e:
            error_message = f"Error writing Modbus register {register} with value {value}: {str(e)}"
            self.log.error(error_message, exc_info=True)
            self.modbus_error_signal.emit(error_message)
        finally:
            self.lock.unlock()

    def _decode(self, read, *args, data_type="16bit_uint", gain=1, offset=0, **kwargs):
        """Decode data safely."""
        if read is None:
            error_message = "Error decoding Modbus data: No data to decode (read returned None)"
            self.log.error(error_message)
            self.modbus_error_signal.emit(error_message)
            return None
        try:
            decoder = BinaryPayloadDecoder.fromRegisters(
                read.registers, byteorder=self.byteorder, wordorder=self.wordorder
            )
            data = getattr(decoder, f"decode_{data_type}")(*args, **kwargs)
            data = (data - offset) / gain
            return int(abs(data)) if "uint" in data_type else int(data)
        except AttributeError:
            error_message = "Modbus read returned invalid data (NoneType encountered)"
            self.log.error(error_message)
            self.modbus_error_signal.emit(error_message)
        except Exception as e:
            error_message = f"Error decoding Modbus data: {str(e)}"
            self.log.error(error_message, exc_info=True)
            self.modbus_error_signal.emit(error_message)
        return None

    def _encode(self, data, *args, data_type="16bit_uint", gain=1, offset=0, **kwargs):
        """Encode data for Modbus write with error handling."""
        try:
            builder = BinaryPayloadBuilder(byteorder=self.byteorder, wordorder=self.wordorder)
            data = data * gain + offset
            if data_type.endswith("uint"):
                data = int(abs(data))
            elif data_type.endswith("int"):
                data = int(data)
            else:
                raise NotImplementedError(f"Data type {data_type!r} is not supported")
            getattr(builder, f"add_{data_type}")(data, *args, **kwargs)
            return builder.build()
        except Exception as e:
            error_message = f"Error encoding Modbus data: {str(e)}"
            self.log.error(error_message, exc_info=True)
            self.modbus_error_signal.emit(error_message)
            return None

    def read(self, register, *args, data_type="16bit_uint", gain=1, offset=0, **kwargs):
        """Read and decode Modbus register data with error handling."""
        try:
            if data_type.startswith("16bit_"):
                count = 1
            elif data_type.startswith("32bit_"):
                count = 2
            else:
                raise NotImplementedError(f"Data type {data_type!r} is not supported")
            return self._decode(
                self._read(register, count=count, **kwargs),
                *args,
                data_type=data_type,
                gain=gain,
                offset=offset,
            )
        except Exception as e:
            error_message = f"Error inside Modbus read: {str(e)}"
            self.log.error(error_message, exc_info=True)
            self.modbus_error_signal.emit(error_message)
            return None

    def write(self, register, data, *args, data_type="16bit_uint", gain=1, offset=0, **kwargs):
        """Encode and write data to Modbus registers with error handling."""
        try:
            encoded_data = self._encode(data, *args, data_type=data_type, gain=gain, offset=offset)
            if encoded_data is not None:
                self._write(register, encoded_data, **kwargs)
        except Exception as e:
            error_message = f"Error inside Modbus write: {str(e)}"
            self.log.error(error_message, exc_info=True)
            self.modbus_error_signal.emit(error_message)

    def __del__(self, event=None):
        try:
            self.lock.lock()
            if self.client.is_socket_open():
                self.client.close()
        except Exception as e:
            error_message = f"Error during Modbus cleanup: {str(e)}"
            self.log.error(error_message, exc_info=True)
        finally:
            self.lock.unlock()