# mypy: warn_unused_ignores=False
"""Coding templates for characteristic composition patterns.

This module provides reusable coding template classes that can be composed into
characteristics via dependency injection. Templates are pure coding strategies
that do NOT inherit from BaseCharacteristic.

All templates follow the CodingTemplate protocol and can be used by both SIG
and custom characteristics through composition.

Pipeline architecture:
    bytes → [Extractor] → raw_int → [Translator] → typed_value

Templates that handle single-field data expose `extractor` and `translator`
properties for pipeline access. Complex templates (multi-field, variable-length)
keep monolithic decode/encode since there's no single raw value to intercept.
"""
# pylint: disable=too-many-lines  # Template classes are cohesive - splitting would break composition pattern

from __future__ import annotations

from abc import ABC, abstractmethod
from datetime import datetime
from enum import IntEnum
from typing import Any, Generic, TypeVar

import msgspec

from ...types.gatt_enums import AdjustReason, DayOfWeek
from ..constants import (
    PERCENTAGE_MAX,
    SINT8_MAX,
    SINT8_MIN,
    SINT16_MAX,
    SINT16_MIN,
    SINT24_MAX,
    SINT24_MIN,
    UINT8_MAX,
    UINT16_MAX,
    UINT24_MAX,
    UINT32_MAX,
)
from ..context import CharacteristicContext
from ..exceptions import InsufficientDataError, ValueRangeError
from .utils import DataParser, IEEE11073Parser
from .utils.extractors import (
    FLOAT32,
    SINT8,
    SINT16,
    SINT24,
    SINT32,
    UINT8,
    UINT16,
    UINT24,
    UINT32,
    RawExtractor,
)
from .utils.translators import (
    IDENTITY,
    SFLOAT,
    IdentityTranslator,
    LinearTranslator,
    SfloatTranslator,
    ValueTranslator,
)

# =============================================================================
# TYPE VARIABLES
# =============================================================================

# Type variable for CodingTemplate generic - represents the decoded value type
T_co = TypeVar("T_co", covariant=True)

# Type variable for EnumTemplate - bound to IntEnum
T = TypeVar("T", bound=IntEnum)

# Resolution constants for common measurement scales
_RESOLUTION_INTEGER = 1.0  # Integer resolution (10^0)
_RESOLUTION_TENTH = 0.1  # 0.1 resolution (10^-1)
_RESOLUTION_HUNDREDTH = 0.01  # 0.01 resolution (10^-2)


# =============================================================================
# LEVEL 4 BASE CLASS
# =============================================================================


class CodingTemplate(ABC, Generic[T_co]):
    """Abstract base class for coding templates.

    Templates are pure coding utilities that don't inherit from BaseCharacteristic.
    They provide coding strategies that can be injected into characteristics.
    All templates MUST inherit from this base class and implement the required methods.

    Generic over T_co, the type of value produced by _decode_value.
    Concrete templates specify their return type, e.g., CodingTemplate[int].

    Pipeline Integration:
        Simple templates (single-field) expose `extractor` and `translator` properties
        for the decode/encode pipeline. Complex templates return None for these properties.
    """

    @abstractmethod
    def decode_value(
        self, data: bytearray, offset: int = 0, ctx: CharacteristicContext | None = None, *, validate: bool = True
    ) -> T_co:
        """Decode raw bytes to typed value.

        Args:
            data: Raw bytes to parse
            offset: Byte offset to start parsing from
            ctx: Optional context for parsing
            validate: Whether to validate ranges (default True)

        Returns:
            Parsed value of type T_co

        """

    @abstractmethod
    def encode_value(self, value: T_co, *, validate: bool = True) -> bytearray:  # type: ignore[misc]  # Covariant type in parameter is intentional for encode/decode symmetry
        """Encode typed value to raw bytes.

        Args:
            value: Typed value to encode
            validate: Whether to validate ranges (default True)

        Returns:
            Raw bytes representing the value

        """

    @property
    @abstractmethod
    def data_size(self) -> int:
        """Size of data in bytes that this template handles."""

    @property
    def extractor(self) -> RawExtractor | None:
        """Get the raw byte extractor for pipeline access.

        Returns None for complex templates where extraction isn't separable.
        """
        return None

    @property
    def translator(self) -> ValueTranslator[Any] | None:
        """Get the value translator for pipeline access.

        Returns None for complex templates where translation isn't separable.
        """
        return None


# =============================================================================
# DATA STRUCTURES
# =============================================================================


class VectorData(msgspec.Struct, frozen=True, kw_only=True):  # pylint: disable=too-few-public-methods
    """3D vector measurement data."""

    x_axis: float
    y_axis: float
    z_axis: float


class Vector2DData(msgspec.Struct, frozen=True, kw_only=True):  # pylint: disable=too-few-public-methods
    """2D vector measurement data."""

    x_axis: float
    y_axis: float


class TimeData(msgspec.Struct, frozen=True, kw_only=True):  # pylint: disable=too-few-public-methods
    """Time characteristic data structure."""

    date_time: datetime | None
    day_of_week: DayOfWeek
    fractions256: int
    adjust_reason: AdjustReason


# =============================================================================
# BASIC INTEGER TEMPLATES
# =============================================================================


class Uint8Template(CodingTemplate[int]):
    """Template for 8-bit unsigned integer parsing (0-255)."""

    @property
    def data_size(self) -> int:
        """Size: 1 byte."""
        return 1

    @property
    def extractor(self) -> RawExtractor:
        """Get uint8 extractor."""
        return UINT8

    @property
    def translator(self) -> ValueTranslator[int]:
        """Return identity translator for no scaling."""
        return IDENTITY

    def decode_value(
        self, data: bytearray, offset: int = 0, ctx: CharacteristicContext | None = None, *, validate: bool = True
    ) -> int:
        """Parse 8-bit unsigned integer."""
        if validate and len(data) < offset + 1:
            raise InsufficientDataError("uint8", data[offset:], 1)
        return self.extractor.extract(data, offset)

    def encode_value(self, value: int, *, validate: bool = True) -> bytearray:
        """Encode uint8 value to bytes."""
        if validate and not 0 <= value <= UINT8_MAX:
            raise ValueError(f"Value {value} out of range for uint8 (0-{UINT8_MAX})")
        return self.extractor.pack(value)


