"""Representation of a Zigbee endpoint for zha."""

from __future__ import annotations

import asyncio
from collections.abc import Awaitable, Callable
import functools
import logging
from typing import TYPE_CHECKING, Any, Final, TypeVar

from zigpy.profiles.zha import PROFILE_ID as ZHA_PROFILE_ID
from zigpy.profiles.zll import PROFILE_ID as ZLL_PROFILE_ID

from zha.application import const
from zha.async_ import gather_with_limited_concurrency
from zha.zigbee.cluster_handlers import ClusterHandler
from zha.zigbee.cluster_handlers.const import (
    CLUSTER_HANDLER_BASIC,
    CLUSTER_HANDLER_IDENTIFY,
    CLUSTER_HANDLER_POWER_CONFIGURATION,
)
from zha.zigbee.cluster_handlers.registries import (
    CLIENT_CLUSTER_HANDLER_REGISTRY,
    CLUSTER_HANDLER_REGISTRY,
)

if TYPE_CHECKING:
    from zigpy import Endpoint as ZigpyEndpoint

    from zha.zigbee.cluster_handlers import ClientClusterHandler
    from zha.zigbee.device import Device

ATTR_DEVICE_TYPE: Final[str] = "device_type"
ATTR_PROFILE_ID: Final[str] = "profile_id"
ATTR_IN_CLUSTERS: Final[str] = "input_clusters"
ATTR_OUT_CLUSTERS: Final[str] = "output_clusters"

_LOGGER = logging.getLogger(__name__)
CALLABLE_T = TypeVar("CALLABLE_T", bound=Callable)


