"""Heart Rate Measurement characteristic implementation."""

from __future__ import annotations

import logging
from enum import IntEnum, IntFlag
from typing import Any, ClassVar

import msgspec

from ...types.gatt_enums import CharacteristicName
from ..constants import UINT16_MAX
from ..context import CharacteristicContext
from .base import BaseCharacteristic
from .body_sensor_location import BodySensorLocation, BodySensorLocationCharacteristic
from .utils import DataParser

logger = logging.getLogger(__name__)

# RR-Interval resolution: 1/1024 seconds per unit
RR_INTERVAL_RESOLUTION = 1024.0


class HeartRateMeasurementFlags(IntFlag):
    """Heart Rate Measurement flags as per Bluetooth SIG specification."""

    HEART_RATE_VALUE_FORMAT_UINT16 = 0x01
    SENSOR_CONTACT_DETECTED = 0x02
    SENSOR_CONTACT_SUPPORTED = 0x04
    ENERGY_EXPENDED_PRESENT = 0x08
    RR_INTERVAL_PRESENT = 0x10


class SensorContactState(IntEnum):
    """Sensor contact state enumeration."""

    NOT_SUPPORTED = 0
    NOT_DETECTED = 1
    DETECTED = 2

    def __str__(self) -> str:
        """Return human-readable sensor contact state."""
        return {0: "not_supported", 1: "not_detected", 2: "detected"}[self.value]

    def __eq__(self, other: object) -> bool:
        """Support comparison with string values for backward compatibility."""
        if isinstance(other, str):
            return str(self) == other
        return super().__eq__(other)

    def __hash__(self) -> int:
        """Make enum hashable."""
        return super().__hash__()

    @classmethod
    def from_flags(cls, flags: int) -> SensorContactState:
        """Create enum from heart rate flags."""
        if not flags & HeartRateMeasurementFlags.SENSOR_CONTACT_SUPPORTED:  # Sensor Contact Supported bit not set
            return cls.NOT_SUPPORTED
        if flags & HeartRateMeasurementFlags.SENSOR_CONTACT_DETECTED:  # Sensor Contact Detected bit set
            return cls.DETECTED
        return cls.NOT_DETECTED


class HeartRateData(msgspec.Struct, frozen=True, kw_only=True):  # pylint: disable=too-few-public-methods
    """Parsed data from Heart Rate Measurement characteristic.

    Attributes:
        heart_rate: Heart rate in beats per minute (0-UINT16_MAX)
        sensor_contact: State of sensor contact detection
        energy_expended: Optional energy expended in kilojoules
        rr_intervals: Tuple of R-R intervals in seconds (immutable)
        flags: Raw flags byte for reference
        sensor_location: Optional body sensor location from context (BodySensorLocation enum)
    """

    heart_rate: int  # BPM (0-UINT16_MAX)
    sensor_contact: SensorContactState
    energy_expended: int | None = None  # kJ
    rr_intervals: tuple[float, ...] = ()  # R-R intervals in seconds, immutable tuple
    flags: HeartRateMeasurementFlags = HeartRateMeasurementFlags(0)  # Raw flags byte for reference
    sensor_location: BodySensorLocation | None = None  # Body sensor location from context (0x2A38)

    def __post_init__(self) -> None:
        """Validate heart rate measurement data."""
        if not 0 <= self.heart_rate <= UINT16_MAX:
            raise ValueError(f"Heart rate must be 0-{UINT16_MAX} bpm, got {self.heart_rate}")
        if self.energy_expended is not None and not 0 <= self.energy_expended <= UINT16_MAX:
            raise ValueError(f"Energy expended must be 0-{UINT16_MAX} kJ, got {self.energy_expended}")
        for interval in self.rr_intervals:
            if not 0.0 <= interval <= (UINT16_MAX / RR_INTERVAL_RESOLUTION):  # Max RR interval in seconds
                raise ValueError(
                    f"RR interval must be 0.0-{UINT16_MAX / RR_INTERVAL_RESOLUTION} seconds, got {interval}"
                )