class Sint8Template(CodingTemplate[int]):
    """Template for 8-bit signed integer parsing (-128 to 127)."""

    @property
    def data_size(self) -> int:
        """Size: 1 byte."""
        return 1

    @property
    def extractor(self) -> RawExtractor:
        """Get sint8 extractor."""
        return SINT8

    @property
    def translator(self) -> ValueTranslator[int]:
        """Return identity translator for no scaling."""
        return IDENTITY

    def decode_value(
        self, data: bytearray, offset: int = 0, ctx: CharacteristicContext | None = None, *, validate: bool = True
    ) -> int:
        """Parse 8-bit signed integer."""
        if validate and len(data) < offset + 1:
            raise InsufficientDataError("sint8", data[offset:], 1)
        return self.extractor.extract(data, offset)

    def encode_value(self, value: int, *, validate: bool = True) -> bytearray:
        """Encode sint8 value to bytes."""
        if validate and not SINT8_MIN <= value <= SINT8_MAX:
            raise ValueError(f"Value {value} out of range for sint8 ({SINT8_MIN} to {SINT8_MAX})")
        return self.extractor.pack(value)


class Uint16Template(CodingTemplate[int]):
    """Template for 16-bit unsigned integer parsing (0-65535)."""

    @property
    def data_size(self) -> int:
        """Size: 2 bytes."""
        return 2

    @property
    def extractor(self) -> RawExtractor:
        """Get uint16 extractor."""
        return UINT16

    @property
    def translator(self) -> ValueTranslator[int]:
        """Return identity translator for no scaling."""
        return IDENTITY

    def decode_value(
        self, data: bytearray, offset: int = 0, ctx: CharacteristicContext | None = None, *, validate: bool = True
    ) -> int:
        """Parse 16-bit unsigned integer."""
        if validate and len(data) < offset + 2:
            raise InsufficientDataError("uint16", data[offset:], 2)
        return self.extractor.extract(data, offset)

    def encode_value(self, value: int, *, validate: bool = True) -> bytearray:
        """Encode uint16 value to bytes."""
        if validate and not 0 <= value <= UINT16_MAX:
            raise ValueError(f"Value {value} out of range for uint16 (0-{UINT16_MAX})")
        return self.extractor.pack(value)


class Sint16Template(CodingTemplate[int]):
    """Template for 16-bit signed integer parsing (-32768 to 32767)."""

    @property
    def data_size(self) -> int:
        """Size: 2 bytes."""
        return 2

    @property
    def extractor(self) -> RawExtractor:
        """Get sint16 extractor."""
        return SINT16

    @property
    def translator(self) -> ValueTranslator[int]:
        """Return identity translator for no scaling."""
        return IDENTITY

    def decode_value(
        self, data: bytearray, offset: int = 0, ctx: CharacteristicContext | None = None, *, validate: bool = True
    ) -> int:
        """Parse 16-bit signed integer."""
        if validate and len(data) < offset + 2:
            raise InsufficientDataError("sint16", data[offset:], 2)
        return self.extractor.extract(data, offset)

    def encode_value(self, value: int, *, validate: bool = True) -> bytearray:
        """Encode sint16 value to bytes."""
        if validate and not SINT16_MIN <= value <= SINT16_MAX:
            raise ValueError(f"Value {value} out of range for sint16 ({SINT16_MIN} to {SINT16_MAX})")
        return self.extractor.pack(value)


class Uint24Template(CodingTemplate[int]):
    """Template for 24-bit unsigned integer parsing (0-16777215)."""

    @property
    def data_size(self) -> int:
        """Size: 3 bytes."""
        return 3

    @property
    def extractor(self) -> RawExtractor:
        """Get uint24 extractor."""
        return UINT24

    @property
    def translator(self) -> ValueTranslator[int]:
        """Return identity translator for no scaling."""
        return IDENTITY

    def decode_value(
        self, data: bytearray, offset: int = 0, ctx: CharacteristicContext | None = None, *, validate: bool = True
    ) -> int:
        """Parse 24-bit unsigned integer."""
        if validate and len(data) < offset + 3:
            raise InsufficientDataError("uint24", data[offset:], 3)
        return self.extractor.extract(data, offset)

    def encode_value(self, value: int, *, validate: bool = True) -> bytearray:
        """Encode uint24 value to bytes."""
        if validate and not 0 <= value <= UINT24_MAX:
            raise ValueError(f"Value {value} out of range for uint24 (0-{UINT24_MAX})")
        return self.extractor.pack(value)


class Uint32Template(CodingTemplate[int]):
    """Template for 32-bit unsigned integer parsing."""

    @property
    def data_size(self) -> int:
        """Size: 4 bytes."""
        return 4

    @property
    def extractor(self) -> RawExtractor:
        """Get uint32 extractor."""
        return UINT32

    @property
    def translator(self) -> ValueTranslator[int]:
        """Return identity translator for no scaling."""
        return IDENTITY

    def decode_value(
        self, data: bytearray, offset: int = 0, ctx: CharacteristicContext | None = None, *, validate: bool = True
    ) -> int:
        """Parse 32-bit unsigned integer."""
        if validate and len(data) < offset + 4:
            raise InsufficientDataError("uint32", data[offset:], 4)
        return self.extractor.extract(data, offset)

    def encode_value(self, value: int, *, validate: bool = True) -> bytearray:
        """Encode uint32 value to bytes."""
        if validate and not 0 <= value <= UINT32_MAX:
            raise ValueError(f"Value {value} out of range for uint32 (0-{UINT32_MAX})")
        return self.extractor.pack(value)


