"""Known BiVital characteristics and payload decoders."""

from __future__ import annotations

import struct
from dataclasses import dataclass
from typing import Any, Callable, Optional

BT_BASE_UUID = "0000{short}-0000-1000-8000-00805f9b34fb"


def canonical_uuid(value: str) -> str:
    """Normalize 16/32/128-bit UUID text to lowercase 128-bit UUID format."""
    raw = value.strip().lower()
    if raw.startswith("0x"):
        raw = raw[2:]
    if len(raw) == 4:
        return BT_BASE_UUID.format(short=raw)
    if len(raw) == 8:
        return f"{raw}-0000-1000-8000-00805f9b34fb"
    return raw


def decode_uint8(payload: bytes) -> int:
    """Decode a little-endian uint8 payload."""
    if len(payload) < 1:
        raise ValueError("payload too short for uint8")
    return payload[0]


def decode_int16_scaled(scale: float) -> Callable[[bytes], float]:
    """Build an int16 decoder with multiplicative scaling."""
    def _decode(payload: bytes) -> float:
        if len(payload) < 2:
            raise ValueError("payload too short for int16")
        return struct.unpack_from("<h", payload, 0)[0] * scale

    return _decode


def decode_uint16_scaled(scale: float) -> Callable[[bytes], float]:
    """Build a uint16 decoder with multiplicative scaling."""
    def _decode(payload: bytes) -> float:
        if len(payload) < 2:
            raise ValueError("payload too short for uint16")
        return struct.unpack_from("<H", payload, 0)[0] * scale

    return _decode


def decode_uint32_scaled(scale: float) -> Callable[[bytes], float]:
    """Build a uint32 decoder with multiplicative scaling."""
    def _decode(payload: bytes) -> float:
        if len(payload) < 4:
            raise ValueError("payload too short for uint32")
        return struct.unpack_from("<I", payload, 0)[0] * scale

    return _decode


def decode_utf8(payload: bytes) -> str:
    """Decode UTF-8 payload and strip trailing NUL padding."""
    return payload.decode("utf-8", errors="replace").rstrip("\x00")


def _parse_int(value: str) -> int:
    """Parse integer text with automatic base detection (`0x`, `0o`, `0b`, decimal)."""
    return int(value.strip(), 0)


def encode_uint8(value: str) -> bytes:
    """Encode uint8 value from text."""
    parsed = _parse_int(value)
    if not 0 <= parsed <= 0xFF:
        raise ValueError("Value must be uint8 (0..255)")
    return bytes([parsed])


def encode_uint16(value: str) -> bytes:
    """Encode uint16 value from text."""
    parsed = _parse_int(value)
    if not 0 <= parsed <= 0xFFFF:
        raise ValueError("Value must be uint16 (0..65535)")
    return struct.pack("<H", parsed)


def encode_uint32(value: str) -> bytes:
    """Encode uint32 value from text."""
    parsed = _parse_int(value)
    if not 0 <= parsed <= 0xFFFFFFFF:
        raise ValueError("Value must be uint32 (0..4294967295)")
    return struct.pack("<I", parsed)


def encode_utf8(value: str) -> bytes:
    """Encode UTF-8 text value."""
    return value.encode("utf-8")


def encode_led(value: str) -> bytes:
    """Encode LED color/mode command into one byte."""
    text = value.strip().lower()
    colors = {
        "off": 0,
        "red": 1,
        "green": 2,
        "yellow": 3,
        "blue": 4,
        "magenta": 5,
        "cyan": 6,
        "white": 7,
    }
    if text in colors:
        return bytes([colors[text]])
    return encode_uint8(text)


def encode_power_mode(value: str) -> bytes:
    """Encode power mode command into one byte."""
    text = value.strip().lower()
    aliases = {
        "running": 0,
        "run": 0,
        "restart": 1,
        "shipping": 2,
        "power_down": 2,
        "powerdown": 2,
    }
    if text in aliases:
        return bytes([aliases[text]])
    parsed = _parse_int(text)
    if parsed not in (0, 1, 2):
        raise ValueError("Power mode must be 0, 1, or 2")
    return bytes([parsed])


def encode_execution_flags(value: str) -> bytes:
    """Encode record mode command (base/record) into one byte."""
    text = value.strip().lower()
    aliases = {
        "base": 0,
        "record": 1,
    }
    if text in aliases:
        return bytes([aliases[text]])
    parsed = _parse_int(text)
    if parsed not in (0, 1):
        raise ValueError("Record mode must be 0 (base) or 1 (record)")
    return bytes([parsed])