class HeartRateMeasurementCharacteristic(BaseCharacteristic[HeartRateData]):
    """Heart Rate Measurement characteristic (0x2A37).

    Used in Heart Rate Service (spec: Heart Rate Service 1.0, Heart Rate Profile 1.0)
    to transmit instantaneous heart rate plus optional energy expended and
    RR-Interval metrics.

    Specification summary (flags byte bit assignments - see adopted spec & Errata 23224):
      Bit 0 (0x01): Heart Rate Value Format (0 = uint8, 1 = uint16)
      Bit 1 (0x02): Sensor Contact Supported
      Bit 2 (0x04): Sensor Contact Detected (valid only when Bit1 set)
      Bit 3 (0x08): Energy Expended Present (adds 2 bytes, little-endian, in kilo Joules)
      Bit 4 (0x10): RR-Interval(s) Present (sequence of 16-bit values, units 1/1024 s)
      Bits 5-7: Reserved (must be 0)

    Parsing Rules:
      * Minimum length: 2 bytes (flags + at least one byte of heart rate value)
      * If Bit0 set, heart rate is 16 bits; else 8 bits
      * Energy Expended only parsed if flag set AND 2 bytes remain
      * RR-Intervals parsed greedily in pairs until buffer end when flag set
      * RR-Interval raw value converted to seconds: raw / 1024.0
      * Sensor contact state derived from Bits1/2 tri-state (not supported, not detected, detected)

    Validation:
      * Heart rate validated within 0..UINT16_MAX (spec does not strictly define upper physiological bound)
      * RR interval constrained to 0.0-65.535 s (fits 16-bit / 1024 scaling and guards against malformed data)
      * Energy expended 0..UINT16_MAX

    References:
      * Bluetooth SIG Heart Rate Service 1.0 (https://www.bluetooth.com/specifications/specs/heart-rate-service-1-0/)
      * Bluetooth SIG Heart Rate Profile 1.0 (https://www.bluetooth.com/specifications/specs/heart-rate-profile-1-0/)
      * Errata Correction 23224 (mandatory for compliance)

    """

    _optional_dependencies: ClassVar[list[type[BaseCharacteristic[Any]]]] = [BodySensorLocationCharacteristic]

    # RR-Interval resolution: 1/1024 seconds per unit
    RR_INTERVAL_RESOLUTION = RR_INTERVAL_RESOLUTION

    min_length: int = 2  # Flags(1) + HR(1/2)
    allow_variable_length: bool = True  # Optional energy expended and RR intervals

    def _decode_value(  # pylint: disable=too-many-branches  # Branches needed for spec-compliant parsing
        self, data: bytearray, ctx: CharacteristicContext | None = None, *, validate: bool = True
    ) -> HeartRateData:
        """Parse heart rate measurement data according to Bluetooth specification.

        Format: Flags(1) + Heart Rate Value(1-2) + [Energy Expended(2)] + [RR-Intervals(2*n)]

        Context Enhancement:
            If ctx is provided, this method will attempt to enhance the parsed data with:
            - Body Sensor Location (0x2A38): Location where the sensor is positioned

        Args:
            data: Raw bytearray from BLE characteristic.
            ctx: Optional CharacteristicContext providing surrounding context (may be None).
            validate: Whether to perform validation (currently unused).

        Returns:
            HeartRateData containing parsed heart rate data with metadata and optional
            context-enhanced information.

        """
        flags = HeartRateMeasurementFlags(data[0])
        offset = 1

        # Parse heart rate value (8-bit or 16-bit depending on flag)
        if flags & HeartRateMeasurementFlags.HEART_RATE_VALUE_FORMAT_UINT16:  # 16-bit heart rate value
            if len(data) < offset + 2:
                raise ValueError("Insufficient data for 16-bit heart rate value")
            heart_rate = DataParser.parse_int16(data, offset, signed=False)
            offset += 2
        else:  # 8-bit heart rate value
            heart_rate = DataParser.parse_int8(data, offset, signed=False)
            offset += 1

        sensor_contact = SensorContactState.from_flags(flags)

        # Optional Energy Expended
        energy_expended = None
        if (flags & HeartRateMeasurementFlags.ENERGY_EXPENDED_PRESENT) and len(data) >= offset + 2:
            energy_expended = DataParser.parse_int16(data, offset, signed=False)
            offset += 2

        # Optional RR-Intervals
        rr_intervals: list[float] = []
        if (flags & HeartRateMeasurementFlags.RR_INTERVAL_PRESENT) and len(data) >= offset + 2:
            while offset + 2 <= len(data):
                rr_interval_raw = DataParser.parse_int16(data, offset, signed=False)
                rr_intervals.append(rr_interval_raw / RR_INTERVAL_RESOLUTION)
                offset += 2

        # Context enhancement: Body Sensor Location
        sensor_location = None
        if ctx:
            location_value = self.get_context_characteristic(ctx, CharacteristicName.BODY_SENSOR_LOCATION)
            if location_value is not None:
                # Body Sensor Location is a uint8 enum value (0-6)
                try:
                    sensor_location = BodySensorLocation(location_value)
                except ValueError:
                    # Invalid value outside enum range, leave as None
                    logger.warning(
                        "Invalid Body Sensor Location value %s in context (valid range: 0-6), ignoring",
                        location_value,
                    )

        return HeartRateData(
            heart_rate=heart_rate,
            sensor_contact=sensor_contact,
            energy_expended=energy_expended,
            rr_intervals=tuple(rr_intervals),  # Convert list to tuple for immutable struct
            flags=flags,
            sensor_location=sensor_location,
        )

    def _encode_value(self, data: HeartRateData) -> bytearray:
        """Encode HeartRateData back to bytes.

        The inverse of decode_value respecting the same flag semantics.

        Args:
            data: HeartRateData instance to encode

        Returns:
            Encoded bytes representing the heart rate measurement

        """
        flags = int(data.flags)  # Use the flags from the data structure

        result = bytearray([flags])

        if flags & HeartRateMeasurementFlags.HEART_RATE_VALUE_FORMAT_UINT16:
            result.extend(DataParser.encode_int16(data.heart_rate, signed=False))
        else:
            result.extend(DataParser.encode_int8(data.heart_rate, signed=False))

        if data.energy_expended is not None:
            result.extend(DataParser.encode_int16(data.energy_expended, signed=False))

        for rr_interval in data.rr_intervals:
            rr_raw = round(rr_interval * RR_INTERVAL_RESOLUTION)
            rr_raw = min(rr_raw, UINT16_MAX)
            result.extend(DataParser.encode_int16(rr_raw, signed=False))

        return result