class EnumTemplate(CodingTemplate[T]):
    """Template for IntEnum encoding/decoding with configurable byte size.

    Maps raw integer bytes to Python IntEnum instances through extraction and validation.
    Supports any integer-based enum with any extractor (UINT8, UINT16, SINT8, etc.).

    This template validates enum membership explicitly, supporting non-contiguous
    enum ranges (e.g., values 0, 2, 5, 10).

    Pipeline Integration:
        bytes → [extractor] → raw_int → [IDENTITY translator] → int → enum constructor

    Examples:
        >>> class Status(IntEnum):
        ...     IDLE = 0
        ...     ACTIVE = 1
        ...     ERROR = 2
        >>>
        >>> # Create template with factory method
        >>> template = EnumTemplate.uint8(Status)
        >>>
        >>> # Or with explicit extractor
        >>> template = EnumTemplate(Status, UINT8)
        >>>
        >>> # Decode from bytes
        >>> status = template.decode_value(bytearray([0x01]))  # Status.ACTIVE
        >>>
        >>> # Encode enum to bytes
        >>> data = template.encode_value(Status.ERROR)  # bytearray([0x02])
        >>>
        >>> # Encode int to bytes (also supported)
        >>> data = template.encode_value(2)  # bytearray([0x02])
    """

    def __init__(self, enum_class: type[T], extractor: RawExtractor) -> None:
        """Initialize with enum class and extractor.

        Args:
            enum_class: IntEnum subclass to encode/decode
            extractor: Raw extractor defining byte size and signedness
                      (e.g., UINT8, UINT16, SINT8, etc.)
        """
        self._enum_class = enum_class
        self._extractor = extractor

    @property
    def data_size(self) -> int:
        """Return byte size required for encoding."""
        return self._extractor.byte_size

    @property
    def extractor(self) -> RawExtractor:
        """Return extractor for pipeline access."""
        return self._extractor

    @property
    def translator(self) -> ValueTranslator[int]:
        """Get IDENTITY translator for enums (no scaling needed)."""
        return IDENTITY

    def decode_value(
        self, data: bytearray, offset: int = 0, ctx: CharacteristicContext | None = None, *, validate: bool = True
    ) -> T:
        """Decode bytes to enum instance.

        Args:
            data: Raw bytes from BLE characteristic
            offset: Starting offset in data buffer
            ctx: Optional context for parsing
            validate: Whether to validate enum membership (default True)

        Returns:
            Enum instance of type T

        Raises:
            InsufficientDataError: If data too short for required byte size
            ValueRangeError: If raw value not a valid enum member and validate=True
        """
        # Check data length
        if validate and len(data) < offset + self.data_size:
            raise InsufficientDataError(self._enum_class.__name__, data[offset:], self.data_size)

        # Extract raw integer value
        raw_value = self._extractor.extract(data, offset)

        # Validate enum membership and construct
        try:
            return self._enum_class(raw_value)
        except ValueError as e:
            # Get valid range from enum members
            valid_values = [member.value for member in self._enum_class]
            min_val = min(valid_values)
            max_val = max(valid_values)
            raise ValueRangeError(self._enum_class.__name__, raw_value, min_val, max_val) from e

    def encode_value(self, value: T | int, *, validate: bool = True) -> bytearray:
        """Encode enum instance or int to bytes.

        Args:
            value: Enum instance or integer value to encode
            validate: Whether to validate enum membership (default True)

        Returns:
            Encoded bytes

        Raises:
            ValueError: If value not a valid enum member and validate=True
        """
        # Convert to int if enum instance
        int_value = value.value if isinstance(value, self._enum_class) else int(value)

        # Validate membership
        if validate:
            valid_values = [member.value for member in self._enum_class]
            if int_value not in valid_values:
                min_val = min(valid_values)
                max_val = max(valid_values)
                raise ValueError(
                    f"{self._enum_class.__name__} value {int_value} is invalid. "
                    f"Valid range: {min_val}-{max_val}, valid values: {sorted(valid_values)}"
                )

        # Pack to bytes
        return self._extractor.pack(int_value)

    @classmethod
    def uint8(cls, enum_class: type[T]) -> EnumTemplate[T]:
        """Create EnumTemplate for 1-byte unsigned enum.

        Args:
            enum_class: IntEnum subclass with values 0-255

        Returns:
            Configured EnumTemplate instance

        Example::
            >>> class Status(IntEnum):
            ...     IDLE = 0
            ...     ACTIVE = 1
            >>> template = EnumTemplate.uint8(Status)
        """
        return cls(enum_class, UINT8)

    @classmethod
    def uint16(cls, enum_class: type[T]) -> EnumTemplate[T]:
        """Create EnumTemplate for 2-byte unsigned enum.

        Args:
            enum_class: IntEnum subclass with values 0-65535

        Returns:
            Configured EnumTemplate instance

        Example::
            >>> class ExtendedStatus(IntEnum):
            ...     STATE_1 = 0x0100
            ...     STATE_2 = 0x0200
            >>> template = EnumTemplate.uint16(ExtendedStatus)
        """
        return cls(enum_class, UINT16)

    @classmethod
    def uint32(cls, enum_class: type[T]) -> EnumTemplate[T]:
        """Create EnumTemplate for 4-byte unsigned enum.

        Args:
            enum_class: IntEnum subclass with values 0-4294967295

        Returns:
            Configured EnumTemplate instance
        """
        return cls(enum_class, UINT32)

    @classmethod
    def sint8(cls, enum_class: type[T]) -> EnumTemplate[T]:
        """Create EnumTemplate for 1-byte signed enum.

        Args:
            enum_class: IntEnum subclass with values -128 to 127

        Returns:
            Configured EnumTemplate instance

        Example::
            >>> class Temperature(IntEnum):
            ...     FREEZING = -10
            ...     NORMAL = 0
            ...     HOT = 10
            >>> template = EnumTemplate.sint8(Temperature)
        """
        return cls(enum_class, SINT8)

    @classmethod
    def sint16(cls, enum_class: type[T]) -> EnumTemplate[T]:
        """Create EnumTemplate for 2-byte signed enum.

        Args:
            enum_class: IntEnum subclass with values -32768 to 32767

        Returns:
            Configured EnumTemplate instance
        """
        return cls(enum_class, SINT16)

    @classmethod
    def sint32(cls, enum_class: type[T]) -> EnumTemplate[T]:
        """Create EnumTemplate for 4-byte signed enum.

        Args:
            enum_class: IntEnum subclass with values -2147483648 to 2147483647

        Returns:
            Configured EnumTemplate instance
        """
        return cls(enum_class, SINT32)


