"""Matter entity base class."""
from __future__ import annotations

from abc import abstractmethod
from collections.abc import Callable
from dataclasses import dataclass
import logging
from typing import TYPE_CHECKING, Any, cast

from matter_server.common.models.device_type_instance import MatterDeviceTypeInstance
from matter_server.common.models.events import EventType
from matter_server.common.models.node_device import AbstractMatterNodeDevice
from matter_server.common.models.server_information import ServerInfo

from homeassistant.core import callback
from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription

from .const import DOMAIN, ID_TYPE_DEVICE_ID
from .helpers import get_device_id, get_operational_instance_id

if TYPE_CHECKING:
    from matter_server.client import MatterClient
    from matter_server.common.models.node import MatterAttribute

LOGGER = logging.getLogger(__name__)


@dataclass
class MatterEntityDescription:
    """Mixin to map a matter device to a Home Assistant entity."""

    entity_cls: type[MatterEntity]
    subscribe_attributes: tuple


@dataclass
class MatterEntityDescriptionBaseClass(EntityDescription, MatterEntityDescription):
    """For typing a base class that inherits from both entity descriptions."""


class MatterEntity(Entity):
    """Entity class for Matter devices."""

    entity_description: MatterEntityDescriptionBaseClass
    _attr_should_poll = False
    _attr_has_entity_name = True

    def __init__(
        self,
        matter_client: MatterClient,
        node_device: AbstractMatterNodeDevice,
        device_type_instance: MatterDeviceTypeInstance,
        entity_description: MatterEntityDescriptionBaseClass,
    ) -> None:
        """Initialize the entity."""
        self.matter_client = matter_client
        self._node_device = node_device
        self._device_type_instance = device_type_instance
        self.entity_description = entity_description
        self._unsubscribes: list[Callable] = []
        # for fast lookups we create a mapping to the attribute paths
        # The server info is set when the client connects to the server.
        self._attributes_map: dict[type, str] = {}
        server_info = cast(ServerInfo, self.matter_client.server_info)
        # create unique_id based on "Operational Instance Name" and endpoint/device type
        self._attr_unique_id = (
            f"{get_operational_instance_id(server_info, self._node_device.node())}-"
            f"{device_type_instance.endpoint}-"
            f"{device_type_instance.device_type.device_type}"
        )
        node_device_id = get_device_id(server_info, node_device)
        self._attr_device_info = DeviceInfo(
            identifiers={(DOMAIN, f"{ID_TYPE_DEVICE_ID}_{node_device_id}")}
        )

    async def async_added_to_hass(self) -> None:
        """Handle being added to Home Assistant."""
        await super().async_added_to_hass()

        # Subscribe to attribute updates.
        for attr_cls in self.entity_description.subscribe_attributes:
            if matter_attr := self.get_matter_attribute(attr_cls):
                self._attributes_map[attr_cls] = matter_attr.path
                self._unsubscribes.append(
                    self.matter_client.subscribe(
                        self._on_matter_event,
                        EventType.ATTRIBUTE_UPDATED,
                        self._device_type_instance.node.node_id,
                        matter_attr.path,
                    )
                )
                continue
            # not sure if this can happen, but just in case log it.
            LOGGER.warning("Attribute not found on device: %s", attr_cls)

        # make sure to update the attributes once
        self._update_from_device()

    async def async_will_remove_from_hass(self) -> None:
        """Run when entity will be removed from hass."""
        for unsub in self._unsubscribes:
            unsub()

    @callback
    def _on_matter_event(self, event: EventType, data: Any = None) -> None:
        """Call on update."""
        self._update_from_device()
        self.async_write_ha_state()

    @callback
    @abstractmethod
    def _update_from_device(self) -> None:
        """Update data from Matter device."""

    @callback
    def get_matter_attribute(self, attribute: type) -> MatterAttribute | None:
        """Lookup MatterAttribute on device by providing the attribute class."""
        return next(
            (
                x
                for x in self._device_type_instance.attributes
                if x.attribute_type == attribute
            ),
            None,
        )