"""Lights on Zigbee Home Automation networks."""
from __future__ import annotations

import asyncio
from collections import Counter
from collections.abc import Callable
from datetime import timedelta
import functools
import itertools
import logging
import random
from typing import TYPE_CHECKING, Any, cast

from zigpy.zcl.clusters.general import Identify, LevelControl, OnOff
from zigpy.zcl.clusters.lighting import Color
from zigpy.zcl.foundation import Status

from homeassistant.components import light
from homeassistant.components.light import (
    ColorMode,
    brightness_supported,
    filter_supported_color_modes,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
    ATTR_SUPPORTED_FEATURES,
    STATE_ON,
    STATE_UNAVAILABLE,
    Platform,
)
from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, State, callback
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.dispatcher import (
    async_dispatcher_connect,
    async_dispatcher_send,
)
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_call_later, async_track_time_interval

from .core import discovery, helpers
from .core.const import (
    CHANNEL_COLOR,
    CHANNEL_LEVEL,
    CHANNEL_ON_OFF,
    CONF_ALWAYS_PREFER_XY_COLOR_MODE,
    CONF_DEFAULT_LIGHT_TRANSITION,
    CONF_ENABLE_ENHANCED_LIGHT_TRANSITION,
    CONF_ENABLE_LIGHT_TRANSITIONING_FLAG,
    DATA_ZHA,
    SIGNAL_ADD_ENTITIES,
    SIGNAL_ATTR_UPDATED,
    SIGNAL_SET_LEVEL,
    ZHA_OPTIONS,
)
from .core.helpers import LogMixin, async_get_zha_config_value
from .core.registries import ZHA_ENTITIES
from .entity import ZhaEntity, ZhaGroupEntity

if TYPE_CHECKING:
    from .core.device import ZHADevice

_LOGGER = logging.getLogger(__name__)

DEFAULT_ON_OFF_TRANSITION = 1  # most bulbs default to a 1-second turn on/off transition
DEFAULT_EXTRA_TRANSITION_DELAY_SHORT = 0.25
DEFAULT_EXTRA_TRANSITION_DELAY_LONG = 2.0
DEFAULT_LONG_TRANSITION_TIME = 10
DEFAULT_MIN_BRIGHTNESS = 2

FLASH_EFFECTS = {
    light.FLASH_SHORT: Identify.EffectIdentifier.Blink,
    light.FLASH_LONG: Identify.EffectIdentifier.Breathe,
}

STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, Platform.LIGHT)
GROUP_MATCH = functools.partial(ZHA_ENTITIES.group_match, Platform.LIGHT)
PARALLEL_UPDATES = 0
SIGNAL_LIGHT_GROUP_STATE_CHANGED = "zha_light_group_state_changed"
SIGNAL_LIGHT_GROUP_TRANSITION_START = "zha_light_group_transition_start"
SIGNAL_LIGHT_GROUP_TRANSITION_FINISHED = "zha_light_group_transition_finished"
DEFAULT_MIN_TRANSITION_MANUFACTURERS = {"sengled"}

COLOR_MODES_GROUP_LIGHT = {ColorMode.COLOR_TEMP, ColorMode.XY}
SUPPORT_GROUP_LIGHT = (
    light.LightEntityFeature.EFFECT
    | light.LightEntityFeature.FLASH
    | light.LightEntityFeature.TRANSITION
)


async def async_setup_entry(
    hass: HomeAssistant,
    config_entry: ConfigEntry,
    async_add_entities: AddEntitiesCallback,
) -> None:
    """Set up the Zigbee Home Automation light from config entry."""
    entities_to_create = hass.data[DATA_ZHA][Platform.LIGHT]

    unsub = async_dispatcher_connect(
        hass,
        SIGNAL_ADD_ENTITIES,
        functools.partial(
            discovery.async_add_entities, async_add_entities, entities_to_create
        ),
    )
    config_entry.async_on_unload(unsub)