# =============================================================================
# SCALED VALUE TEMPLATES
# =============================================================================


class ScaledTemplate(CodingTemplate[float]):
    """Base class for scaled integer templates.

    Handles common scaling logic: value = (raw + offset) * scale_factor
    Subclasses implement raw parsing/encoding and range checking.

    Exposes `extractor` and `translator` for pipeline access.
    """

    _extractor: RawExtractor
    _translator: LinearTranslator

    def __init__(self, scale_factor: float, offset: int) -> None:
        """Initialize with scale factor and offset.

        Args:
            scale_factor: Factor to multiply raw value by
            offset: Offset to add to raw value before scaling

        """
        self._translator = LinearTranslator(scale_factor=scale_factor, offset=offset)

    @property
    def scale_factor(self) -> float:
        """Get the scale factor."""
        return self._translator.scale_factor

    @property
    def offset(self) -> int:
        """Get the offset."""
        return self._translator.offset

    @property
    def extractor(self) -> RawExtractor:
        """Get the byte extractor for pipeline access."""
        return self._extractor

    @property
    def translator(self) -> LinearTranslator:
        """Get the value translator for pipeline access."""
        return self._translator

    def decode_value(
        self, data: bytearray, offset: int = 0, ctx: CharacteristicContext | None = None, *, validate: bool = True
    ) -> float:
        """Parse scaled integer value."""
        raw_value = self._extractor.extract(data, offset)
        return self._translator.translate(raw_value)

    def encode_value(self, value: float, *, validate: bool = True) -> bytearray:
        """Encode scaled value to bytes."""
        raw_value = self._translator.untranslate(value)
        if validate:
            self._check_range(raw_value)
        return self._extractor.pack(raw_value)

    @abstractmethod
    def _check_range(self, raw: int) -> None:
        """Check if raw value is in valid range."""

    @classmethod
    def from_scale_offset(cls, scale_factor: float, offset: int) -> ScaledTemplate:
        """Create instance using scale factor and offset.

        Args:
            scale_factor: Factor to multiply raw value by
            offset: Offset to add to raw value before scaling

        Returns:
            ScaledTemplate instance

        """
        return cls(scale_factor=scale_factor, offset=offset)

    @classmethod
    def from_letter_method(cls, M: int, d: int, b: int) -> ScaledTemplate:  # noqa: N803
        """Create instance using Bluetooth SIG M, d, b parameters.

        Args:
            M: Multiplier factor
            d: Decimal exponent (10^d)
            b: Offset to add to raw value before scaling

        Returns:
            ScaledTemplate instance

        """
        scale_factor = M * (10**d)
        return cls(scale_factor=scale_factor, offset=b)


class ScaledUint16Template(ScaledTemplate):
    """Template for scaled 16-bit unsigned integer.

    Used for values that need decimal precision encoded as integers.
    Can be initialized with scale_factor/offset or Bluetooth SIG M, d, b parameters.
    Formula: value = scale_factor * (raw + offset) or value = M * 10^d * (raw + b)
    Example: Temperature 25.5°C stored as 2550 with scale_factor=0.01, offset=0 or M=1, d=-2, b=0
    """

    _extractor = UINT16

    def __init__(self, scale_factor: float = 1.0, offset: int = 0) -> None:
        """Initialize with scale factor and offset.

        Args:
            scale_factor: Factor to multiply raw value by
            offset: Offset to add to raw value before scaling

        """
        super().__init__(scale_factor, offset)

    @property
    def data_size(self) -> int:
        """Size: 2 bytes."""
        return 2

    def _check_range(self, raw: int) -> None:
        """Check range for uint16."""
        if not 0 <= raw <= UINT16_MAX:
            raise ValueError(f"Scaled value {raw} out of range for uint16")


class ScaledSint16Template(ScaledTemplate):
    """Template for scaled 16-bit signed integer.

    Used for signed values that need decimal precision encoded as integers.
    Can be initialized with scale_factor/offset or Bluetooth SIG M, d, b parameters.
    Formula: value = scale_factor * (raw + offset) or value = M * 10^d * (raw + b)
    Example: Temperature -10.5°C stored as -1050 with scale_factor=0.01, offset=0 or M=1, d=-2, b=0
    """

    _extractor = SINT16

    def __init__(self, scale_factor: float = 0.01, offset: int = 0) -> None:
        """Initialize with scale factor and offset.

        Args:
            scale_factor: Factor to multiply raw value by
            offset: Offset to add to raw value before scaling

        """
        super().__init__(scale_factor, offset)

    @property
    def data_size(self) -> int:
        """Size: 2 bytes."""
        return 2

    def _check_range(self, raw: int) -> None:
        """Check range for sint16."""
        if not SINT16_MIN <= raw <= SINT16_MAX:
            raise ValueError(f"Scaled value {raw} out of range for sint16")


class ScaledSint8Template(ScaledTemplate):
    """Template for scaled 8-bit signed integer.

    Used for signed values that need decimal precision encoded as integers.
    Can be initialized with scale_factor/offset or Bluetooth SIG M, d, b parameters.
    Formula: value = scale_factor * (raw + offset) or value = M * 10^d * (raw + b)
    Example: Temperature with scale_factor=0.01, offset=0 or M=1, d=-2, b=0
    """

    _extractor = SINT8

    def __init__(self, scale_factor: float = 1.0, offset: int = 0) -> None:
        """Initialize with scale factor and offset.

        Args:
            scale_factor: Factor to multiply raw value by
            offset: Offset to add to raw value before scaling

        """
        super().__init__(scale_factor, offset)

    @property
    def data_size(self) -> int:
        """Size: 1 byte."""
        return 1

    def _check_range(self, raw: int) -> None:
        """Check range for sint8."""
        if not SINT8_MIN <= raw <= SINT8_MAX:
            raise ValueError(f"Scaled value {raw} out of range for sint8")