class Endpoint:
    """Endpoint for a zha device."""

    def __init__(self, zigpy_endpoint: ZigpyEndpoint, device: Device) -> None:
        """Initialize instance."""
        assert zigpy_endpoint is not None
        assert device is not None
        self._zigpy_endpoint: ZigpyEndpoint = zigpy_endpoint
        self._device: Device = device
        self._all_cluster_handlers: dict[str, ClusterHandler] = {}
        self._claimed_cluster_handlers: dict[str, ClusterHandler] = {}
        self._client_cluster_handlers: dict[str, ClientClusterHandler] = {}
        self._unique_id: str = f"{device.unique_id}-{zigpy_endpoint.endpoint_id}"

    def on_remove(self) -> None:
        """Run when endpoint is removed."""
        for handler in self.all_cluster_handlers.values():
            handler.on_remove()

        self.all_cluster_handlers.clear()

        for handler in self.client_cluster_handlers.values():
            handler.on_remove()

        self.client_cluster_handlers.clear()

    @functools.cached_property
    def device(self) -> Device:
        """Return the device this endpoint belongs to."""
        return self._device

    @property
    def all_cluster_handlers(self) -> dict[str, ClusterHandler]:
        """All server cluster handlers of an endpoint."""
        return self._all_cluster_handlers

    @property
    def claimed_cluster_handlers(self) -> dict[str, ClusterHandler]:
        """Cluster handlers in use."""
        return self._claimed_cluster_handlers

    @property
    def client_cluster_handlers(self) -> dict[str, ClientClusterHandler]:
        """Return a dict of client cluster handlers."""
        return self._client_cluster_handlers

    @functools.cached_property
    def zigpy_endpoint(self) -> ZigpyEndpoint:
        """Return endpoint of zigpy device."""
        return self._zigpy_endpoint

    @functools.cached_property
    def id(self) -> int:
        """Return endpoint id."""
        return self._zigpy_endpoint.endpoint_id

    @functools.cached_property
    def unique_id(self) -> str:
        """Return the unique id for this endpoint."""
        return self._unique_id

    @property
    def zigbee_signature(self) -> tuple[int, dict[str, Any]]:
        """Get the zigbee signature for the endpoint this pool represents."""
        return (
            self.id,
            {
                ATTR_PROFILE_ID: f"0x{self._zigpy_endpoint.profile_id:04x}"
                if self._zigpy_endpoint.profile_id is not None
                else "",
                ATTR_DEVICE_TYPE: f"0x{self._zigpy_endpoint.device_type:04x}"
                if self._zigpy_endpoint.device_type is not None
                else "",
                ATTR_IN_CLUSTERS: [
                    f"0x{cluster_id:04x}"
                    for cluster_id in sorted(self._zigpy_endpoint.in_clusters)
                ],
                ATTR_OUT_CLUSTERS: [
                    f"0x{cluster_id:04x}"
                    for cluster_id in sorted(self._zigpy_endpoint.out_clusters)
                ],
            },
        )

    @classmethod
    def new(cls, zigpy_endpoint: ZigpyEndpoint, device: Device) -> Endpoint:
        """Create new endpoint and populate cluster handlers."""
        endpoint = cls(zigpy_endpoint, device)
        endpoint.add_all_cluster_handlers()
        endpoint.add_client_cluster_handlers()

        return endpoint

    def add_all_cluster_handlers(self) -> None:
        """Create and add cluster handlers for all input clusters."""
        profile_id = self._zigpy_endpoint.profile_id
        if profile_id is None:
            _LOGGER.debug("Skipping endpoint, profile is None")
            return
        elif profile_id not in (ZLL_PROFILE_ID, ZHA_PROFILE_ID):
            _LOGGER.debug(
                "Skipping endpoint, profile is not ZLL or ZHA: 0x%04X",
                profile_id,
            )
            return

        for cluster_id, cluster in self.zigpy_endpoint.in_clusters.items():
            cluster_handler_classes = CLUSTER_HANDLER_REGISTRY.get(
                cluster_id, {None: ClusterHandler}
            )
            quirk_id = (
                self.device.quirk_id
                if self.device.quirk_id in cluster_handler_classes
                else None
            )
            cluster_handler_class = cluster_handler_classes.get(
                quirk_id, ClusterHandler
            )

            # Allow cluster handler to filter out bad matches
            if not cluster_handler_class.matches(cluster, self):
                cluster_handler_class = ClusterHandler

            _LOGGER.debug(
                "Creating cluster handler for cluster id: %s class: %s",
                cluster_id,
                cluster_handler_class,
            )

            try:
                cluster_handler = cluster_handler_class(cluster, self)
            except KeyError as err:
                _LOGGER.warning(
                    "Cluster handler %s for cluster %s on endpoint %s is invalid: %s",
                    cluster_handler_class,
                    cluster,
                    self,
                    err,
                )
                continue

            if cluster_handler.name == CLUSTER_HANDLER_POWER_CONFIGURATION:
                self._device.power_configuration_ch = cluster_handler
            elif cluster_handler.name == CLUSTER_HANDLER_IDENTIFY:
                self._device.identify_ch = cluster_handler
            elif cluster_handler.name == CLUSTER_HANDLER_BASIC:
                self._device.basic_ch = cluster_handler

            self._all_cluster_handlers[cluster_handler.id] = cluster_handler
            cluster_handler.on_add()

    def add_client_cluster_handlers(self) -> None:
        """Create client cluster handlers for all output clusters if in the registry."""
        for (
            cluster_id,
            cluster_handler_class,
        ) in CLIENT_CLUSTER_HANDLER_REGISTRY.items():
            cluster = self.zigpy_endpoint.out_clusters.get(cluster_id)
            if cluster is not None:
                _LOGGER.debug(
                    "Creating client cluster handler for cluster id: %s class: %s",
                    cluster_id,
                    cluster_handler_class,
                )
                cluster_handler = cluster_handler_class(cluster, self)
                self.client_cluster_handlers[cluster_handler.id] = cluster_handler
                cluster_handler.on_add()

    async def async_initialize(self, from_cache: bool = False) -> None:
        """Initialize claimed cluster handlers."""
        await self._execute_handler_tasks(
            "async_initialize", from_cache, max_concurrency=1
        )

    async def async_configure(self) -> None:
        """Configure claimed cluster handlers."""
        await self._execute_handler_tasks("async_configure")

    async def _execute_handler_tasks(
        self, func_name: str, *args: Any, max_concurrency: int | None = None
    ) -> None:
        """Add a throttled cluster handler task and swallow exceptions."""
        cluster_handlers = [
            *self.claimed_cluster_handlers.values(),
            *self.client_cluster_handlers.values(),
        ]
        tasks = [getattr(ch, func_name)(*args) for ch in cluster_handlers]

        gather: Callable[..., Awaitable]

        if max_concurrency is None:
            gather = asyncio.gather
        else:
            gather = functools.partial(gather_with_limited_concurrency, max_concurrency)

        results = await gather(*tasks, return_exceptions=True)
        for cluster_handler, outcome in zip(cluster_handlers, results):
            if isinstance(outcome, Exception):
                cluster_handler.debug(
                    "'%s' stage failed: %s", func_name, str(outcome), exc_info=outcome
                )
            else:
                cluster_handler.debug("'%s' stage succeeded", func_name)

    def emit_zha_event(self, event_data: dict[str, Any]) -> None:
        """Broadcast an event from this endpoint."""
        self.device.emit_zha_event(
            {
                const.ATTR_UNIQUE_ID: self.unique_id,
                const.ATTR_ENDPOINT_ID: self.id,
                **event_data,
            }
        )

    def claim_cluster_handlers(self, cluster_handlers: list[ClusterHandler]) -> None:
        """Claim cluster handlers."""
        self.claimed_cluster_handlers.update({ch.id: ch for ch in cluster_handlers})

    def unclaimed_cluster_handlers(self) -> list[ClusterHandler]:
        """Return a list of available (unclaimed) cluster handlers."""
        claimed = set(self.claimed_cluster_handlers)
        available = set(self.all_cluster_handlers)
        return [
            self.all_cluster_handlers[cluster_id]
            for cluster_id in (available - claimed)
        ]