class BaseLight(LogMixin, light.LightEntity):
    """Operations common to all light entities."""

    _FORCE_ON = False
    _DEFAULT_MIN_TRANSITION_TIME = 0

    def __init__(self, *args, **kwargs):
        """Initialize the light."""
        self._zha_device: ZHADevice = None
        super().__init__(*args, **kwargs)
        self._attr_min_mireds: int | None = 153
        self._attr_max_mireds: int | None = 500
        self._attr_color_mode = ColorMode.UNKNOWN  # Set by sub classes
        self._attr_supported_features: int = 0
        self._attr_state: bool | None
        self._off_with_transition: bool = False
        self._off_brightness: int | None = None
        self._zha_config_transition = self._DEFAULT_MIN_TRANSITION_TIME
        self._zha_config_enhanced_light_transition: bool = False
        self._zha_config_enable_light_transitioning_flag: bool = True
        self._zha_config_always_prefer_xy_color_mode: bool = True
        self._on_off_channel = None
        self._level_channel = None
        self._color_channel = None
        self._identify_channel = None
        self._transitioning: bool = False
        self._transition_listener: Callable[[], None] | None = None

    @property
    def extra_state_attributes(self) -> dict[str, Any]:
        """Return state attributes."""
        attributes = {
            "off_with_transition": self._off_with_transition,
            "off_brightness": self._off_brightness,
        }
        return attributes

    @property
    def is_on(self) -> bool:
        """Return true if entity is on."""
        if self._attr_state is None:
            return False
        return self._attr_state

    @callback
    def set_level(self, value: int) -> None:
        """Set the brightness of this light between 0..254.

        brightness level 255 is a special value instructing the device to come
        on at `on_level` Zigbee attribute value, regardless of the last set
        level
        """
        if self._transitioning:
            self.debug(
                "received level %s while transitioning - skipping update",
                value,
            )
            return
        value = max(0, min(254, value))
        self._attr_brightness = value
        self.async_write_ha_state()

    async def async_turn_on(self, **kwargs: Any) -> None:
        """Turn the entity on."""
        transition = kwargs.get(light.ATTR_TRANSITION)
        duration = (
            transition * 10
            if transition is not None
            else self._zha_config_transition * 10
        ) or self._DEFAULT_MIN_TRANSITION_TIME  # if 0 is passed in some devices still need the minimum default
        brightness = kwargs.get(light.ATTR_BRIGHTNESS)
        effect = kwargs.get(light.ATTR_EFFECT)
        flash = kwargs.get(light.ATTR_FLASH)
        temperature = kwargs.get(light.ATTR_COLOR_TEMP)
        xy_color = kwargs.get(light.ATTR_XY_COLOR)
        hs_color = kwargs.get(light.ATTR_HS_COLOR)

        set_transition_flag = (
            brightness_supported(self._attr_supported_color_modes)
            or temperature is not None
            or xy_color is not None
            or hs_color is not None
        ) and self._zha_config_enable_light_transitioning_flag
        transition_time = (
            (
                duration / 10 + DEFAULT_EXTRA_TRANSITION_DELAY_SHORT
                if (
                    (brightness is not None or transition is not None)
                    and brightness_supported(self._attr_supported_color_modes)
                    or (self._off_with_transition and self._off_brightness is not None)
                    or temperature is not None
                    or xy_color is not None
                    or hs_color is not None
                )
                else DEFAULT_ON_OFF_TRANSITION + DEFAULT_EXTRA_TRANSITION_DELAY_SHORT
            )
            if set_transition_flag
            else 0
        )

        # If we need to pause attribute report parsing, we'll do so here.
        # After successful calls, we later start a timer to unset the flag after transition_time.
        # On an error on the first move to level call, we unset the flag immediately if no previous timer is running.
        # On an error on subsequent calls, we start the transition timer, as a brightness call might have come through.
        if set_transition_flag:
            self.async_transition_set_flag()

        # If the light is currently off but a turn_on call with a color/temperature is sent,
        # the light needs to be turned on first at a low brightness level where the light is immediately transitioned
        # to the correct color. Afterwards, the transition is only from the low brightness to the new brightness.
        # Otherwise, the transition is from the color the light had before being turned on to the new color.
        # This can look especially bad with transitions longer than a second. We do not want to do this for
        # devices that need to be forced to use the on command because we would end up with 4 commands sent:
        # move to level, on, color, move to level... We also will not set this if the bulb is already in the
        # desired color mode with the desired color or color temperature.
        new_color_provided_while_off = (
            self._zha_config_enhanced_light_transition
            and not self._FORCE_ON
            and not self._attr_state
            and (
                (
                    temperature is not None
                    and (
                        self._attr_color_temp != temperature
                        or self._attr_color_mode != ColorMode.COLOR_TEMP
                    )
                )
                or (
                    xy_color is not None
                    and (
                        self._attr_xy_color != xy_color
                        or self._attr_color_mode != ColorMode.XY
                    )
                )
                or (
                    hs_color is not None
                    and (
                        self._attr_hs_color != hs_color
                        or self._attr_color_mode != ColorMode.HS
                    )
                )
            )
            and brightness_supported(self._attr_supported_color_modes)
        )

        if (
            brightness is None
            and (self._off_with_transition or new_color_provided_while_off)
            and self._off_brightness is not None
        ):
            brightness = self._off_brightness

        if brightness is not None:
            level = min(254, brightness)
        else:
            level = self._attr_brightness or 254

        t_log = {}

        if new_color_provided_while_off:
            # If the light is currently off, we first need to turn it on at a low brightness level with no transition.
            # After that, we set it to the desired color/temperature with no transition.
            result = await self._level_channel.move_to_level_with_on_off(
                level=DEFAULT_MIN_BRIGHTNESS,
                transition_time=self._DEFAULT_MIN_TRANSITION_TIME,
            )
            t_log["move_to_level_with_on_off"] = result
            if isinstance(result, Exception) or result[1] is not Status.SUCCESS:
                # First 'move to level' call failed, so if the transitioning delay isn't running from a previous call,
                # the flag can be unset immediately
                if set_transition_flag and not self._transition_listener:
                    self.async_transition_complete()
                self.debug("turned on: %s", t_log)
                return
            # Currently only setting it to "on", as the correct level state will be set at the second move_to_level call
            self._attr_state = True

        if (
            (brightness is not None or transition)
            and not new_color_provided_while_off
            and brightness_supported(self._attr_supported_color_modes)
        ):
            result = await self._level_channel.move_to_level_with_on_off(
                level=level,
                transition_time=duration,
            )
            t_log["move_to_level_with_on_off"] = result
            if isinstance(result, Exception) or result[1] is not Status.SUCCESS:
                # First 'move to level' call failed, so if the transitioning delay isn't running from a previous call,
                # the flag can be unset immediately
                if set_transition_flag and not self._transition_listener:
                    self.async_transition_complete()
                self.debug("turned on: %s", t_log)
                return
            self._attr_state = bool(level)
            if level:
                self._attr_brightness = level

        if (
            brightness is None
            and not new_color_provided_while_off
            or (self._FORCE_ON and brightness)
        ):
            # since some lights don't always turn on with move_to_level_with_on_off,
            # we should call the on command on the on_off cluster if brightness is not 0.
            result = await self._on_off_channel.on()
            t_log["on_off"] = result
            if isinstance(result, Exception) or result[1] is not Status.SUCCESS:
                # 'On' call failed, but as brightness may still transition (for FORCE_ON lights),
                # we start the timer to unset the flag after the transition_time if necessary.
                self.async_transition_start_timer(transition_time)
                self.debug("turned on: %s", t_log)
                return
            self._attr_state = True

        if not await self.async_handle_color_commands(
            temperature,
            duration,
            hs_color,
            xy_color,
            new_color_provided_while_off,
            t_log,
        ):
            # Color calls failed, but as brightness may still transition, we start the timer to unset the flag
            self.async_transition_start_timer(transition_time)
            self.debug("turned on: %s", t_log)
            return

        if new_color_provided_while_off:
            # The light is has the correct color, so we can now transition it to the correct brightness level.
            result = await self._level_channel.move_to_level(
                level=level, transition_time=duration
            )
            t_log["move_to_level_if_color"] = result
            if isinstance(result, Exception) or result[1] is not Status.SUCCESS:
                self.debug("turned on: %s", t_log)
                return
            self._attr_state = bool(level)
            if level:
                self._attr_brightness = level

        # Our light is guaranteed to have just started the transitioning process if necessary,
        # so we start the delay for the transition (to stop parsing attribute reports after the completed transition).
        self.async_transition_start_timer(transition_time)

        if effect == light.EFFECT_COLORLOOP:
            result = await self._color_channel.color_loop_set(
                update_flags=(
                    Color.ColorLoopUpdateFlags.Action
                    | Color.ColorLoopUpdateFlags.Direction
                    | Color.ColorLoopUpdateFlags.Time
                ),
                action=Color.ColorLoopAction.Activate_from_current_hue,
                direction=Color.ColorLoopDirection.Increment,
                time=transition if transition else 7,
                start_hue=0,
            )
            t_log["color_loop_set"] = result
            self._attr_effect = light.EFFECT_COLORLOOP
        elif (
            self._attr_effect == light.EFFECT_COLORLOOP
            and effect != light.EFFECT_COLORLOOP
        ):
            result = await self._color_channel.color_loop_set(
                update_flags=Color.ColorLoopUpdateFlags.Action,
                action=Color.ColorLoopAction.Deactivate,
                direction=Color.ColorLoopDirection.Decrement,
                time=0,
                start_hue=0,
            )
            t_log["color_loop_set"] = result
            self._attr_effect = None

        if flash is not None:
            result = await self._identify_channel.trigger_effect(
                effect_id=FLASH_EFFECTS[flash],
                effect_variant=Identify.EffectVariant.Default,
            )
            t_log["trigger_effect"] = result

        self._off_with_transition = False
        self._off_brightness = None
        self.debug("turned on: %s", t_log)
        self.async_write_ha_state()

    async def async_turn_off(self, **kwargs: Any) -> None:
        """Turn the entity off."""
        transition = kwargs.get(light.ATTR_TRANSITION)
        supports_level = brightness_supported(self._attr_supported_color_modes)

        transition_time = (
            transition or self._DEFAULT_MIN_TRANSITION_TIME
            if transition is not None
            else DEFAULT_ON_OFF_TRANSITION
        ) + DEFAULT_EXTRA_TRANSITION_DELAY_SHORT

        # Start pausing attribute report parsing
        if self._zha_config_enable_light_transitioning_flag:
            self.async_transition_set_flag()

        # is not none looks odd here but it will override built in bulb transition times if we pass 0 in here
        if transition is not None and supports_level:
            result = await self._level_channel.move_to_level_with_on_off(
                level=0,
                transition_time=(transition * 10 or self._DEFAULT_MIN_TRANSITION_TIME),
            )
        else:
            result = await self._on_off_channel.off()

        # Pause parsing attribute reports until transition is complete
        if self._zha_config_enable_light_transitioning_flag:
            self.async_transition_start_timer(transition_time)
        self.debug("turned off: %s", result)
        if isinstance(result, Exception) or result[1] is not Status.SUCCESS:
            return
        self._attr_state = False

        if supports_level:
            # store current brightness so that the next turn_on uses it.
            self._off_with_transition = transition is not None
            self._off_brightness = self._attr_brightness

        self.async_write_ha_state()

    async def async_handle_color_commands(
        self,
        temperature,
        duration,
        hs_color,
        xy_color,
        new_color_provided_while_off,
        t_log,
    ):
        """Process ZCL color commands."""

        transition_time = (
            self._DEFAULT_MIN_TRANSITION_TIME
            if new_color_provided_while_off
            else duration
        )

        if temperature is not None:
            result = await self._color_channel.move_to_color_temp(
                color_temp_mireds=temperature,
                transition_time=transition_time,
            )
            t_log["move_to_color_temp"] = result
            if isinstance(result, Exception) or result[1] is not Status.SUCCESS:
                return False
            self._attr_color_mode = ColorMode.COLOR_TEMP
            self._attr_color_temp = temperature
            self._attr_xy_color = None
            self._attr_hs_color = None

        if hs_color is not None:
            if (
                not isinstance(self, LightGroup)
                and self._color_channel.enhanced_hue_supported
            ):
                result = await self._color_channel.enhanced_move_to_hue_and_saturation(
                    enhanced_hue=int(hs_color[0] * 65535 / 360),
                    saturation=int(hs_color[1] * 2.54),
                    transition_time=transition_time,
                )
                t_log["enhanced_move_to_hue_and_saturation"] = result
            else:
                result = await self._color_channel.move_to_hue_and_saturation(
                    hue=int(hs_color[0] * 254 / 360),
                    saturation=int(hs_color[1] * 2.54),
                    transition_time=transition_time,
                )
                t_log["move_to_hue_and_saturation"] = result
            if isinstance(result, Exception) or result[1] is not Status.SUCCESS:
                return False
            self._attr_color_mode = ColorMode.HS
            self._attr_hs_color = hs_color
            self._attr_xy_color = None
            self._attr_color_temp = None
            xy_color = None  # don't set xy_color if it is also present

        if xy_color is not None:
            result = await self._color_channel.move_to_color(
                color_x=int(xy_color[0] * 65535),
                color_y=int(xy_color[1] * 65535),
                transition_time=transition_time,
            )
            t_log["move_to_color"] = result
            if isinstance(result, Exception) or result[1] is not Status.SUCCESS:
                return False
            self._attr_color_mode = ColorMode.XY
            self._attr_xy_color = xy_color
            self._attr_color_temp = None
            self._attr_hs_color = None

        return True

    @callback
    def async_transition_set_flag(self) -> None:
        """Set _transitioning to True."""
        self.debug("setting transitioning flag to True")
        self._transitioning = True
        if isinstance(self, LightGroup):
            async_dispatcher_send(
                self.hass,
                SIGNAL_LIGHT_GROUP_TRANSITION_START,
                {"entity_ids": self._entity_ids},
            )
        if self._transition_listener is not None:
            self._transition_listener()

    @callback
    def async_transition_start_timer(self, transition_time) -> None:
        """Start a timer to unset _transitioning after transition_time if necessary."""
        if not transition_time:
            return
        # For longer transitions, we want to extend the timer a bit more
        if transition_time >= DEFAULT_LONG_TRANSITION_TIME:
            transition_time += DEFAULT_EXTRA_TRANSITION_DELAY_LONG
        self.debug("starting transitioning timer for %s", transition_time)
        self._transition_listener = async_call_later(
            self._zha_device.hass,
            transition_time,
            self.async_transition_complete,
        )

    @callback
    def async_transition_complete(self, _=None) -> None:
        """Set _transitioning to False and write HA state."""
        self.debug("transition complete - future attribute reports will write HA state")
        self._transitioning = False
        if self._transition_listener:
            self._transition_listener()
            self._transition_listener = None
        self.async_write_ha_state()
        if isinstance(self, LightGroup):
            async_dispatcher_send(
                self.hass,
                SIGNAL_LIGHT_GROUP_TRANSITION_FINISHED,
                {"entity_ids": self._entity_ids},
            )
            if self._debounced_member_refresh is not None:
                self.debug("transition complete - refreshing group member states")
                asyncio.create_task(self._debounced_member_refresh.async_call())