class ScaledUint8Template(ScaledTemplate):
    """Template for scaled 8-bit unsigned integer.

    Used for unsigned values that need decimal precision encoded as integers.
    Can be initialized with scale_factor/offset or Bluetooth SIG M, d, b parameters.
    Formula: value = scale_factor * (raw + offset) or value = M * 10^d * (raw + b)
    Example: Uncertainty with scale_factor=0.1, offset=0 or M=1, d=-1, b=0
    """

    _extractor = UINT8

    def __init__(self, scale_factor: float = 1.0, offset: int = 0) -> None:
        """Initialize with scale factor and offset.

        Args:
            scale_factor: Factor to multiply raw value by
            offset: Offset to add to raw value before scaling

        """
        super().__init__(scale_factor, offset)

    @property
    def data_size(self) -> int:
        """Size: 1 byte."""
        return 1

    def _check_range(self, raw: int) -> None:
        """Check range for uint8."""
        if not 0 <= raw <= UINT8_MAX:
            raise ValueError(f"Scaled value {raw} out of range for uint8")


class ScaledUint32Template(ScaledTemplate):
    """Template for scaled 32-bit unsigned integer with configurable resolution and offset."""

    _extractor = UINT32

    def __init__(self, scale_factor: float = 0.1, offset: int = 0) -> None:
        """Initialize with scale factor and offset.

        Args:
            scale_factor: Factor to multiply raw value by (e.g., 0.1 for 1 decimal place)
            offset: Offset to add to raw value before scaling

        """
        super().__init__(scale_factor, offset)

    @property
    def data_size(self) -> int:
        """Size: 4 bytes."""
        return 4

    def _check_range(self, raw: int) -> None:
        """Check range for uint32."""
        if not 0 <= raw <= UINT32_MAX:
            raise ValueError(f"Scaled value {raw} out of range for uint32")


class ScaledUint24Template(ScaledTemplate):
    """Template for scaled 24-bit unsigned integer with configurable resolution and offset.

    Used for values encoded in 3 bytes as unsigned integers.
    Example: Illuminance 1000 lux stored as bytes with scale_factor=1.0, offset=0
    """

    _extractor = UINT24

    def __init__(self, scale_factor: float = 1.0, offset: int = 0) -> None:
        """Initialize with scale factor and offset.

        Args:
            scale_factor: Factor to multiply raw value by
            offset: Offset to add to raw value before scaling

        """
        super().__init__(scale_factor, offset)

    @property
    def data_size(self) -> int:
        """Size: 3 bytes."""
        return 3

    def _check_range(self, raw: int) -> None:
        """Check range for uint24."""
        if not 0 <= raw <= UINT24_MAX:
            raise ValueError(f"Scaled value {raw} out of range for uint24")


class ScaledSint24Template(ScaledTemplate):
    """Template for scaled 24-bit signed integer with configurable resolution and offset.

    Used for signed values encoded in 3 bytes.
    Example: Elevation 500.00m stored as bytes with scale_factor=0.01, offset=0
    """

    _extractor = SINT24

    def __init__(self, scale_factor: float = 0.01, offset: int = 0) -> None:
        """Initialize with scale factor and offset.

        Args:
            scale_factor: Factor to multiply raw value by
            offset: Offset to add to raw value before scaling

        """
        super().__init__(scale_factor, offset)

    @property
    def data_size(self) -> int:
        """Size: 3 bytes."""
        return 3

    def _check_range(self, raw: int) -> None:
        """Check range for sint24."""
        if not SINT24_MIN <= raw <= SINT24_MAX:
            raise ValueError(f"Scaled value {raw} out of range for sint24")


class ScaledSint32Template(ScaledTemplate):
    """Template for scaled 32-bit signed integer with configurable resolution and offset.

    Used for signed values encoded in 4 bytes.
    Example: Longitude -180.0 to 180.0 degrees stored with scale_factor=1e-7
    """

    _extractor = SINT32

    def __init__(self, scale_factor: float = 0.01, offset: int = 0) -> None:
        """Initialize with scale factor and offset.

        Args:
            scale_factor: Factor to multiply raw value by
            offset: Offset to add to raw value before scaling

        """
        super().__init__(scale_factor, offset)

    @property
    def data_size(self) -> int:
        """Size: 4 bytes."""
        return 4

    def _check_range(self, raw: int) -> None:
        """Check range for sint32."""
        sint32_min = -(2**31)
        sint32_max = (2**31) - 1
        if not sint32_min <= raw <= sint32_max:
            raise ValueError(f"Scaled value {raw} out of range for sint32")


# =============================================================================
# DOMAIN-SPECIFIC TEMPLATES
# =============================================================================


class PercentageTemplate(CodingTemplate[int]):
    """Template for percentage values (0-100%) using uint8."""

    @property
    def data_size(self) -> int:
        """Size: 1 byte."""
        return 1

    @property
    def extractor(self) -> RawExtractor:
        """Get uint8 extractor."""
        return UINT8

    @property
    def translator(self) -> IdentityTranslator:
        """Return identity translator since validation is separate from translation."""
        return IDENTITY

    def decode_value(
        self, data: bytearray, offset: int = 0, ctx: CharacteristicContext | None = None, *, validate: bool = True
    ) -> int:
        """Parse percentage value."""
        if validate and len(data) < offset + 1:
            raise InsufficientDataError("percentage", data[offset:], 1)
        value = self.extractor.extract(data, offset)
        # Only validate range if validation is enabled
        if validate and not 0 <= value <= PERCENTAGE_MAX:
            raise ValueRangeError("percentage", value, 0, PERCENTAGE_MAX)
        return self.translator.translate(value)

    def encode_value(self, value: int, *, validate: bool = True) -> bytearray:
        """Encode percentage value to bytes."""
        if validate and not 0 <= value <= PERCENTAGE_MAX:
            raise ValueError(f"Percentage value {value} out of range (0-{PERCENTAGE_MAX})")
        raw = self.translator.untranslate(value)
        return self.extractor.pack(raw)


