import asyncio
from dataclasses import dataclass
import logging
from bleak import BleakScanner, BleakClient


logging.basicConfig(
    level=logging.INFO,
    format="%(levelname)s: %(message)s",
)


class DeviceError(RuntimeError):
    pass


FIELDS_UNITS = {
    "co2": "ppm",
    "temperature": "°C",
    "pressure": "mbar",
    "humidity": "%",
}


@dataclass
class Device:
    address: str
    name: str
    rssi: int


class Reading:
    def __init__(self, device: Device, response: bytearray):
        self.name: str = device.name
        self.address: str = device.address
        self.rssi: int = device.rssi
        self.co2: int = le16(response)
        self.temperature: float = round(le16(response, 2) / 20, 1)
        self.pressure: float = round(le16(response, 4) / 10, 1)
        self.humidity: float = round(le16(response, 5) / 255, 1)


def le16(data: bytearray, start: int = 0) -> int:
    """Read long integer from specified offset of bytearray"""
    raw = bytearray(data)
    if start + 1 >= len(raw):
        raise DeviceError(
            f"Response too short: expected at least {start + 2} bytes, got {len(raw)} bytes. "
            "On Linux, you may need to pair the device first using bluetoothctl."
        )
    return raw[start] + (raw[start + 1] << 8)


async def discover(timeout: int = 5):
    """Return list of Devices sorted by descending RSSI dBm"""
    discoveries = await BleakScanner.discover(timeout=timeout, return_adv=True)
    devices = [
        Device(address=d.address, name=str(d.name), rssi=a.rssi)
        for d, a in discoveries.values()
    ]
    logging.info(f"Found {len(devices)} device(s)")
    return sorted(devices, key=lambda d: d.rssi, reverse=True)


async def request_measurements(address: str) -> bytearray:
    """Request measurements bytearray for target address"""
    UUID_CURRENT_MEASUREMENTS_SIMPLE = "f0cd1503-95da-4f4b-9ac8-aa55d312af0c"
    async with BleakClient(address) as client:
        return await client.read_gatt_char(UUID_CURRENT_MEASUREMENTS_SIMPLE)


def scan(timeout: int = 5) -> list[Device]:
    """Show Bluetooth devices in the vicinity"""
    return asyncio.run(discover(timeout=timeout))


def discover_ara4s(substring: str = "Aranet4", timeout: int = 5) -> list[Device]:
    """Find Aranet4s in the vicinity"""
    devices = scan(timeout=timeout)
    ara4_devices = [d for d in devices if substring in d.name]
    logging.info(f"Found {len(ara4_devices)} Aranet4 device(s)")
    return ara4_devices


def find_device(address) -> Device:
    """Find Device by address"""
    result = asyncio.run(BleakScanner.find_device_by_address(address, return_adv=True))
    if result:
        d, a = result
        return Device(address=d.address, name=str(d.name), rssi=a.rssi)
    else:
        raise DeviceError(f"Could not find device {address}")


def read(address: str = "", timeout: int = 5) -> Reading:
    if address:
        device = find_device(address)
    else:
        ara4_devices = discover_ara4s(timeout=timeout)
        if not ara4_devices:
            raise DeviceError("No Aranet4 devices discovered")
        else:
            device = ara4_devices[0]
    logging.info(f"Selected {device.name} ({device.rssi}dBm)")
    measurements = asyncio.run(request_measurements(device.address))
    return Reading(device, measurements)
