import logging

from ruuvitag_sensor.ruuvi_types import DataFormatAndRawSensorData, RawSensorData

log = logging.getLogger(__name__)


class ShortDataError(Exception):
    pass


def _dechunk(raw: str) -> tuple[str, str]:
    """
    Given a BLE advertisement in hex format, interpret the first
    byte as a length byte, return the data indicated by the length
    byte, and the remainder of the data in a tuple.

    The length byte itself is not included in the length.

    If the length indicated is longer than the data, raise a ValueError
    """
    if len(raw) < 2:
        raise ShortDataError("Data too short")

    dlen = int(raw[:2], 16)
    if (dlen + 1) * 2 > len(raw):
        raise ShortDataError(f"Cannot read {dlen} bytes, data too short: {raw}")

    return (raw[2 : (dlen * 2) + 2], raw[(dlen * 2) + 2 :])


class DataFormats:
    """
    RuuviTag and Ruuvi Air broadcasted raw data handling for each data format
    """

    @staticmethod
    def convert_data(raw: str) -> DataFormatAndRawSensorData:  # noqa: PLR0911, PLR0912, C901
        """
        Validate that data is from RuuviTag or Ruuvi Air and get correct data part.

        There is a special case where this function will return
        None for the data format, and '' for the data. This indicates
        that we just heard an advertisement from a Ruuvi tag that
        doesn't contain any data, but was sent for discovery purposes
        (firmware 3.x does this).

        Returns:
            tuple (int, string): Data Format type and Sensor data
        """
        log.debug("Parsing advertisement data: %s", raw)

        try:
            # The data starts with a length byte, covering the data
            # length, minus the length byte itself. There might be additional
            # data at the end: the RSSI value
            data, rssi = _dechunk(raw)

            # The remaining data is a list of length:type:data chunks.
            # We look for a chunk with vendor specific data (type 0xff),
            # used by formats 3 and 5, or a chunk with service data (type 0x16)
            # used by formats 2 and 4.
            #
            # Firmware 3.x also sends advertisements that contain chunks
            # of type 0x09, followed by 'Ruuvi', in ASCII encoding.
            candidate = None
            while data != "":
                cdata, data = _dechunk(data)

                ctype = cdata[:2]
                log.debug("Found chunk of type %s: %s", ctype, cdata)

                # See if we found a potential candidate. Break the loop
                if ctype in ("FF", "16", "09"):
                    candidate = cdata
                    break
        except ShortDataError as ex:
            # Data might be from RuuviTag, but received data was invalid
            # e.g. it's possible that Bluetooth stack received only partial data
            # Set the format to None, and data to '', this allows the
            # caller to determine that we did indeed see a RuuviTag.
            log.debug("Error parsing advertisement data: %s", ex)
            return (None, "")
        except Exception:
            log.exception("Invalid advertisement data: %s", raw)
            return (None, None)

        if candidate is None:
            log.debug("No candidate found")
            return (None, None)

        log.debug("Found candidate %s", candidate)

        # Ruuvi advertisements start with FF9904 (for format 3, 5, 6 and E1),
        # or 16AAFE (for format 2 and 4).
        match candidate:
            # Data formats are ordered by priority, so the most common formats are at the top.
            case _ if candidate.startswith("FF990405"):
                return (5, (candidate[6:] + rssi) if rssi else candidate[6:])
            case _ if candidate.startswith("FF990406"):
                return (6, candidate[6:])
            case _ if candidate.lower().startswith("ff9904e1"):
                # Ruuvi Air sends E1 in lowercase, e.g. `FF9904e1...`
                return ("E1", candidate[6:].lower())
            case _ if candidate.startswith("FF990403"):
                return (3, candidate[6:])
            case _ if candidate.startswith("16AAFE"):
                # TODO: Check from raw data correct data format
                # Now this returns 2 also for Data Format 4
                url_data = DataFormats._get_data_format_2and4(DataFormats._parse_raw(raw))
                if url_data is not None:
                    return (2, url_data)
                return (None, None)
            case _ if candidate.startswith("095275757669"):
                # This is a RuuviTag, but this advertisement does not contain any data.
                # Set the format to None, and data to '', this allows the
                # caller to determine that we did indeed see a RuuviTag.
                return (None, "")
            case _:
                return (None, None)

    @staticmethod
    def _parse_raw(raw: str) -> str:
        return raw

    @staticmethod
    def _get_data_format_2and4(raw: str) -> RawSensorData:
        """
        Validate that data is from RuuviTag and is Data Format 2 or 4.
        Convert hexadecimal data to string.
        Encoded data part is after ruu.vi/#

        Returns:
            string: Encoded sensor data
        """
        try:
            base16_split = [raw[i : i + 2] for i in range(0, len(raw), 2)]
            selected_hexs = filter(lambda x: int(x, 16) < 128, base16_split)
            characters = [chr(int(c, 16)) for c in selected_hexs]
            data = "".join(characters)

            # take only part after ruu.vi/#
            index = data.find("ruu.vi/#")
            if index > -1:
                return data[(index + 8) :]

            return None
        except Exception:
            return None

    @staticmethod
    def _get_data_format_3(raw: str) -> RawSensorData:
        """
        Validate that data is from RuuviTag and is Data Format 3

        Returns:
            string: Sensor data
        """
        # Search of FF990403 (Manufacturer Specific Data (FF) /
        # Ruuvi Innovations ltd (9904) / Format 3 (03))
        try:
            if "FF990403" not in raw:
                return None

            payload_start = raw.index("FF990403") + 6
            return raw[payload_start:]
        except Exception:
            return None

    @staticmethod
    def _get_data_format_5(raw: str) -> RawSensorData:
        """
        Validate that data is from RuuviTag and is Data Format 5

        Returns:
            string: Sensor data
        """
        # Search of FF990405 (Manufacturer Specific Data (FF) /
        # Ruuvi Innovations ltd (9904) / Format 5 (05))
        try:
            if "FF990405" not in raw:
                return None

            payload_start = raw.index("FF990405") + 6
            return raw[payload_start:]
        except Exception:
            return None

    @staticmethod
    def _get_data_format_e1(raw: str) -> RawSensorData:
        """
        Validate that data is from Ruuvi Air and is Data Format E1

        Returns:
            string: Sensor data
        """
        # Search of ff9904e1 (Manufacturer Specific Data (FF) /
        # Ruuvi Innovations ltd / Data format E1 (Extended v1)
        try:
            raw = raw.lower()
            if "ff9904e1" not in raw:
                return None

            payload_start = raw.index("ff9904e1") + 6
            return raw[payload_start:]
        except Exception:
            return None
