import logging
from binascii import hexlify
from collections import namedtuple
from struct import unpack

from bluetooth_data_tools import short_address
from bluetooth_sensor_state_data import BluetoothData
from home_assistant_bluetooth import BluetoothServiceInfo
from sensor_state_data import SensorLibrary

from .custom_state_data import DeviceClass, Units

_LOGGER = logging.getLogger(__name__)


RAPTPillMetrics = namedtuple(
    "RAPTPillMetrics",
    "version, mac, gravity_velocity_valid, gravity_velocity,"
    "temperature, gravity, x, y, z, battery",
)

RAPTPillMetricsV1 = namedtuple(
    "RAPTPillMetricsV1", "version, mac, temperature, gravity, x, y, z, battery"
)

RAPTPillMetricsV2 = namedtuple(
    "RAPTPillMetricsV2",
    "version, gravity_velocity_valid, gravity_velocity,"
    "temperature, gravity, x, y, z, battery",
)


class RAPTPillBluetoothDeviceData(BluetoothData):
    """Data update for RAPT Bluetooth devices"""

    def _process_metrics(self, data: bytes) -> None:
        """
        Process advertisement with metrics.

        This is what the advertisement data payload looks like in C,
        endianness is big endian:

        Version 1 (with MAC, no gravity velocity):

        typedef struct __attribute__((packed)) {
            char prefix[4];        // RAPT
            uint8_t version;       // always 0x01
            uint8_t mac[6];
            uint16_t temperature;  // x / 128 - 273.15
            float gravity;         // / 1000
            int16_t x;             // x / 16
            int16_t y;             // x / 16
            int16_t z;             // x / 16
            int16_t battery;       // x / 256
        } RAPTPillMetricsV1;

        Version 2 (no MAC, with gravity velocity):

        typedef struct __attribute__((packed)) {
            char prefix[4];        // RAPT
            uint8_t version;       // always 0x02
            uint8_t reserved;      // always 0x00
            bool gravity_velocity_valid;
            float gravity_velocity;  // points per day, negative when falling
            uint16_t temperature;  // x / 128 - 273.15
            float gravity;         // / 1000
            int16_t x;             // x / 16
            int16_t y;             // x / 16
            int16_t z;             // x / 16
            int16_t battery;       // x / 256
        } RAPTPillMetricsV2;
        """
        if len(data) != 23:
            raise ValueError("Metrics data must have length 23")

        metrics_version = data[2]

        metrics_raw: RAPTPillMetrics

        if metrics_version == 1:
            # get "raw" data, drop second part of the prefix ("PT"),
            # start with the version
            metrics_raw_v1 = RAPTPillMetricsV1._make(unpack(">B6sHfhhhh", data[2:]))
            metrics_raw = RAPTPillMetrics(
                **metrics_raw_v1._asdict(),
                gravity_velocity=None,
                gravity_velocity_valid=None,
            )
        else:
            if metrics_version != 2:
                _LOGGER.warning(
                    "Unexpected RAPT payload version %d, "
                    "measurements may be incorrect!",
                    metrics_version,
                )
            metrics_raw_v2 = RAPTPillMetricsV2._make(unpack(">Bx?fHfhhhh", data[2:]))
            metrics_raw = RAPTPillMetrics(**metrics_raw_v2._asdict(), mac=None)

        # convert to actual metrics
        metrics = RAPTPillMetrics(
            version=metrics_raw.version,
            mac=hexlify(metrics_raw.mac).decode("ascii")
            if metrics_version == 1
            else None,
            gravity_velocity_valid=metrics_raw.gravity_velocity_valid
            if metrics_version > 1
            else None,
            gravity_velocity=metrics_raw.gravity_velocity
            if metrics_version > 1
            else None,
            temperature=round(metrics_raw.temperature / 128 - 273.15, 2),
            gravity=round(metrics_raw.gravity / 1000, 4),
            x=metrics_raw.x / 16,
            y=metrics_raw.y / 16,
            z=metrics_raw.z / 16,
            battery=round(metrics_raw.battery / 256),
        )

        _LOGGER.debug("Parsed RAPT Pill data: %s", metrics)

        self.update_predefined_sensor(
            SensorLibrary.BATTERY__PERCENTAGE, metrics.battery
        )
        self.update_predefined_sensor(
            SensorLibrary.TEMPERATURE__CELSIUS, metrics.temperature
        )
        self.update_sensor(
            key=DeviceClass.SPECIFIC_GRAVITY,
            device_class=DeviceClass.SPECIFIC_GRAVITY,
            native_unit_of_measurement=Units.SPECIFIC_GRAVITY,
            native_value=metrics.gravity,
        )

        if metrics_version >= 2:
            self.update_sensor(
                key=DeviceClass.SPECIFIC_GRAVITY_VELOCITY,
                device_class=DeviceClass.SPECIFIC_GRAVITY_VELOCITY,
                native_unit_of_measurement=Units.SPECIFIC_GRAVITY_POINTS_PER_DAY,
                native_value=metrics.gravity_velocity
                if metrics.gravity_velocity_valid
                else None,
            )

    def _process_version(self, data: bytes) -> None:
        """Process advertisement with SW version."""
        # version payload is e.g. "KEG20220612_050156_81c6d1"
        # first two bytes are manufacturer ID, we just need to strip initial "G"
        if data[0] != 71:  # "G"
            _LOGGER.warning(
                "'%r' doesn't seem to be version advertisement payload, ignoring", data
            )
            return
        version = data[1:].decode("ascii")
        self.set_device_sw_version(version)

    def _start_update(self, service_info: BluetoothServiceInfo) -> None:
        """Update from BLE advertisement data."""
        manufacturer_data = service_info.manufacturer_data

        if not manufacturer_data:
            return

        # 16722 = 0x52 0x41 prefix in little endian - start of "RAPT"
        # 17739 = 0x4b 0x45 prefix in little endian - start of "KEG"
        if 16722 not in manufacturer_data and 17739 not in manufacturer_data:
            _LOGGER.debug("RAPT Pill manufacturer ID not found in data")
            return

        _LOGGER.debug("Got RAPT Pill data payload: %s", manufacturer_data)

        self.set_device_manufacturer("RAPT")
        self.set_device_type("RAPT Pill hydrometer")
        mac_suffix = short_address(service_info.address)
        self.set_device_name(f"RAPT Pill {mac_suffix}")
        self.set_title(f"RAPT Pill {mac_suffix}")

        if 17739 in manufacturer_data:
            if data := manufacturer_data[17739]:
                self._process_version(data)

        if 16722 not in manufacturer_data:
            return

        data = manufacturer_data[16722]

        if manufacturer_data[16722] == b"PTdPillG1":
            # likely a HW revision advertisement - can be ignored
            _LOGGER.debug("Ignoring 'RAPTdPillG1' payload")
            return

        if len(data) != 23:
            _LOGGER.error("Unknown RAPT payload: %s", data)
            return

        self._process_metrics(data)