@STRICT_MATCH(channel_names=CHANNEL_ON_OFF, aux_channels={CHANNEL_COLOR, CHANNEL_LEVEL})
class Light(BaseLight, ZhaEntity):
    """Representation of a ZHA or ZLL light."""

    _attr_supported_color_modes: set[ColorMode]
    _REFRESH_INTERVAL = (45, 75)

    def __init__(self, unique_id, zha_device: ZHADevice, channels, **kwargs):
        """Initialize the ZHA light."""
        super().__init__(unique_id, zha_device, channels, **kwargs)
        self._on_off_channel = self.cluster_channels[CHANNEL_ON_OFF]
        self._attr_state = bool(self._on_off_channel.on_off)
        self._level_channel = self.cluster_channels.get(CHANNEL_LEVEL)
        self._color_channel = self.cluster_channels.get(CHANNEL_COLOR)
        self._identify_channel = self.zha_device.channels.identify_ch
        if self._color_channel:
            self._attr_min_mireds: int = self._color_channel.min_mireds
            self._attr_max_mireds: int = self._color_channel.max_mireds
        self._cancel_refresh_handle: CALLBACK_TYPE | None = None
        effect_list = []

        self._zha_config_always_prefer_xy_color_mode = async_get_zha_config_value(
            zha_device.gateway.config_entry,
            ZHA_OPTIONS,
            CONF_ALWAYS_PREFER_XY_COLOR_MODE,
            True,
        )

        self._attr_supported_color_modes = {ColorMode.ONOFF}
        if self._level_channel:
            self._attr_supported_color_modes.add(ColorMode.BRIGHTNESS)
            self._attr_supported_features |= light.LightEntityFeature.TRANSITION
            self._attr_brightness = self._level_channel.current_level

        if self._color_channel:
            if self._color_channel.color_temp_supported:
                self._attr_supported_color_modes.add(ColorMode.COLOR_TEMP)
                self._attr_color_temp = self._color_channel.color_temperature

            if self._color_channel.xy_supported and (
                self._zha_config_always_prefer_xy_color_mode
                or not self._color_channel.hs_supported
            ):
                self._attr_supported_color_modes.add(ColorMode.XY)
                curr_x = self._color_channel.current_x
                curr_y = self._color_channel.current_y
                if curr_x is not None and curr_y is not None:
                    self._attr_xy_color = (curr_x / 65535, curr_y / 65535)
                else:
                    self._attr_xy_color = (0, 0)

            if (
                self._color_channel.hs_supported
                and not self._zha_config_always_prefer_xy_color_mode
            ):
                self._attr_supported_color_modes.add(ColorMode.HS)
                if (
                    self._color_channel.enhanced_hue_supported
                    and self._color_channel.enhanced_current_hue is not None
                ):
                    curr_hue = self._color_channel.enhanced_current_hue * 65535 / 360
                elif self._color_channel.current_hue is not None:
                    curr_hue = self._color_channel.current_hue * 254 / 360
                else:
                    curr_hue = 0

                if (curr_saturation := self._color_channel.current_saturation) is None:
                    curr_saturation = 0

                self._attr_hs_color = (
                    int(curr_hue),
                    int(curr_saturation * 2.54),
                )

            if self._color_channel.color_loop_supported:
                self._attr_supported_features |= light.LightEntityFeature.EFFECT
                effect_list.append(light.EFFECT_COLORLOOP)
                if self._color_channel.color_loop_active == 1:
                    self._attr_effect = light.EFFECT_COLORLOOP
        self._attr_supported_color_modes = filter_supported_color_modes(
            self._attr_supported_color_modes
        )
        if len(self._attr_supported_color_modes) == 1:
            self._attr_color_mode = next(iter(self._attr_supported_color_modes))
        else:  # Light supports color_temp + hs, determine which mode the light is in
            assert self._color_channel
            if self._color_channel.color_mode == Color.ColorMode.Color_temperature:
                self._attr_color_mode = ColorMode.COLOR_TEMP
            else:
                self._attr_color_mode = ColorMode.XY

        if self._identify_channel:
            self._attr_supported_features |= light.LightEntityFeature.FLASH

        if effect_list:
            self._attr_effect_list = effect_list

        self._zha_config_transition = async_get_zha_config_value(
            zha_device.gateway.config_entry,
            ZHA_OPTIONS,
            CONF_DEFAULT_LIGHT_TRANSITION,
            0,
        )
        self._zha_config_enhanced_light_transition = async_get_zha_config_value(
            zha_device.gateway.config_entry,
            ZHA_OPTIONS,
            CONF_ENABLE_ENHANCED_LIGHT_TRANSITION,
            False,
        )
        self._zha_config_enable_light_transitioning_flag = async_get_zha_config_value(
            zha_device.gateway.config_entry,
            ZHA_OPTIONS,
            CONF_ENABLE_LIGHT_TRANSITIONING_FLAG,
            True,
        )

    @callback
    def async_set_state(self, attr_id, attr_name, value):
        """Set the state."""
        if self._transitioning:
            self.debug(
                "received onoff %s while transitioning - skipping update",
                value,
            )
            return
        self._attr_state = bool(value)
        if value:
            self._off_with_transition = False
            self._off_brightness = None
        self.async_write_ha_state()

    async def async_added_to_hass(self) -> None:
        """Run when about to be added to hass."""
        await super().async_added_to_hass()
        self.async_accept_signal(
            self._on_off_channel, SIGNAL_ATTR_UPDATED, self.async_set_state
        )
        if self._level_channel:
            self.async_accept_signal(
                self._level_channel, SIGNAL_SET_LEVEL, self.set_level
            )
        refresh_interval = random.randint(*(x * 60 for x in self._REFRESH_INTERVAL))
        self._cancel_refresh_handle = async_track_time_interval(
            self.hass, self._refresh, timedelta(seconds=refresh_interval)
        )
        self.async_accept_signal(
            None,
            SIGNAL_LIGHT_GROUP_STATE_CHANGED,
            self._maybe_force_refresh,
            signal_override=True,
        )

        @callback
        def transition_on(signal):
            """Handle a transition start event from a group."""
            if self.entity_id in signal["entity_ids"]:
                self.debug(
                    "group transition started - setting member transitioning flag"
                )
                self._transitioning = True

        self.async_accept_signal(
            None,
            SIGNAL_LIGHT_GROUP_TRANSITION_START,
            transition_on,
            signal_override=True,
        )

        @callback
        def transition_off(signal):
            """Handle a transition finished event from a group."""
            if self.entity_id in signal["entity_ids"]:
                self.debug(
                    "group transition completed - unsetting member transitioning flag"
                )
                self._transitioning = False

        self.async_accept_signal(
            None,
            SIGNAL_LIGHT_GROUP_TRANSITION_FINISHED,
            transition_off,
            signal_override=True,
        )

    async def async_will_remove_from_hass(self) -> None:
        """Disconnect entity object when removed."""
        assert self._cancel_refresh_handle
        self._cancel_refresh_handle()
        await super().async_will_remove_from_hass()

    @callback
    def async_restore_last_state(self, last_state):
        """Restore previous state."""
        self._attr_state = last_state.state == STATE_ON
        if "brightness" in last_state.attributes:
            self._attr_brightness = last_state.attributes["brightness"]
        if "off_with_transition" in last_state.attributes:
            self._off_with_transition = last_state.attributes["off_with_transition"]
        if "off_brightness" in last_state.attributes:
            self._off_brightness = last_state.attributes["off_brightness"]
        if "color_mode" in last_state.attributes:
            self._attr_color_mode = ColorMode(last_state.attributes["color_mode"])
        if "color_temp" in last_state.attributes:
            self._attr_color_temp = last_state.attributes["color_temp"]
        if "xy_color" in last_state.attributes:
            self._attr_xy_color = last_state.attributes["xy_color"]
        if "hs_color" in last_state.attributes:
            self._attr_hs_color = last_state.attributes["hs_color"]
        if "effect" in last_state.attributes:
            self._attr_effect = last_state.attributes["effect"]

    async def async_get_state(self) -> None:
        """Attempt to retrieve the state from the light."""
        if not self._attr_available:
            return
        self.debug("polling current state")
        if self._on_off_channel:
            state = await self._on_off_channel.get_attribute_value(
                "on_off", from_cache=False
            )
            if state is not None:
                self._attr_state = state
        if self._level_channel:
            level = await self._level_channel.get_attribute_value(
                "current_level", from_cache=False
            )
            if level is not None:
                self._attr_brightness = level
        if self._color_channel:
            attributes = [
                "color_mode",
                "current_x",
                "current_y",
            ]
            if (
                not self._zha_config_always_prefer_xy_color_mode
                and self._color_channel.enhanced_hue_supported
            ):
                attributes.append("enhanced_current_hue")
                attributes.append("current_saturation")
            if (
                self._color_channel.hs_supported
                and not self._color_channel.enhanced_hue_supported
                and not self._zha_config_always_prefer_xy_color_mode
            ):
                attributes.append("current_hue")
                attributes.append("current_saturation")
            if self._color_channel.color_temp_supported:
                attributes.append("color_temperature")
            if self._color_channel.color_loop_supported:
                attributes.append("color_loop_active")

            results = await self._color_channel.get_attributes(
                attributes, from_cache=False, only_cache=False
            )

            if (color_mode := results.get("color_mode")) is not None:
                if color_mode == Color.ColorMode.Color_temperature:
                    self._attr_color_mode = ColorMode.COLOR_TEMP
                    color_temp = results.get("color_temperature")
                    if color_temp is not None and color_mode:
                        self._attr_color_temp = color_temp
                        self._attr_xy_color = None
                        self._attr_hs_color = None
                elif (
                    color_mode == Color.ColorMode.Hue_and_saturation
                    and not self._zha_config_always_prefer_xy_color_mode
                ):
                    self._attr_color_mode = ColorMode.HS
                    if self._color_channel.enhanced_hue_supported:
                        current_hue = results.get("enhanced_current_hue")
                    else:
                        current_hue = results.get("current_hue")
                    current_saturation = results.get("current_saturation")
                    if current_hue is not None and current_saturation is not None:
                        self._attr_hs_color = (
                            int(current_hue * 360 / 65535)
                            if self._color_channel.enhanced_hue_supported
                            else int(current_hue * 360 / 254),
                            int(current_saturation / 2.54),
                        )
                        self._attr_xy_color = None
                        self._attr_color_temp = None
                else:
                    self._attr_color_mode = ColorMode.XY
                    color_x = results.get("current_x")
                    color_y = results.get("current_y")
                    if color_x is not None and color_y is not None:
                        self._attr_xy_color = (color_x / 65535, color_y / 65535)
                        self._attr_color_temp = None
                        self._attr_hs_color = None

            color_loop_active = results.get("color_loop_active")
            if color_loop_active is not None:
                if color_loop_active == 1:
                    self._attr_effect = light.EFFECT_COLORLOOP
                else:
                    self._attr_effect = None

    async def async_update(self) -> None:
        """Update to the latest state."""
        if self._transitioning:
            self.debug("skipping async_update while transitioning")
            return
        await self.async_get_state()

    async def _refresh(self, time):
        """Call async_get_state at an interval."""
        if self._transitioning:
            self.debug("skipping _refresh while transitioning")
            return
        await self.async_get_state()
        self.async_write_ha_state()

    async def _maybe_force_refresh(self, signal):
        """Force update the state if the signal contains the entity id for this entity."""
        if self.entity_id in signal["entity_ids"]:
            if self._transitioning:
                self.debug("skipping _maybe_force_refresh while transitioning")
                return
            await self.async_get_state()
            self.async_write_ha_state()


