#!/usr/bin/env python3
"""RAMSES RF - Base class for all RAMSES-II objects: devices and constructs."""

from __future__ import annotations

import asyncio
import logging
from inspect import getmembers, isclass
from sys import modules
from types import ModuleType
from typing import TYPE_CHECKING, Any, cast

from ramses_tx import Priority, QosParams
from ramses_tx.address import ALL_DEVICE_ID
from ramses_tx.const import RQ, Code

from .discovery import DiscoveryService
from .state_store import StateStore

if TYPE_CHECKING:
    from ramses_tx import Command, Message, Packet
    from ramses_tx.typing import DeviceIdT, DevIndexT

    from .device import Controller
    from .gateway import Gateway
    from .interfaces import DeviceInterface
    from .system import Evohome


_QOS_TX_LIMIT = 12
_ID_SLICE = 9

_LOGGER = logging.getLogger(__name__)


def class_by_attr(name: str, attr: str) -> dict[str, Any]:
    """Return a mapping of a (unique) attr of classes in a module to that class.

    :param name: The module name to inspect.
    :type name: str
    :param attr: The attribute name to use as a key.
    :type attr: str
    :returns: A dictionary mapping attribute values to classes.
    :rtype: dict[str, Any]
    """

    def predicate(m: ModuleType) -> bool:
        return isclass(m) and m.__module__ == name and getattr(m, attr, None)

    return {getattr(c[1], attr): c[1] for c in getmembers(modules[name], predicate)}


class _Entity:
    """The ultimate base class for Devices/Zones/Systems.

    This class is primarily a coordinator that initializes the entity's identity
    and composes the specialized services for state management and discovery.
    """

    _SLUG: str = None  # type: ignore[assignment]

    def __init__(self, gwy: Gateway) -> None:
        """Initialize the base entity and its composed components.

        :param gwy: The gateway orchestrator.
        :type gwy: Gateway
        """
        self._gwy = gwy
        self.id: DeviceIdT = None  # type: ignore[assignment]
        self._qos_tx_count = 0

        # Specialized components via Composition
        self.state_store: StateStore = StateStore(
            cast("DeviceInterface", self), self._gwy
        )
        self.discovery: DiscoveryService = DiscoveryService(self, self._gwy)

        # Context required by children (Zones/Devices)
        self._z_id: DeviceIdT = None  # type: ignore[assignment]
        self._z_idx: DevIndexT | None = None
        self.ctl: Controller = None  # type: ignore[assignment]
        self.tcs: Evohome = None  # type: ignore[assignment]

    def __repr__(self) -> str:
        return f"{self.id} ({self._SLUG})"

    def deprecate_device(self, pkt: Packet, reset: bool = False) -> None:
        """If an entity is deprecated enough times, stop sending to it.

        :param pkt: The packet triggering deprecation.
        :type pkt: Packet
        :param reset: If True, reset the deprecation counter, defaults to False.
        :type reset: bool, optional
        """
        if reset:
            self._qos_tx_count = 0
            return

        self._qos_tx_count += 1
        if self._qos_tx_count == _QOS_TX_LIMIT:
            _LOGGER.warning(
                f"{pkt} < Sending now deprecated for {self} "
                "(consider adjusting device_id filters)"
            )

    def _handle_msg(self, msg: Message) -> None:
        """Delegate message handling to the StateStore.

        :param msg: The message to process.
        :type msg: Message
        """
        if not (
            msg.src.id == self.id[:_ID_SLICE]
            or (msg.dst.id == self.id[:_ID_SLICE] and msg.verb != RQ)
            or (msg.dst.id == ALL_DEVICE_ID and msg.code == Code._1FC9)
        ):
            return

        # Direct the message to the StateStore for persistence and indexing
        self.state_store._handle_msg(msg)

    def _send_cmd(self, cmd: Command, **kwargs: Any) -> asyncio.Task | None:
        """Proxy command sending to the Gateway.

        :param cmd: The command to send.
        :type cmd: Command
        :param kwargs: Optional sending parameters (e.g., priority).
        :type kwargs: Any
        :returns: The corresponding asyncio Task or None.
        :rtype: asyncio.Task | None
        """
        if self._qos_tx_count > _QOS_TX_LIMIT:
            _LOGGER.info(f"{cmd} < Sending was deprecated for {self}")
            return None

        return self._gwy.send_cmd(cmd, wait_for_reply=False, **kwargs)

    async def _async_send_cmd(
        self,
        cmd: Command,
        priority: Priority | None = None,
        qos: QosParams | None = None,
    ) -> Packet | None:
        """Proxy asynchronous command sending to the Gateway.

        :param cmd: The command to send.
        :type cmd: Command
        :param priority: Transmission priority, defaults to None.
        :type priority: Priority | None, optional
        :param qos: Quality of Service parameters, defaults to None.
        :type qos: QosParams | None, optional
        :returns: The response or echo packet.
        :rtype: Packet | None
        """
        if self._qos_tx_count > _QOS_TX_LIMIT:
            _LOGGER.warning(f"{cmd} < Sending was deprecated for {self}")
            return None

        return await self._gwy.async_send_cmd(
            cmd,
            max_retries=qos.max_retries if qos else None,
            priority=priority,
            timeout=qos.timeout if qos else None,
            wait_for_reply=qos.wait_for_reply if qos else None,
        )


class Entity(_Entity):
    """The base class for Devices/Zones/Systems."""
