"""Support for Hue groups (room/zone)."""
from __future__ import annotations

import asyncio
from typing import Any

from aiohue.v2 import HueBridgeV2
from aiohue.v2.controllers.events import EventType
from aiohue.v2.controllers.groups import GroupedLight, Room, Zone
from aiohue.v2.models.feature import DynamicStatus

from homeassistant.components.light import (
    ATTR_BRIGHTNESS,
    ATTR_COLOR_TEMP,
    ATTR_FLASH,
    ATTR_TRANSITION,
    ATTR_XY_COLOR,
    COLOR_MODE_BRIGHTNESS,
    COLOR_MODE_COLOR_TEMP,
    COLOR_MODE_ONOFF,
    COLOR_MODE_XY,
    FLASH_SHORT,
    SUPPORT_FLASH,
    SUPPORT_TRANSITION,
    LightEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from ..bridge import HueBridge
from ..const import CONF_ALLOW_HUE_GROUPS, DOMAIN
from .entity import HueBaseEntity
from .helpers import (
    normalize_hue_brightness,
    normalize_hue_colortemp,
    normalize_hue_transition,
)


async def async_setup_entry(
    hass: HomeAssistant,
    config_entry: ConfigEntry,
    async_add_entities: AddEntitiesCallback,
) -> None:
    """Set up Hue groups on light platform."""
    bridge: HueBridge = hass.data[DOMAIN][config_entry.entry_id]
    api: HueBridgeV2 = bridge.api

    # to prevent race conditions (groupedlight is created before zone/room)
    # we create groupedlights from the room/zone and actually use the
    # underlying grouped_light resource for control

    @callback
    def async_add_light(event_type: EventType, resource: Room | Zone) -> None:
        """Add Grouped Light for Hue Room/Zone."""
        if grouped_light_id := resource.grouped_light:
            grouped_light = api.groups.grouped_light[grouped_light_id]
            light = GroupedHueLight(bridge, grouped_light, resource)
            async_add_entities([light])

    # add current items
    for item in api.groups.room.items + api.groups.zone.items:
        async_add_light(EventType.RESOURCE_ADDED, item)

    # register listener for new zones/rooms
    config_entry.async_on_unload(
        api.groups.room.subscribe(
            async_add_light, event_filter=EventType.RESOURCE_ADDED
        )
    )
    config_entry.async_on_unload(
        api.groups.zone.subscribe(
            async_add_light, event_filter=EventType.RESOURCE_ADDED
        )
    )


class GroupedHueLight(HueBaseEntity, LightEntity):
    """Representation of a Grouped Hue light."""

    _attr_icon = "mdi:lightbulb-group"

    def __init__(
        self, bridge: HueBridge, resource: GroupedLight, group: Room | Zone
    ) -> None:
        """Initialize the light."""
        controller = bridge.api.groups.grouped_light
        super().__init__(bridge, controller, resource)
        self.resource = resource
        self.group = group
        self.controller = controller
        self.api: HueBridgeV2 = bridge.api
        self._attr_supported_features |= SUPPORT_FLASH
        self._attr_supported_features |= SUPPORT_TRANSITION

        # Entities for Hue groups are disabled by default
        # unless they were enabled in old version (legacy option)
        self._attr_entity_registry_enabled_default = bridge.config_entry.options.get(
            CONF_ALLOW_HUE_GROUPS, False
        )
        self._dynamic_mode_active = False
        self._update_values()

    async def async_added_to_hass(self) -> None:
        """Call when entity is added."""
        await super().async_added_to_hass()

        # subscribe to group updates
        self.async_on_remove(
            self.api.groups.subscribe(self._handle_event, self.group.id)
        )
        # We need to watch the underlying lights too
        # if we want feedback about color/brightness changes
        if self._attr_supported_color_modes:
            light_ids = tuple(
                x.id for x in self.controller.get_lights(self.resource.id)
            )
            self.async_on_remove(
                self.api.lights.subscribe(self._handle_event, light_ids)
            )

    @property
    def name(self) -> str:
        """Return name of room/zone for this grouped light."""
        return self.group.metadata.name

    @property
    def is_on(self) -> bool:
        """Return true if light is on."""
        return self.resource.on.on

    @property
    def extra_state_attributes(self) -> dict[str, Any] | None:
        """Return the optional state attributes."""
        scenes = {
            x.metadata.name for x in self.api.scenes if x.group.rid == self.group.id
        }
        lights = {x.metadata.name for x in self.controller.get_lights(self.resource.id)}
        return {
            "is_hue_group": True,
            "hue_scenes": scenes,
            "hue_type": self.group.type.value,
            "lights": lights,
            "dynamics": self._dynamic_mode_active,
        }

    async def async_turn_on(self, **kwargs: Any) -> None:
        """Turn the light on."""
        transition = normalize_hue_transition(kwargs.get(ATTR_TRANSITION))
        xy_color = kwargs.get(ATTR_XY_COLOR)
        color_temp = normalize_hue_colortemp(kwargs.get(ATTR_COLOR_TEMP))
        brightness = normalize_hue_brightness(kwargs.get(ATTR_BRIGHTNESS))
        flash = kwargs.get(ATTR_FLASH)

        if flash is not None:
            await self.async_set_flash(flash)
            # flash can not be sent with other commands at the same time
            return

        # NOTE: a grouped_light can only handle turn on/off
        # To set other features, you'll have to control the attached lights
        if (
            brightness is None
            and xy_color is None
            and color_temp is None
            and transition is None
            and flash is None
        ):
            await self.bridge.async_request_call(
                self.controller.set_state, id=self.resource.id, on=True
            )
            return

        # redirect all other feature commands to underlying lights
        # note that this silently ignores params sent to light that are not supported
        await asyncio.gather(
            *[
                self.bridge.async_request_call(
                    self.api.lights.set_state,
                    light.id,
                    on=True,
                    brightness=brightness if light.supports_dimming else None,
                    color_xy=xy_color if light.supports_color else None,
                    color_temp=color_temp if light.supports_color_temperature else None,
                    transition_time=transition,
                )
                for light in self.controller.get_lights(self.resource.id)
            ]
        )

    async def async_turn_off(self, **kwargs: Any) -> None:
        """Turn the light off."""
        transition = normalize_hue_transition(kwargs.get(ATTR_TRANSITION))
        flash = kwargs.get(ATTR_FLASH)

        if flash is not None:
            await self.async_set_flash(flash)
            # flash can not be sent with other commands at the same time
            return

        # NOTE: a grouped_light can only handle turn on/off
        # To set other features, you'll have to control the attached lights
        if transition is None:
            await self.bridge.async_request_call(
                self.controller.set_state, id=self.resource.id, on=False
            )
            return

        # redirect all other feature commands to underlying lights
        await asyncio.gather(
            *[
                self.bridge.async_request_call(
                    self.api.lights.set_state,
                    light.id,
                    on=False,
                    transition_time=transition,
                )
                for light in self.controller.get_lights(self.resource.id)
            ]
        )

    async def async_set_flash(self, flash: str) -> None:
        """Send flash command to light."""
        await asyncio.gather(
            *[
                self.bridge.async_request_call(
                    self.api.lights.set_flash,
                    id=light.id,
                    short=flash == FLASH_SHORT,
                )
                for light in self.controller.get_lights(self.resource.id)
            ]
        )

    @callback
    def on_update(self) -> None:
        """Call on update event."""
        self._update_values()

    @callback
    def _update_values(self) -> None:
        """Set base values from underlying lights of a group."""
        supported_color_modes = set()
        lights_with_color_support = 0
        lights_with_color_temp_support = 0
        lights_with_dimming_support = 0
        total_brightness = 0
        all_lights = self.controller.get_lights(self.resource.id)
        lights_in_colortemp_mode = 0
        lights_in_dynamic_mode = 0
        # loop through all lights to find capabilities
        for light in all_lights:
            if color_temp := light.color_temperature:
                lights_with_color_temp_support += 1
                # we assume mired values from the first capable light
                self._attr_color_temp = color_temp.mirek
                self._attr_max_mireds = color_temp.mirek_schema.mirek_maximum
                self._attr_min_mireds = color_temp.mirek_schema.mirek_minimum
                if color_temp.mirek is not None and color_temp.mirek_valid:
                    lights_in_colortemp_mode += 1
            if color := light.color:
                lights_with_color_support += 1
                # we assume xy values from the first capable light
                self._attr_xy_color = (color.xy.x, color.xy.y)
            if dimming := light.dimming:
                lights_with_dimming_support += 1
                total_brightness += dimming.brightness
            if (
                light.dynamics
                and light.dynamics.status == DynamicStatus.DYNAMIC_PALETTE
            ):
                lights_in_dynamic_mode += 1

        # this is a bit hacky because light groups may contain lights
        # of different capabilities. We set a colormode as supported
        # if any of the lights support it
        # this means that the state is derived from only some of the lights
        # and will never be 100% accurate but it will be close
        if lights_with_color_support > 0:
            supported_color_modes.add(COLOR_MODE_XY)
        if lights_with_color_temp_support > 0:
            supported_color_modes.add(COLOR_MODE_COLOR_TEMP)
        if lights_with_dimming_support > 0:
            if len(supported_color_modes) == 0:
                # only add color mode brightness if no color variants
                supported_color_modes.add(COLOR_MODE_BRIGHTNESS)
            self._attr_brightness = round(
                ((total_brightness / lights_with_dimming_support) / 100) * 255
            )
        else:
            supported_color_modes.add(COLOR_MODE_ONOFF)
        self._dynamic_mode_active = lights_in_dynamic_mode > 0
        self._attr_supported_color_modes = supported_color_modes
        # pick a winner for the current colormode
        if (
            lights_with_color_temp_support > 0
            and lights_in_colortemp_mode == lights_with_color_temp_support
        ):
            self._attr_color_mode = COLOR_MODE_COLOR_TEMP
        elif lights_with_color_support > 0:
            self._attr_color_mode = COLOR_MODE_XY
        elif lights_with_dimming_support > 0:
            self._attr_color_mode = COLOR_MODE_BRIGHTNESS
        else:
            self._attr_color_mode = COLOR_MODE_ONOFF