def encode_current_time(value: str) -> bytes:
    """Encode current time from unix timestamp text or special value `now`."""
    text = value.strip().lower()
    if text == "now":
        import time

        return struct.pack("<I", int(time.time()))
    return encode_uint32(text)


def decode_motion_acc_gyro_mag(payload: bytes) -> dict[str, tuple[float, float, float]]:
    """Decode combined accelerometer/gyroscope/magnetometer payload."""
    if len(payload) < 18:
        raise ValueError("payload too short for acc/gyro/mag")
    values = struct.unpack_from("<9h", payload, 0)
    return {
        "acc_ms2": tuple(v * (9.80665 / 1024.0) for v in values[0:3]),
        "gyro_dps": tuple(v * (1.0 / 32.0) for v in values[3:6]),
        "mag_uT": tuple(v * (1.0 / 16.0) for v in values[6:9]),
    }


def decode_motion_quaternion(payload: bytes) -> dict[str, float]:
    """Decode quaternion payload."""
    if len(payload) < 16:
        raise ValueError("payload too short for quaternion")
    w, x, y, z = struct.unpack_from("<4i", payload, 0)
    scale = 1.0 / 1073741824.0
    return {"w": w * scale, "x": x * scale, "y": y * scale, "z": z * scale}


def decode_motion_euler(payload: bytes) -> dict[str, float]:
    """Decode Euler angle payload."""
    if len(payload) < 12:
        raise ValueError("payload too short for euler")
    roll, pitch, yaw = struct.unpack_from("<3i", payload, 0)
    scale = 1.0 / 65536.0
    return {"roll_deg": roll * scale, "pitch_deg": pitch * scale, "yaw_deg": yaw * scale}


def decode_steps(payload: bytes) -> dict[str, int]:
    """Decode pedometer payload with support for short and full packet variants."""
    if len(payload) >= 8:
        steps, timestamp = struct.unpack_from("<II", payload, 0)
        return {"steps": steps, "timestamp": timestamp}
    if len(payload) >= 2:
        steps_short = struct.unpack_from("<H", payload, 0)[0]
        return {"steps": steps_short}
    raise ValueError("payload too short for steps")


def decode_ecg(payload: bytes) -> list[int]:
    """Decode 30-sample ECG packet."""
    if len(payload) < 60:
        return []
    return list(struct.unpack_from("<30H", payload, 0))


def decode_heart_rate_measurement(payload: bytes) -> dict[str, object]:
    """Decode Bluetooth SIG Heart Rate Measurement (2A37)."""
    if not payload:
        raise ValueError("empty heart rate payload")

    flags = payload[0]
    hr_16bit = bool(flags & 0x01)
    contact_status_supported = bool(flags & 0x04)
    contact_detected = bool(flags & 0x02) if contact_status_supported else None
    energy_present = bool(flags & 0x08)
    rr_present = bool(flags & 0x10)

    offset = 1
    if hr_16bit:
        if len(payload) < offset + 2:
            raise ValueError("payload too short for 16-bit heart rate")
        bpm = int.from_bytes(payload[offset : offset + 2], byteorder="little")
        offset += 2
    else:
        if len(payload) < offset + 1:
            raise ValueError("payload too short for 8-bit heart rate")
        bpm = payload[offset]
        offset += 1

    energy_expended: int | None = None
    if energy_present:
        if len(payload) < offset + 2:
            raise ValueError("payload too short for energy expended")
        energy_expended = int.from_bytes(payload[offset : offset + 2], byteorder="little")
        offset += 2

    rr_intervals: list[float] = []
    if rr_present:
        while offset + 1 < len(payload):
            rr_raw = int.from_bytes(payload[offset : offset + 2], byteorder="little")
            rr_intervals.append(rr_raw / 1024.0)
            offset += 2

    return {
        "bpm": bpm,
        "contact_detected": contact_detected,
        "energy_expended": energy_expended,
        "rr_intervals": tuple(rr_intervals),
    }


@dataclass(frozen=True)
class ServiceDef:
    """Static metadata describing a BLE service."""
    key: str
    uuid: str
    name: str


@dataclass(frozen=True)
class CharacteristicDef:
    """Static metadata describing a BLE characteristic and its capabilities."""
    key: str
    service_uuid: str
    uuid: str
    name: str
    readable: bool = True
    stream: bool = False
    writable: bool = False
    decoder: Optional[Callable[[bytes], object]] = None
    encoder: Optional[Callable[[str], bytes]] = None