class TemperatureTemplate(CodingTemplate[float]):
    """Template for standard Bluetooth SIG temperature format (sint16, 0.01°C resolution)."""

    def __init__(self) -> None:
        """Initialize with standard temperature resolution."""
        self._scaled_template = ScaledSint16Template.from_letter_method(1, -2, 0)

    @property
    def data_size(self) -> int:
        """Size: 2 bytes."""
        return 2

    @property
    def extractor(self) -> RawExtractor:
        """Get extractor from underlying scaled template."""
        return self._scaled_template.extractor

    @property
    def translator(self) -> ValueTranslator[float]:
        """Return the linear translator from the underlying scaled template."""
        return self._scaled_template.translator

    def decode_value(
        self, data: bytearray, offset: int = 0, ctx: CharacteristicContext | None = None, *, validate: bool = True
    ) -> float:
        """Parse temperature in 0.01°C resolution."""
        return self._scaled_template.decode_value(data, offset, ctx, validate=validate)  # pylint: disable=protected-access

    def encode_value(self, value: float, *, validate: bool = True) -> bytearray:
        """Encode temperature to bytes."""
        return self._scaled_template.encode_value(value, validate=validate)  # pylint: disable=protected-access


class ConcentrationTemplate(CodingTemplate[float]):
    """Template for concentration measurements with configurable resolution.

    Used for environmental sensors like CO2, VOC, particulate matter, etc.
    """

    def __init__(self, resolution: float = 1.0) -> None:
        """Initialize with resolution.

        Args:
            resolution: Measurement resolution (e.g., 1.0 for integer ppm, 0.1 for 0.1 ppm)

        """
        # Convert resolution to M, d, b parameters when it fits the pattern
        # resolution = M * 10^d, so we find M and d such that M * 10^d = resolution
        if resolution == _RESOLUTION_INTEGER:
            # resolution = 1 * 10^0  # noqa: ERA001
            self._scaled_template = ScaledUint16Template.from_letter_method(M=1, d=0, b=0)
        elif resolution == _RESOLUTION_TENTH:
            # resolution = 1 * 10^-1  # noqa: ERA001
            self._scaled_template = ScaledUint16Template.from_letter_method(M=1, d=-1, b=0)
        elif resolution == _RESOLUTION_HUNDREDTH:
            # resolution = 1 * 10^-2  # noqa: ERA001
            self._scaled_template = ScaledUint16Template.from_letter_method(M=1, d=-2, b=0)
        else:
            # Fallback to scale_factor for resolutions that don't fit M * 10^d pattern
            self._scaled_template = ScaledUint16Template(scale_factor=resolution)

    @classmethod
    def from_letter_method(cls, M: int, d: int, b: int = 0) -> ConcentrationTemplate:  # noqa: N803
        """Create instance using Bluetooth SIG M, d, b parameters.

        Args:
            M: Multiplier factor
            d: Decimal exponent (10^d)
            b: Offset to add to raw value before scaling

        Returns:
            ConcentrationTemplate instance

        """
        instance = cls.__new__(cls)
        instance._scaled_template = ScaledUint16Template.from_letter_method(M=M, d=d, b=b)
        return instance

    @property
    def data_size(self) -> int:
        """Size: 2 bytes."""
        return 2

    @property
    def extractor(self) -> RawExtractor:
        """Get extractor from underlying scaled template."""
        return self._scaled_template.extractor

    @property
    def translator(self) -> ValueTranslator[float]:
        """Return the linear translator from the underlying scaled template."""
        return self._scaled_template.translator

    def decode_value(
        self, data: bytearray, offset: int = 0, ctx: CharacteristicContext | None = None, *, validate: bool = True
    ) -> float:
        """Parse concentration with resolution."""
        return self._scaled_template.decode_value(data, offset, ctx, validate=validate)  # pylint: disable=protected-access

    def encode_value(self, value: float, *, validate: bool = True) -> bytearray:
        """Encode concentration value to bytes."""
        return self._scaled_template.encode_value(value, validate=validate)  # pylint: disable=protected-access


class PressureTemplate(CodingTemplate[float]):
    """Template for pressure measurements (uint32, 0.1 Pa resolution)."""

    def __init__(self) -> None:
        """Initialize with standard pressure resolution (0.1 Pa)."""
        self._scaled_template = ScaledUint32Template(scale_factor=0.1)

    @property
    def data_size(self) -> int:
        """Size: 4 bytes."""
        return 4

    @property
    def extractor(self) -> RawExtractor:
        """Get extractor from underlying scaled template."""
        return self._scaled_template.extractor

    @property
    def translator(self) -> ValueTranslator[float]:
        """Return the linear translator from the underlying scaled template."""
        return self._scaled_template.translator

    def decode_value(
        self, data: bytearray, offset: int = 0, ctx: CharacteristicContext | None = None, *, validate: bool = True
    ) -> float:
        """Parse pressure in 0.1 Pa resolution (returns Pa)."""
        return self._scaled_template.decode_value(data, offset, ctx, validate=validate)  # pylint: disable=protected-access

    def encode_value(self, value: float, *, validate: bool = True) -> bytearray:
        """Encode pressure to bytes."""
        return self._scaled_template.encode_value(value, validate=validate)  # pylint: disable=protected-access