@STRICT_MATCH(
    channel_names=CHANNEL_ON_OFF,
    aux_channels={CHANNEL_COLOR, CHANNEL_LEVEL},
    manufacturers={"Philips", "Signify Netherlands B.V."},
)
class HueLight(Light):
    """Representation of a HUE light which does not report attributes."""

    _REFRESH_INTERVAL = (3, 5)


@STRICT_MATCH(
    channel_names=CHANNEL_ON_OFF,
    aux_channels={CHANNEL_COLOR, CHANNEL_LEVEL},
    manufacturers={"Jasco", "Quotra-Vision", "eWeLight", "eWeLink"},
)
class ForceOnLight(Light):
    """Representation of a light which does not respect move_to_level_with_on_off."""

    _FORCE_ON = True


@STRICT_MATCH(
    channel_names=CHANNEL_ON_OFF,
    aux_channels={CHANNEL_COLOR, CHANNEL_LEVEL},
    manufacturers=DEFAULT_MIN_TRANSITION_MANUFACTURERS,
)
class MinTransitionLight(Light):
    """Representation of a light which does not react to any "move to" calls with 0 as a transition."""

    _DEFAULT_MIN_TRANSITION_TIME = 1


@GROUP_MATCH()
class LightGroup(BaseLight, ZhaGroupEntity):
    """Representation of a light group."""

    def __init__(
        self,
        entity_ids: list[str],
        unique_id: str,
        group_id: int,
        zha_device: ZHADevice,
        **kwargs: Any,
    ) -> None:
        """Initialize a light group."""
        super().__init__(entity_ids, unique_id, group_id, zha_device, **kwargs)
        group = self.zha_device.gateway.get_group(self._group_id)
        self._DEFAULT_MIN_TRANSITION_TIME = any(  # pylint: disable=invalid-name
            member.device.manufacturer in DEFAULT_MIN_TRANSITION_MANUFACTURERS
            for member in group.members
        )
        self._on_off_channel = group.endpoint[OnOff.cluster_id]
        self._level_channel = group.endpoint[LevelControl.cluster_id]
        self._color_channel = group.endpoint[Color.cluster_id]
        self._identify_channel = group.endpoint[Identify.cluster_id]
        self._debounced_member_refresh: Debouncer | None = None
        self._zha_config_transition = async_get_zha_config_value(
            zha_device.gateway.config_entry,
            ZHA_OPTIONS,
            CONF_DEFAULT_LIGHT_TRANSITION,
            0,
        )
        self._zha_config_enable_light_transitioning_flag = async_get_zha_config_value(
            zha_device.gateway.config_entry,
            ZHA_OPTIONS,
            CONF_ENABLE_LIGHT_TRANSITIONING_FLAG,
            True,
        )
        self._zha_config_always_prefer_xy_color_mode = async_get_zha_config_value(
            zha_device.gateway.config_entry,
            ZHA_OPTIONS,
            CONF_ALWAYS_PREFER_XY_COLOR_MODE,
            True,
        )
        self._zha_config_enhanced_light_transition = False
        self._attr_color_mode = None

    # remove this when all ZHA platforms and base entities are updated
    @property
    def available(self) -> bool:
        """Return entity availability."""
        return self._attr_available

    async def async_added_to_hass(self) -> None:
        """Run when about to be added to hass."""
        await super().async_added_to_hass()
        if self._debounced_member_refresh is None:
            force_refresh_debouncer = Debouncer(
                self.hass,
                _LOGGER,
                cooldown=3,
                immediate=True,
                function=self._force_member_updates,
            )
            self._debounced_member_refresh = force_refresh_debouncer

    async def async_turn_on(self, **kwargs: Any) -> None:
        """Turn the entity on."""
        await super().async_turn_on(**kwargs)
        if self._transitioning:
            return
        if self._debounced_member_refresh:
            await self._debounced_member_refresh.async_call()

    async def async_turn_off(self, **kwargs: Any) -> None:
        """Turn the entity off."""
        await super().async_turn_off(**kwargs)
        if self._transitioning:
            return
        if self._debounced_member_refresh:
            await self._debounced_member_refresh.async_call()

    @callback
    def async_state_changed_listener(self, event: Event) -> None:
        """Handle child updates."""
        if self._transitioning:
            self.debug("skipping group entity state update during transition")
            return
        super().async_state_changed_listener(event)

    async def async_update_ha_state(self, force_refresh: bool = False) -> None:
        """Update Home Assistant with current state of entity."""
        if self._transitioning:
            self.debug("skipping group entity state update during transition")
            return
        await super().async_update_ha_state(force_refresh)

    async def async_update(self) -> None:
        """Query all members and determine the light group state."""
        all_states = [self.hass.states.get(x) for x in self._entity_ids]
        states: list[State] = list(filter(None, all_states))
        on_states = [state for state in states if state.state == STATE_ON]

        self._attr_state = len(on_states) > 0
        self._attr_available = any(state.state != STATE_UNAVAILABLE for state in states)

        self._attr_brightness = helpers.reduce_attribute(
            on_states, light.ATTR_BRIGHTNESS
        )

        self._attr_xy_color = helpers.reduce_attribute(
            on_states, light.ATTR_XY_COLOR, reduce=helpers.mean_tuple
        )

        if not self._zha_config_always_prefer_xy_color_mode:
            self._attr_hs_color = helpers.reduce_attribute(
                on_states, light.ATTR_HS_COLOR, reduce=helpers.mean_tuple
            )

        self._attr_color_temp = helpers.reduce_attribute(
            on_states, light.ATTR_COLOR_TEMP
        )
        self._attr_min_mireds = helpers.reduce_attribute(
            states, light.ATTR_MIN_MIREDS, default=153, reduce=min
        )
        self._attr_max_mireds = helpers.reduce_attribute(
            states, light.ATTR_MAX_MIREDS, default=500, reduce=max
        )

        self._attr_effect_list = None
        all_effect_lists = list(
            helpers.find_state_attributes(states, light.ATTR_EFFECT_LIST)
        )
        if all_effect_lists:
            # Merge all effects from all effect_lists with a union merge.
            self._attr_effect_list = list(set().union(*all_effect_lists))

        self._attr_effect = None
        all_effects = list(helpers.find_state_attributes(on_states, light.ATTR_EFFECT))
        if all_effects:
            # Report the most common effect.
            effects_count = Counter(itertools.chain(all_effects))
            self._attr_effect = effects_count.most_common(1)[0][0]

        self._attr_color_mode = None
        all_color_modes = list(
            helpers.find_state_attributes(on_states, light.ATTR_COLOR_MODE)
        )
        if all_color_modes:
            # Report the most common color mode, select brightness and onoff last
            color_mode_count = Counter(itertools.chain(all_color_modes))
            if ColorMode.ONOFF in color_mode_count:
                color_mode_count[ColorMode.ONOFF] = -1
            if ColorMode.BRIGHTNESS in color_mode_count:
                color_mode_count[ColorMode.BRIGHTNESS] = 0
            self._attr_color_mode = color_mode_count.most_common(1)[0][0]
            if self._attr_color_mode == ColorMode.HS and (
                color_mode_count[ColorMode.HS] != len(self._group.members)
                or self._zha_config_always_prefer_xy_color_mode
            ):  # switch to XY if all members do not support HS
                self._attr_color_mode = ColorMode.XY

        self._attr_supported_color_modes = None
        all_supported_color_modes = list(
            helpers.find_state_attributes(states, light.ATTR_SUPPORTED_COLOR_MODES)
        )
        if all_supported_color_modes:
            # Merge all color modes.
            self._attr_supported_color_modes = cast(
                set[str], set().union(*all_supported_color_modes)
            )

        self._attr_supported_features = 0
        for support in helpers.find_state_attributes(states, ATTR_SUPPORTED_FEATURES):
            # Merge supported features by emulating support for every feature
            # we find.
            self._attr_supported_features |= support
        # Bitwise-and the supported features with the GroupedLight's features
        # so that we don't break in the future when a new feature is added.
        self._attr_supported_features &= SUPPORT_GROUP_LIGHT

    async def _force_member_updates(self) -> None:
        """Force the update of member entities to ensure the states are correct for bulbs that don't report their state."""
        async_dispatcher_send(
            self.hass,
            SIGNAL_LIGHT_GROUP_STATE_CHANGED,
            {"entity_ids": self._entity_ids},
        )