_SERVICES = [
    ServiceDef(
        key="motion",
        uuid=canonical_uuid("ef680400-9b35-4933-9b10-52ffa9740042"),
        name="Motion Service",
    ),
    ServiceDef(
        key="environmental",
        uuid=canonical_uuid("181a"),
        name="Environmental Sensing",
    ),
    ServiceDef(
        key="heart_rate",
        uuid=canonical_uuid("180d"),
        name="Heart Rate",
    ),
    ServiceDef(
        key="battery",
        uuid=canonical_uuid("180f"),
        name="Battery Service",
    ),
    ServiceDef(
        key="custom",
        uuid=canonical_uuid("46cc73ec-90b4-4c73-87bf-3377946b9488"),
        name="Custom Device Config",
    ),
    ServiceDef(
        key="current_time",
        uuid=canonical_uuid("1805"),
        name="Current Time Service",
    ),
    ServiceDef(
        key="device_information",
        uuid=canonical_uuid("180a"),
        name="Device Information Service",
    ),
]

SERVICES_BY_KEY = {item.key: item for item in _SERVICES}
SERVICES_BY_UUID = {item.uuid: item for item in _SERVICES}


_DEFS = [
    CharacteristicDef(
        key="battery_level",
        service_uuid=canonical_uuid("180f"),
        uuid=canonical_uuid("2a19"),
        name="Battery Level",
        stream=True,
        decoder=decode_uint8,
    ),
    CharacteristicDef(
        key="temperature",
        service_uuid=canonical_uuid("181a"),
        uuid=canonical_uuid("2a6e"),
        name="Temperature",
        stream=True,
        decoder=decode_int16_scaled(0.01),
    ),
    CharacteristicDef(
        key="humidity",
        service_uuid=canonical_uuid("181a"),
        uuid=canonical_uuid("2a6f"),
        name="Humidity",
        stream=True,
        decoder=decode_uint16_scaled(0.01),
    ),
    CharacteristicDef(
        key="pressure",
        service_uuid=canonical_uuid("181a"),
        uuid=canonical_uuid("2a6d"),
        name="Pressure",
        stream=True,
        decoder=decode_uint32_scaled(0.1),
    ),
    CharacteristicDef(
        key="heart_rate",
        service_uuid=canonical_uuid("180d"),
        uuid=canonical_uuid("2a37"),
        name="Heart Rate Measurement",
        readable=False,
        stream=True,
        decoder=decode_heart_rate_measurement,
    ),
    CharacteristicDef(
        key="heart_rate_location",
        service_uuid=canonical_uuid("180d"),
        uuid=canonical_uuid("2a38"),
        name="Body Sensor Location",
        stream=False,
        decoder=decode_uint8,
    ),
    CharacteristicDef(
        key="ecg",
        service_uuid=canonical_uuid("180d"),
        uuid=canonical_uuid("2a58"),
        name="ECG",
        readable=False,
        stream=True,
        decoder=decode_ecg,
    ),
    CharacteristicDef(
        key="heart_rate_quality",
        service_uuid=canonical_uuid("180d"),
        uuid=canonical_uuid("85f08df9-1316-4880-bf18-77da89830e2c"),
        name="Heart Rate Quality",
        readable=False,
        stream=True,
        decoder=decode_uint8,
    ),
    CharacteristicDef(
        key="motion_raw",
        service_uuid=canonical_uuid("ef680400-9b35-4933-9b10-52ffa9740042"),
        uuid=canonical_uuid("ef680406-9b35-4933-9b10-52ffa9740042"),
        name="Motion Acc/Gyro/Mag",
        readable=False,
        stream=True,
        decoder=decode_motion_acc_gyro_mag,
    ),
    CharacteristicDef(
        key="motion_quaternion",
        service_uuid=canonical_uuid("ef680400-9b35-4933-9b10-52ffa9740042"),
        uuid=canonical_uuid("ef680404-9b35-4933-9b10-52ffa9740042"),
        name="Motion Quaternion",
        readable=False,
        stream=True,
        decoder=decode_motion_quaternion,
    ),
    CharacteristicDef(
        key="motion_euler",
        service_uuid=canonical_uuid("ef680400-9b35-4933-9b10-52ffa9740042"),
        uuid=canonical_uuid("ef680407-9b35-4933-9b10-52ffa9740042"),
        name="Motion Euler",
        readable=False,
        stream=True,
        decoder=decode_motion_euler,
    ),
    CharacteristicDef(
        key="steps",
        service_uuid=canonical_uuid("ef680400-9b35-4933-9b10-52ffa9740042"),
        uuid=canonical_uuid("ef680405-9b35-4933-9b10-52ffa9740042"),
        name="Pedometer Steps",
        readable=False,
        stream=True,
        decoder=decode_steps,
    ),
    CharacteristicDef(
        key="current_time",
        service_uuid=canonical_uuid("1805"),
        uuid=canonical_uuid("2a2b"),
        name="Current Time",
        stream=False,
        decoder=decode_uint32_scaled(1.0),
        writable=True,
        encoder=encode_current_time,
    ),
    CharacteristicDef(
        key="model_number",
        service_uuid=canonical_uuid("180a"),
        uuid=canonical_uuid("2a24"),
        name="Model Number",
        stream=False,
        decoder=decode_utf8,
    ),
    CharacteristicDef(
        key="manufacturer_name",
        service_uuid=canonical_uuid("180a"),
        uuid=canonical_uuid("2a29"),
        name="Manufacturer Name",
        stream=False,
        decoder=decode_utf8,
    ),
    CharacteristicDef(
        key="software_revision",
        service_uuid=canonical_uuid("180a"),
        uuid=canonical_uuid("2a28"),
        name="Software Revision",
        stream=False,
        decoder=decode_utf8,
    ),
    CharacteristicDef(
        key="led_color",
        service_uuid=canonical_uuid("46cc73ec-90b4-4c73-87bf-3377946b9488"),
        uuid=canonical_uuid("00001525-1212-efde-1523-785feabcd123"),
        name="LED Color",
        stream=False,
        writable=True,
        encoder=encode_led,
    ),
    CharacteristicDef(
        key="power_mode",
        service_uuid=canonical_uuid("46cc73ec-90b4-4c73-87bf-3377946b9488"),
        uuid=canonical_uuid("ee0c2087-8786-40ba-ab96-99b91ac981d8"),
        name="Power Mode",
        readable=False,
        stream=False,
        writable=True,
        encoder=encode_power_mode,
    ),
    CharacteristicDef(
        key="record_mode",
        service_uuid=canonical_uuid("46cc73ec-90b4-4c73-87bf-3377946b9488"),
        uuid=canonical_uuid("ee0c2085-8786-40ba-ab96-99b91ac981d8"),
        name="Record Mode",
        stream=False,
        writable=True,
        encoder=encode_execution_flags,
    ),
    CharacteristicDef(
        key="remote_command",
        service_uuid=canonical_uuid("46cc73ec-90b4-4c73-87bf-3377946b9488"),
        uuid=canonical_uuid("9b3c81d8-57b1-4a8a-b8df-0e56f7ca51c2"),
        name="Remote Command",
        readable=False,
        stream=False,
        decoder=decode_utf8,
        writable=True,
        encoder=encode_utf8,
    ),
    CharacteristicDef(
        key="database_hash",
        service_uuid=canonical_uuid("46cc73ec-90b4-4c73-87bf-3377946b9488"),
        uuid=canonical_uuid("2b2a"),
        name="Database Hash",
        stream=False,
    ),
    CharacteristicDef(
        key="used_storage",
        service_uuid=canonical_uuid("46cc73ec-90b4-4c73-87bf-3377946b9488"),
        uuid=canonical_uuid("2b85"),
        name="Used Storage",
        stream=False,
        decoder=decode_uint32_scaled(1.0 / 1024.0),
    ),
    CharacteristicDef(
        key="object_size",
        service_uuid=canonical_uuid("46cc73ec-90b4-4c73-87bf-3377946b9488"),
        uuid=canonical_uuid("2ac0"),
        name="Object Size",
        stream=False,
    ),
]

CHARACTERISTICS_BY_KEY = {item.key: item for item in _DEFS}
CHARACTERISTICS_BY_UUID = {item.uuid: item for item in _DEFS}


def encode_write_value(key_or_uuid: str, value: str) -> bytes:
    """Encode user text input into BLE write payload for a known key/UUID."""
    key = key_or_uuid.strip().lower()
    known = CHARACTERISTICS_BY_KEY.get(key)
    if known:
        if not known.writable:
            raise ValueError(f"Characteristic '{key}' is not marked writable")
        if known.encoder is None:
            raise ValueError(f"No encoder available for '{key}'")
        return known.encoder(value)

    uuid = canonical_uuid(key_or_uuid)
    by_uuid = CHARACTERISTICS_BY_UUID.get(uuid)
    if by_uuid and by_uuid.encoder is not None:
        return by_uuid.encoder(value)

    text = value.strip()
    if text.startswith("hex:"):
        hex_text = text[4:].replace(" ", "").replace(":", "").replace("-", "")
        return bytes.fromhex(hex_text)
    if text.startswith("utf8:"):
        return text[5:].encode("utf-8")

    raise ValueError(
        "Unknown write format for UUID. Use known key or prefix value with 'hex:' or 'utf8:'."
    )