class TimeDataTemplate(CodingTemplate[TimeData]):
    """Template for Bluetooth SIG time data parsing (10 bytes).

    Used for Current Time and Time with DST characteristics.
    Structure: Date Time (7 bytes) + Day of Week (1) + Fractions256 (1) + Adjust Reason (1)
    """

    LENGTH = 10
    DAY_OF_WEEK_MAX = 7
    FRACTIONS256_MAX = 255
    ADJUST_REASON_MAX = 255

    @property
    def data_size(self) -> int:
        """Size: 10 bytes."""
        return self.LENGTH

    def decode_value(
        self, data: bytearray, offset: int = 0, ctx: CharacteristicContext | None = None, *, validate: bool = True
    ) -> TimeData:
        """Parse time data from bytes."""
        if validate and len(data) < offset + self.LENGTH:
            raise InsufficientDataError("time data", data[offset:], self.LENGTH)

        # Parse Date Time (7 bytes)
        year = DataParser.parse_int16(data, offset, signed=False)
        month = data[offset + 2]
        day = data[offset + 3]

        date_time = None if year == 0 or month == 0 or day == 0 else IEEE11073Parser.parse_timestamp(data, offset)

        # Parse Day of Week (1 byte)
        day_of_week_raw = data[offset + 7]
        if validate and day_of_week_raw > self.DAY_OF_WEEK_MAX:
            raise ValueRangeError("day_of_week", day_of_week_raw, 0, self.DAY_OF_WEEK_MAX)
        day_of_week = DayOfWeek(day_of_week_raw)

        # Parse Fractions256 (1 byte)
        fractions256 = data[offset + 8]

        # Parse Adjust Reason (1 byte)
        adjust_reason = AdjustReason.from_raw(data[offset + 9])

        return TimeData(
            date_time=date_time, day_of_week=day_of_week, fractions256=fractions256, adjust_reason=adjust_reason
        )

    def encode_value(self, value: TimeData, *, validate: bool = True) -> bytearray:
        """Encode time data to bytes."""
        result = bytearray()

        # Encode Date Time (7 bytes)
        if value.date_time is None:
            result.extend(bytearray(IEEE11073Parser.TIMESTAMP_LENGTH))
        else:
            result.extend(IEEE11073Parser.encode_timestamp(value.date_time))

        # Encode Day of Week (1 byte)
        day_of_week_value = int(value.day_of_week)
        if validate and day_of_week_value > self.DAY_OF_WEEK_MAX:
            raise ValueRangeError("day_of_week", day_of_week_value, 0, self.DAY_OF_WEEK_MAX)
        result.append(day_of_week_value)

        # Encode Fractions256 (1 byte)
        if validate and value.fractions256 > self.FRACTIONS256_MAX:
            raise ValueRangeError("fractions256", value.fractions256, 0, self.FRACTIONS256_MAX)
        result.append(value.fractions256)

        # Encode Adjust Reason (1 byte)
        if validate and int(value.adjust_reason) > self.ADJUST_REASON_MAX:
            raise ValueRangeError("adjust_reason", int(value.adjust_reason), 0, self.ADJUST_REASON_MAX)
        result.append(int(value.adjust_reason))

        return result


class IEEE11073FloatTemplate(CodingTemplate[float]):
    """Template for IEEE 11073 SFLOAT format (16-bit medical device float)."""

    @property
    def data_size(self) -> int:
        """Size: 2 bytes."""
        return 2

    @property
    def extractor(self) -> RawExtractor:
        """Get uint16 extractor for raw bits."""
        return UINT16

    @property
    def translator(self) -> SfloatTranslator:
        """Get SFLOAT translator."""
        return SFLOAT

    def decode_value(
        self, data: bytearray, offset: int = 0, ctx: CharacteristicContext | None = None, *, validate: bool = True
    ) -> float:
        """Parse IEEE 11073 SFLOAT format."""
        if validate and len(data) < offset + 2:
            raise InsufficientDataError("IEEE11073 SFLOAT", data[offset:], 2)
        raw = self.extractor.extract(data, offset)
        return self.translator.translate(raw)

    def encode_value(self, value: float, *, validate: bool = True) -> bytearray:
        """Encode value to IEEE 11073 SFLOAT format."""
        raw = self.translator.untranslate(value)
        return self.extractor.pack(raw)


class Float32Template(CodingTemplate[float]):
    """Template for IEEE-754 32-bit float parsing."""

    @property
    def data_size(self) -> int:
        """Size: 4 bytes."""
        return 4

    @property
    def extractor(self) -> RawExtractor:
        """Get float32 extractor."""
        return FLOAT32

    def decode_value(
        self, data: bytearray, offset: int = 0, ctx: CharacteristicContext | None = None, *, validate: bool = True
    ) -> float:
        """Parse IEEE-754 32-bit float."""
        if validate and len(data) < offset + 4:
            raise InsufficientDataError("float32", data[offset:], 4)
        return DataParser.parse_float32(data, offset)

    def encode_value(self, value: float, *, validate: bool = True) -> bytearray:
        """Encode float32 value to bytes."""
        return DataParser.encode_float32(float(value))


# =============================================================================
# STRING TEMPLATES
# =============================================================================


class Utf8StringTemplate(CodingTemplate[str]):
    """Template for UTF-8 string parsing with variable length."""

    def __init__(self, max_length: int = 256) -> None:
        """Initialize with maximum string length.

        Args:
            max_length: Maximum string length in bytes

        """
        self.max_length = max_length

    @property
    def data_size(self) -> int:
        """Size: Variable (0 to max_length)."""
        return self.max_length

    def decode_value(
        self, data: bytearray, offset: int = 0, ctx: CharacteristicContext | None = None, *, validate: bool = True
    ) -> str:
        """Parse UTF-8 string from remaining data."""
        if offset >= len(data):
            return ""

        # Take remaining data from offset
        string_data = data[offset:]

        # Remove null terminator if present
        if b"\x00" in string_data:
            null_index = string_data.index(b"\x00")
            string_data = string_data[:null_index]

        try:
            return string_data.decode("utf-8")
        except UnicodeDecodeError as e:
            if validate:
                raise ValueError(f"Invalid UTF-8 string data: {e}") from e
            return ""

    def encode_value(self, value: str, *, validate: bool = True) -> bytearray:
        """Encode string to UTF-8 bytes."""
        encoded = value.encode("utf-8")
        if validate and len(encoded) > self.max_length:
            raise ValueError(f"String too long: {len(encoded)} > {self.max_length}")
        return bytearray(encoded)


class Utf16StringTemplate(CodingTemplate[str]):
    """Template for UTF-16LE string parsing with variable length."""

    # Unicode constants for UTF-16 validation
    UNICODE_SURROGATE_START = 0xD800
    UNICODE_SURROGATE_END = 0xDFFF
    UNICODE_BOM = "\ufeff"

    def __init__(self, max_length: int = 256) -> None:
        """Initialize with maximum string length.

        Args:
            max_length: Maximum string length in bytes (must be even)

        """
        if max_length % 2 != 0:
            raise ValueError("max_length must be even for UTF-16 strings")
        self.max_length = max_length

    @property
    def data_size(self) -> int:
        """Size: Variable (0 to max_length, even bytes only)."""
        return self.max_length

    def decode_value(
        self, data: bytearray, offset: int = 0, ctx: CharacteristicContext | None = None, *, validate: bool = True
    ) -> str:
        """Parse UTF-16LE string from remaining data."""
        if offset >= len(data):
            return ""

        # Take remaining data from offset
        string_data = data[offset:]

        # Find null terminator at even positions (UTF-16 alignment)
        null_index = len(string_data)
        for i in range(0, len(string_data) - 1, 2):
            if string_data[i : i + 2] == bytearray(b"\x00\x00"):
                null_index = i
                break
        string_data = string_data[:null_index]

        # UTF-16 requires even number of bytes
        if validate and len(string_data) % 2 != 0:
            raise ValueError(f"UTF-16 data must have even byte count, got {len(string_data)}")

        try:
            decoded = string_data.decode("utf-16-le")
            # Strip BOM if present (robustness)
            if decoded.startswith(self.UNICODE_BOM):
                decoded = decoded[1:]
            # Check for invalid surrogate pairs
            if validate and any(self.UNICODE_SURROGATE_START <= ord(c) <= self.UNICODE_SURROGATE_END for c in decoded):
                raise ValueError("Invalid UTF-16LE string data: contains unpaired surrogates")
        except UnicodeDecodeError as e:
            if validate:
                raise ValueError(f"Invalid UTF-16LE string data: {e}") from e
            return ""
        else:
            return decoded

    def encode_value(self, value: str, *, validate: bool = True) -> bytearray:
        """Encode string to UTF-16LE bytes."""
        encoded = value.encode("utf-16-le")
        if validate and len(encoded) > self.max_length:
            raise ValueError(f"String too long: {len(encoded)} > {self.max_length}")
        return bytearray(encoded)


# =============================================================================
# VECTOR TEMPLATES
# =============================================================================


class VectorTemplate(CodingTemplate[VectorData]):
    """Template for 3D vector measurements (x, y, z float32 components)."""

    @property
    def data_size(self) -> int:
        """Size: 12 bytes (3 x float32)."""
        return 12

    def decode_value(
        self, data: bytearray, offset: int = 0, ctx: CharacteristicContext | None = None, *, validate: bool = True
    ) -> VectorData:
        """Parse 3D vector data."""
        if validate and len(data) < offset + 12:
            raise InsufficientDataError("3D vector", data[offset:], 12)

        x_axis = DataParser.parse_float32(data, offset)
        y_axis = DataParser.parse_float32(data, offset + 4)
        z_axis = DataParser.parse_float32(data, offset + 8)

        return VectorData(x_axis=x_axis, y_axis=y_axis, z_axis=z_axis)

    def encode_value(self, value: VectorData, *, validate: bool = True) -> bytearray:
        """Encode 3D vector data to bytes."""
        result = bytearray()
        result.extend(DataParser.encode_float32(value.x_axis))
        result.extend(DataParser.encode_float32(value.y_axis))
        result.extend(DataParser.encode_float32(value.z_axis))
        return result


class Vector2DTemplate(CodingTemplate[Vector2DData]):
    """Template for 2D vector measurements (x, y float32 components)."""

    @property
    def data_size(self) -> int:
        """Size: 8 bytes (2 x float32)."""
        return 8

    def decode_value(
        self, data: bytearray, offset: int = 0, ctx: CharacteristicContext | None = None, *, validate: bool = True
    ) -> Vector2DData:
        """Parse 2D vector data."""
        if validate and len(data) < offset + 8:
            raise InsufficientDataError("2D vector", data[offset:], 8)

        x_axis = DataParser.parse_float32(data, offset)
        y_axis = DataParser.parse_float32(data, offset + 4)

        return Vector2DData(x_axis=x_axis, y_axis=y_axis)

    def encode_value(self, value: Vector2DData, *, validate: bool = True) -> bytearray:
        """Encode 2D vector data to bytes."""
        result = bytearray()
        result.extend(DataParser.encode_float32(value.x_axis))
        result.extend(DataParser.encode_float32(value.y_axis))
        return result


# =============================================================================
# EXPORTS
# =============================================================================

__all__ = [
    # Protocol
    "CodingTemplate",
    "ConcentrationTemplate",
    # Enum template
    "EnumTemplate",
    "Float32Template",
    "IEEE11073FloatTemplate",
    # Domain-specific templates
    "PercentageTemplate",
    "PressureTemplate",
    "ScaledSint8Template",
    "ScaledSint16Template",
    "ScaledSint24Template",
    "ScaledUint8Template",
    # Scaled templates
    "ScaledUint16Template",
    "ScaledUint24Template",
    "ScaledUint32Template",
    "Sint8Template",
    "Sint16Template",
    "TemperatureTemplate",
    "TimeData",
    "TimeDataTemplate",
    # Basic integer templates
    "Uint8Template",
    "Uint16Template",
    "Uint24Template",
    "Uint32Template",
    # String templates
    "Utf8StringTemplate",
    "Utf16StringTemplate",
    "Vector2DData",
    "Vector2DTemplate",
    # Data structures
    "VectorData",
    # Vector templates
    "VectorTemplate",
]
