"""Light platform support for yeelight."""

from __future__ import annotations

from collections.abc import Callable, Coroutine
import logging
import math
from typing import Any, Concatenate

import voluptuous as vol
import yeelight
from yeelight import Flow, RGBTransition, SleepTransition, flows
from yeelight.aio import AsyncBulb
from yeelight.enums import BulbType, LightType, PowerMode, SceneClass
from yeelight.main import BulbException

from homeassistant.components.light import (
    ATTR_BRIGHTNESS,
    ATTR_COLOR_TEMP_KELVIN,
    ATTR_EFFECT,
    ATTR_FLASH,
    ATTR_HS_COLOR,
    ATTR_RGB_COLOR,
    ATTR_TRANSITION,
    FLASH_LONG,
    FLASH_SHORT,
    ColorMode,
    LightEntity,
    LightEntityFeature,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_ENTITY_ID, ATTR_MODE, CONF_NAME
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv, entity_platform
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.event import async_call_later
from homeassistant.helpers.typing import VolDictType
from homeassistant.util import color as color_util

from . import YEELIGHT_FLOW_TRANSITION_SCHEMA
from .const import (
    ACTION_RECOVER,
    ATTR_ACTION,
    ATTR_COUNT,
    ATTR_MODE_MUSIC,
    ATTR_TRANSITIONS,
    CONF_FLOW_PARAMS,
    CONF_MODE_MUSIC,
    CONF_NIGHTLIGHT_SWITCH,
    CONF_SAVE_ON_CHANGE,
    CONF_TRANSITION,
    DATA_CONFIG_ENTRIES,
    DATA_CUSTOM_EFFECTS,
    DATA_DEVICE,
    DATA_UPDATED,
    DOMAIN,
    MODELS_WITH_DELAYED_ON_TRANSITION,
    POWER_STATE_CHANGE_TIME,
)
from .device import YeelightDevice
from .entity import YeelightEntity

_LOGGER = logging.getLogger(__name__)

ATTR_MINUTES = "minutes"
ATTR_KELVIN = "kelvin"

SERVICE_SET_MODE = "set_mode"
SERVICE_SET_MUSIC_MODE = "set_music_mode"
SERVICE_START_FLOW = "start_flow"
SERVICE_SET_COLOR_SCENE = "set_color_scene"
SERVICE_SET_HSV_SCENE = "set_hsv_scene"
SERVICE_SET_COLOR_TEMP_SCENE = "set_color_temp_scene"
SERVICE_SET_COLOR_FLOW_SCENE = "set_color_flow_scene"
SERVICE_SET_AUTO_DELAY_OFF_SCENE = "set_auto_delay_off_scene"

EFFECT_DISCO = "Disco"
EFFECT_TEMP = "Slow Temp"
EFFECT_STROBE = "Strobe epilepsy!"
EFFECT_STROBE_COLOR = "Strobe color"
EFFECT_ALARM = "Alarm"
EFFECT_POLICE = "Police"
EFFECT_POLICE2 = "Police2"
EFFECT_CHRISTMAS = "Christmas"
EFFECT_RGB = "RGB"
EFFECT_RANDOM_LOOP = "Random Loop"
EFFECT_FAST_RANDOM_LOOP = "Fast Random Loop"
EFFECT_LSD = "LSD"
EFFECT_SLOWDOWN = "Slowdown"
EFFECT_WHATSAPP = "WhatsApp"
EFFECT_FACEBOOK = "Facebook"
EFFECT_TWITTER = "Twitter"
EFFECT_STOP = "Stop"
EFFECT_HOME = "Home"
EFFECT_NIGHT_MODE = "Night Mode"
EFFECT_DATE_NIGHT = "Date Night"
EFFECT_MOVIE = "Movie"
EFFECT_SUNRISE = "Sunrise"
EFFECT_SUNSET = "Sunset"
EFFECT_ROMANCE = "Romance"
EFFECT_HAPPY_BIRTHDAY = "Happy Birthday"
EFFECT_CANDLE_FLICKER = "Candle Flicker"
EFFECT_TEA_TIME = "Tea Time"

YEELIGHT_TEMP_ONLY_EFFECT_LIST = [EFFECT_TEMP, EFFECT_STOP]

YEELIGHT_MONO_EFFECT_LIST = [
    EFFECT_DISCO,
    EFFECT_STROBE,
    EFFECT_ALARM,
    EFFECT_POLICE2,
    EFFECT_WHATSAPP,
    EFFECT_FACEBOOK,
    EFFECT_TWITTER,
    EFFECT_HOME,
    EFFECT_CANDLE_FLICKER,
    EFFECT_TEA_TIME,
    *YEELIGHT_TEMP_ONLY_EFFECT_LIST,
]

YEELIGHT_COLOR_EFFECT_LIST = [
    EFFECT_STROBE_COLOR,
    EFFECT_POLICE,
    EFFECT_CHRISTMAS,
    EFFECT_RGB,
    EFFECT_RANDOM_LOOP,
    EFFECT_FAST_RANDOM_LOOP,
    EFFECT_LSD,
    EFFECT_SLOWDOWN,
    EFFECT_NIGHT_MODE,
    EFFECT_DATE_NIGHT,
    EFFECT_MOVIE,
    EFFECT_SUNRISE,
    EFFECT_SUNSET,
    EFFECT_ROMANCE,
    EFFECT_HAPPY_BIRTHDAY,
    *YEELIGHT_MONO_EFFECT_LIST,
]

EFFECTS_MAP = {
    EFFECT_DISCO: flows.disco,
    EFFECT_TEMP: flows.temp,
    EFFECT_STROBE: flows.strobe,
    EFFECT_STROBE_COLOR: flows.strobe_color,
    EFFECT_ALARM: flows.alarm,
    EFFECT_POLICE: flows.police,
    EFFECT_POLICE2: flows.police2,
    EFFECT_CHRISTMAS: flows.christmas,
    EFFECT_RGB: flows.rgb,
    EFFECT_RANDOM_LOOP: flows.random_loop,
    EFFECT_LSD: flows.lsd,
    EFFECT_SLOWDOWN: flows.slowdown,
    EFFECT_HOME: flows.home,
    EFFECT_NIGHT_MODE: flows.night_mode,
    EFFECT_DATE_NIGHT: flows.date_night,
    EFFECT_MOVIE: flows.movie,
    EFFECT_SUNRISE: flows.sunrise,
    EFFECT_SUNSET: flows.sunset,
    EFFECT_ROMANCE: flows.romance,
    EFFECT_HAPPY_BIRTHDAY: flows.happy_birthday,
    EFFECT_CANDLE_FLICKER: flows.candle_flicker,
    EFFECT_TEA_TIME: flows.tea_time,
}

VALID_BRIGHTNESS = vol.All(vol.Coerce(int), vol.Range(min=1, max=100))

SERVICE_SCHEMA_SET_MODE: VolDictType = {
    vol.Required(ATTR_MODE): vol.In([mode.name.lower() for mode in PowerMode])
}

SERVICE_SCHEMA_SET_MUSIC_MODE: VolDictType = {vol.Required(ATTR_MODE_MUSIC): cv.boolean}

SERVICE_SCHEMA_START_FLOW = YEELIGHT_FLOW_TRANSITION_SCHEMA

SERVICE_SCHEMA_SET_COLOR_SCENE: VolDictType = {
    vol.Required(ATTR_RGB_COLOR): vol.All(
        vol.Coerce(tuple), vol.ExactSequence((cv.byte, cv.byte, cv.byte))
    ),
    vol.Required(ATTR_BRIGHTNESS): VALID_BRIGHTNESS,
}

SERVICE_SCHEMA_SET_HSV_SCENE: VolDictType = {
    vol.Required(ATTR_HS_COLOR): vol.All(
        vol.Coerce(tuple),
        vol.ExactSequence(
            (
                vol.All(vol.Coerce(float), vol.Range(min=0, max=359)),
                vol.All(vol.Coerce(float), vol.Range(min=0, max=100)),
            )
        ),
    ),
    vol.Required(ATTR_BRIGHTNESS): VALID_BRIGHTNESS,
}

SERVICE_SCHEMA_SET_COLOR_TEMP_SCENE: VolDictType = {
    vol.Required(ATTR_KELVIN): vol.All(vol.Coerce(int), vol.Range(min=1700, max=6500)),
    vol.Required(ATTR_BRIGHTNESS): VALID_BRIGHTNESS,
}

SERVICE_SCHEMA_SET_COLOR_FLOW_SCENE = YEELIGHT_FLOW_TRANSITION_SCHEMA

SERVICE_SCHEMA_SET_AUTO_DELAY_OFF_SCENE: VolDictType = {
    vol.Required(ATTR_MINUTES): vol.All(vol.Coerce(int), vol.Range(min=1, max=60)),
    vol.Required(ATTR_BRIGHTNESS): VALID_BRIGHTNESS,
}


@callback
def _transitions_config_parser(transitions):
    """Parse transitions config into initialized objects."""
    transition_objects = []
    for transition_config in transitions:
        transition, params = list(transition_config.items())[0]
        transition_objects.append(getattr(yeelight, transition)(*params))

    return transition_objects


@callback
def _parse_custom_effects(effects_config) -> dict[str, dict[str, Any]]:
    effects = {}
    for config in effects_config:
        params = config[CONF_FLOW_PARAMS]
        action = Flow.actions[params[ATTR_ACTION]]
        transitions = _transitions_config_parser(params[ATTR_TRANSITIONS])

        effects[config[CONF_NAME]] = {
            ATTR_COUNT: params[ATTR_COUNT],
            ATTR_ACTION: action,
            ATTR_TRANSITIONS: transitions,
        }

    return effects


def _async_cmd[_YeelightBaseLightT: YeelightBaseLight, **_P, _R](
    func: Callable[Concatenate[_YeelightBaseLightT, _P], Coroutine[Any, Any, _R]],
) -> Callable[Concatenate[_YeelightBaseLightT, _P], Coroutine[Any, Any, _R | None]]:
    """Define a wrapper to catch exceptions from the bulb."""

    async def _async_wrap(
        self: _YeelightBaseLightT, *args: _P.args, **kwargs: _P.kwargs
    ) -> _R | None:
        for attempts in range(2):
            try:
                _LOGGER.debug("Calling %s with %s %s", func, args, kwargs)
                return await func(self, *args, **kwargs)
            except TimeoutError as ex:
                # The wifi likely dropped, so we want to retry once since
                # python-yeelight will auto reconnect
                if attempts == 0:
                    continue
                raise HomeAssistantError(
                    f"Timed out when calling {func.__name__} for bulb "
                    f"{self.device.name} at {self.device.host}: {str(ex) or type(ex)}"
                ) from ex
            except OSError as ex:
                # A network error happened, the bulb is likely offline now
                self.device.async_mark_unavailable()
                self.async_state_changed()
                raise HomeAssistantError(
                    f"Error when calling {func.__name__} for bulb "
                    f"{self.device.name} at {self.device.host}: {str(ex) or type(ex)}"
                ) from ex
            except BulbException as ex:
                # The bulb likely responded but had an error
                raise HomeAssistantError(
                    f"Error when calling {func.__name__} for bulb "
                    f"{self.device.name} at {self.device.host}: {str(ex) or type(ex)}"
                ) from ex
        return None

    return _async_wrap


async def async_setup_entry(
    hass: HomeAssistant,
    config_entry: ConfigEntry,
    async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
    """Set up Yeelight from a config entry."""
    custom_effects = _parse_custom_effects(hass.data[DOMAIN][DATA_CUSTOM_EFFECTS])

    device = hass.data[DOMAIN][DATA_CONFIG_ENTRIES][config_entry.entry_id][DATA_DEVICE]
    _LOGGER.debug("Adding %s", device.name)

    nl_switch_light = device.config.get(CONF_NIGHTLIGHT_SWITCH)

    lights = []

    device_type = device.type

    def _lights_setup_helper(klass):
        lights.append(klass(device, config_entry, custom_effects=custom_effects))

    if device_type == BulbType.White:
        _lights_setup_helper(YeelightGenericLight)
    elif device_type == BulbType.Color:
        if nl_switch_light and device.is_nightlight_supported:
            _lights_setup_helper(YeelightColorLightWithNightlightSwitch)
            _lights_setup_helper(YeelightNightLightModeWithoutBrightnessControl)
        else:
            _lights_setup_helper(YeelightColorLightWithoutNightlightSwitchLight)
    elif device_type == BulbType.WhiteTemp:
        if nl_switch_light and device.is_nightlight_supported:
            _lights_setup_helper(YeelightWithNightLight)
            _lights_setup_helper(YeelightNightLightMode)
        else:
            _lights_setup_helper(YeelightWhiteTempWithoutNightlightSwitch)
    elif device_type == BulbType.WhiteTempMood:
        if nl_switch_light and device.is_nightlight_supported:
            _lights_setup_helper(YeelightNightLightModeWithAmbientSupport)
            _lights_setup_helper(YeelightWithAmbientAndNightlight)
        else:
            _lights_setup_helper(YeelightWithAmbientWithoutNightlight)
        _lights_setup_helper(YeelightAmbientLight)
    else:
        _lights_setup_helper(YeelightGenericLight)
        _LOGGER.warning(
            "Cannot determine device type for %s, %s. Falling back to white only",
            device.host,
            device.name,
        )

    async_add_entities(lights)
    _async_setup_services(hass)


@callback
def _async_setup_services(hass: HomeAssistant):
    """Set up custom services."""

    async def _async_start_flow(entity, service_call):
        params = {**service_call.data}
        params.pop(ATTR_ENTITY_ID)
        params[ATTR_TRANSITIONS] = _transitions_config_parser(params[ATTR_TRANSITIONS])
        await entity.async_start_flow(**params)

    async def _async_set_color_scene(entity, service_call):
        await entity.async_set_scene(
            SceneClass.COLOR,
            *service_call.data[ATTR_RGB_COLOR],
            service_call.data[ATTR_BRIGHTNESS],
        )

    async def _async_set_hsv_scene(entity, service_call):
        await entity.async_set_scene(
            SceneClass.HSV,
            *service_call.data[ATTR_HS_COLOR],
            service_call.data[ATTR_BRIGHTNESS],
        )

    async def _async_set_color_temp_scene(entity, service_call):
        await entity.async_set_scene(
            SceneClass.CT,
            service_call.data[ATTR_KELVIN],
            service_call.data[ATTR_BRIGHTNESS],
        )

    async def _async_set_color_flow_scene(entity, service_call):
        flow = Flow(
            count=service_call.data[ATTR_COUNT],
            action=Flow.actions[service_call.data[ATTR_ACTION]],
            transitions=_transitions_config_parser(service_call.data[ATTR_TRANSITIONS]),
        )
        await entity.async_set_scene(SceneClass.CF, flow)

    async def _async_set_auto_delay_off_scene(entity, service_call):
        await entity.async_set_scene(
            SceneClass.AUTO_DELAY_OFF,
            service_call.data[ATTR_BRIGHTNESS],
            service_call.data[ATTR_MINUTES],
        )

    platform = entity_platform.async_get_current_platform()

    platform.async_register_entity_service(
        SERVICE_SET_MODE, SERVICE_SCHEMA_SET_MODE, "async_set_mode"
    )
    platform.async_register_entity_service(
        SERVICE_START_FLOW, SERVICE_SCHEMA_START_FLOW, _async_start_flow
    )
    platform.async_register_entity_service(
        SERVICE_SET_COLOR_SCENE, SERVICE_SCHEMA_SET_COLOR_SCENE, _async_set_color_scene
    )
    platform.async_register_entity_service(
        SERVICE_SET_HSV_SCENE, SERVICE_SCHEMA_SET_HSV_SCENE, _async_set_hsv_scene
    )
    platform.async_register_entity_service(
        SERVICE_SET_COLOR_TEMP_SCENE,
        SERVICE_SCHEMA_SET_COLOR_TEMP_SCENE,
        _async_set_color_temp_scene,
    )
    platform.async_register_entity_service(
        SERVICE_SET_COLOR_FLOW_SCENE,
        SERVICE_SCHEMA_SET_COLOR_FLOW_SCENE,
        _async_set_color_flow_scene,
    )
    platform.async_register_entity_service(
        SERVICE_SET_AUTO_DELAY_OFF_SCENE,
        SERVICE_SCHEMA_SET_AUTO_DELAY_OFF_SCENE,
        _async_set_auto_delay_off_scene,
    )
    platform.async_register_entity_service(
        SERVICE_SET_MUSIC_MODE, SERVICE_SCHEMA_SET_MUSIC_MODE, "async_set_music_mode"
    )


class YeelightBaseLight(YeelightEntity, LightEntity):
    """Abstract Yeelight light."""

    _attr_color_mode = ColorMode.BRIGHTNESS
    _attr_supported_color_modes = {ColorMode.BRIGHTNESS}
    _attr_supported_features = (
        LightEntityFeature.TRANSITION
        | LightEntityFeature.FLASH
        | LightEntityFeature.EFFECT
    )
    _attr_should_poll = False

    def __init__(
        self,
        device: YeelightDevice,
        entry: ConfigEntry,
        custom_effects: dict[str, dict[str, Any]] | None = None,
    ) -> None:
        """Initialize the Yeelight light."""
        super().__init__(device, entry)

        self.config = device.config

        self._color_temp: int | None = None
        self._effect = None

        model_specs = self._bulb.get_model_specs()
        self._attr_max_color_temp_kelvin = model_specs["color_temp"]["max"]
        self._attr_min_color_temp_kelvin = model_specs["color_temp"]["min"]

        self._light_type = LightType.Main

        if custom_effects:
            self._custom_effects = custom_effects
        else:
            self._custom_effects = {}

        self._unexpected_state_check = None

    @callback
    def async_state_changed(self) -> None:
        """Call when the device changes state."""
        if not self._device.available:
            self._async_cancel_pending_state_check()
        self.async_write_ha_state()

    async def async_added_to_hass(self) -> None:
        """Handle entity which will be added."""
        self.async_on_remove(
            async_dispatcher_connect(
                self.hass,
                DATA_UPDATED.format(self._device.host),
                self.async_state_changed,
            )
        )
        await super().async_added_to_hass()

    @property
    def effect_list(self) -> list[str]:
        """Return the list of supported effects."""
        return self._predefined_effects + self.custom_effects_names

    @property
    def color_temp_kelvin(self) -> int | None:
        """Return the color temperature value in Kelvin."""
        if temp_in_k := self._get_property("ct"):
            self._color_temp = int(temp_in_k)
        return self._color_temp

    @property
    def is_on(self) -> bool:
        """Return true if device is on."""
        return self._get_property(self._power_property) == "on"

    @property
    def brightness(self) -> int:
        """Return the brightness of this light between 1..255."""
        # Always use "bright" as property name in music mode
        # Since music mode states are only caches in upstream library
        # and the cache key is always "bright" for brightness
        brightness_property = (
            "bright" if self._bulb.music_mode else self._brightness_property
        )
        brightness = self._get_property(brightness_property) or 0
        return round(255 * (int(brightness) / 100))

    @property
    def custom_effects(self) -> dict[str, dict[str, Any]]:
        """Return dict with custom effects."""
        return self._custom_effects

    @property
    def custom_effects_names(self) -> list[str]:
        """Return list with custom effects names."""
        return list(self.custom_effects)

    @property
    def light_type(self) -> LightType:
        """Return light type."""
        return self._light_type

    @property
    def hs_color(self) -> tuple[float, float] | None:
        """Return the color property."""
        hue = self._get_property("hue")
        sat = self._get_property("sat")
        if hue is None or sat is None:
            return None

        return (int(hue), int(sat))

    @property
    def rgb_color(self) -> tuple[int, int, int] | None:
        """Return the color property."""
        if (rgb := self._get_property("rgb")) is None:
            return None

        rgb = int(rgb)
        blue = rgb & 0xFF
        green = (rgb >> 8) & 0xFF
        red = (rgb >> 16) & 0xFF

        return (red, green, blue)

    @property
    def effect(self) -> str | None:
        """Return the current effect."""
        return self._effect if self.device.is_color_flow_enabled else None

    @property
    def _bulb(self) -> AsyncBulb:
        return self.device.bulb

    @property
    def _properties(self) -> dict:
        return self._bulb.last_properties if self._bulb else {}

    def _get_property(self, prop: str, default=None):
        return self._properties.get(prop, default)

    @property
    def _brightness_property(self) -> str:
        return "bright"

    @property
    def _power_property(self) -> str:
        return "power"

    @property
    def _turn_on_power_mode(self) -> PowerMode:
        return PowerMode.LAST

    @property
    def _predefined_effects(self) -> list[str]:
        return YEELIGHT_MONO_EFFECT_LIST

    @property
    def extra_state_attributes(self) -> dict[str, Any]:
        """Return the device specific state attributes."""
        attributes = {
            "flowing": self.device.is_color_flow_enabled,
            "music_mode": self._bulb.music_mode,
        }

        if self.device.is_nightlight_supported:
            attributes["night_light"] = self.device.is_nightlight_enabled

        return attributes

    @property
    def device(self) -> YeelightDevice:
        """Return yeelight device."""
        return self._device

    async def async_update(self) -> None:
        """Update light properties."""
        await self.device.async_update(True)

    async def async_set_music_mode(self, music_mode) -> None:
        """Set the music mode on or off."""
        try:
            await self._async_set_music_mode(music_mode)
        except AssertionError as ex:
            _LOGGER.error("Unable to turn on music mode, consider disabling it: %s", ex)

    @_async_cmd
    async def _async_set_music_mode(self, music_mode) -> None:
        """Set the music mode on or off wrapped with _async_cmd."""
        bulb = self._bulb
        if music_mode:
            await bulb.async_start_music()
        else:
            await bulb.async_stop_music()

    @_async_cmd
    async def async_set_brightness(self, brightness, duration) -> None:
        """Set bulb brightness."""
        if not brightness:
            return
        if (
            math.floor(self.brightness) == math.floor(brightness)
            and self._bulb.model not in MODELS_WITH_DELAYED_ON_TRANSITION
        ):
            _LOGGER.debug("brightness already set to: %s", brightness)
            # Already set, and since we get pushed updates
            # we avoid setting it again to ensure we do not
            # hit the rate limit
            return

        _LOGGER.debug("Setting brightness: %s", brightness)
        await self._bulb.async_set_brightness(
            brightness / 255 * 100, duration=duration, light_type=self.light_type
        )

    @_async_cmd
    async def async_set_hs(self, hs_color, duration) -> None:
        """Set bulb's color."""
        if (
            not hs_color
            or not self.supported_color_modes
            or ColorMode.HS not in self.supported_color_modes
        ):
            return
        if (
            not self.device.is_color_flow_enabled
            and self.color_mode == ColorMode.HS
            and self.hs_color == hs_color
        ):
            _LOGGER.debug("HS already set to: %s", hs_color)
            # Already set, and since we get pushed updates
            # we avoid setting it again to ensure we do not
            # hit the rate limit
            return

        _LOGGER.debug("Setting HS: %s", hs_color)
        await self._bulb.async_set_hsv(
            hs_color[0], hs_color[1], duration=duration, light_type=self.light_type
        )

    @_async_cmd
    async def async_set_rgb(self, rgb, duration) -> None:
        """Set bulb's color."""
        if (
            not rgb
            or not self.supported_color_modes
            or ColorMode.RGB not in self.supported_color_modes
        ):
            return
        if (
            not self.device.is_color_flow_enabled
            and self.color_mode == ColorMode.RGB
            and self.rgb_color == rgb
        ):
            _LOGGER.debug("RGB already set to: %s", rgb)
            # Already set, and since we get pushed updates
            # we avoid setting it again to ensure we do not
            # hit the rate limit
            return

        _LOGGER.debug("Setting RGB: %s", rgb)
        await self._bulb.async_set_rgb(
            *rgb, duration=duration, light_type=self.light_type
        )

    @_async_cmd
    async def async_set_colortemp(self, temp_in_k, duration) -> None:
        """Set bulb's color temperature."""
        if (
            not temp_in_k
            or not self.supported_color_modes
            or ColorMode.COLOR_TEMP not in self.supported_color_modes
        ):
            return

        if (
            not self.device.is_color_flow_enabled
            and self.color_mode == ColorMode.COLOR_TEMP
            and self.color_temp_kelvin == temp_in_k
        ):
            _LOGGER.debug("Color temp already set to: %s", temp_in_k)
            # Already set, and since we get pushed updates
            # we avoid setting it again to ensure we do not
            # hit the rate limit
            return

        await self._bulb.async_set_color_temp(
            temp_in_k, duration=duration, light_type=self.light_type
        )

    @_async_cmd
    async def async_set_default(self) -> None:
        """Set current options as default."""
        await self._bulb.async_set_default()

    @_async_cmd
    async def async_set_flash(self, flash) -> None:
        """Activate flash."""
        if not flash:
            return
        if int(self._get_property("color_mode")) != 1 or not self.hs_color:
            _LOGGER.error("Flash supported currently only in RGB mode")
            return

        transition = int(self.config[CONF_TRANSITION])
        if flash == FLASH_LONG:
            count = 1
            duration = transition * 5
        if flash == FLASH_SHORT:
            count = 1
            duration = transition * 2

        red, green, blue = color_util.color_hs_to_RGB(*self.hs_color)

        transitions = []
        transitions.append(RGBTransition(255, 0, 0, brightness=10, duration=duration))
        transitions.append(SleepTransition(duration=transition))
        transitions.append(
            RGBTransition(
                red, green, blue, brightness=self.brightness, duration=duration
            )
        )

        flow = Flow(count=count, transitions=transitions)
        await self._bulb.async_start_flow(flow, light_type=self.light_type)

    @_async_cmd
    async def async_set_effect(self, effect) -> None:
        """Activate effect."""
        if not effect:
            return

        if effect == EFFECT_STOP:
            await self._bulb.async_stop_flow(light_type=self.light_type)
            return

        if effect in self.custom_effects_names:
            flow = Flow(**self.custom_effects[effect])
        elif effect in EFFECTS_MAP:
            flow = EFFECTS_MAP[effect]()
        elif effect == EFFECT_FAST_RANDOM_LOOP:
            flow = flows.random_loop(duration=250)
        elif effect == EFFECT_WHATSAPP:
            flow = flows.pulse(37, 211, 102, count=2)
        elif effect == EFFECT_FACEBOOK:
            flow = flows.pulse(59, 89, 152, count=2)
        elif effect == EFFECT_TWITTER:
            flow = flows.pulse(0, 172, 237, count=2)
        else:
            return

        await self._bulb.async_start_flow(flow, light_type=self.light_type)
        self._effect = effect

    @_async_cmd
    async def _async_turn_on(self, duration) -> None:
        """Turn on the bulb for with a transition duration wrapped with _async_cmd."""
        await self._bulb.async_turn_on(
            duration=duration,
            light_type=self.light_type,
            power_mode=self._turn_on_power_mode,
        )

    async def async_turn_on(self, **kwargs: Any) -> None:
        """Turn the bulb on."""
        brightness = kwargs.get(ATTR_BRIGHTNESS)
        colortemp = kwargs.get(ATTR_COLOR_TEMP_KELVIN)
        hs_color = kwargs.get(ATTR_HS_COLOR)
        rgb = kwargs.get(ATTR_RGB_COLOR)
        flash = kwargs.get(ATTR_FLASH)
        effect = kwargs.get(ATTR_EFFECT)

        duration = int(self.config[CONF_TRANSITION])  # in ms
        if ATTR_TRANSITION in kwargs:  # passed kwarg overrides config
            duration = int(kwargs[ATTR_TRANSITION] * 1000)  # kwarg in s

        if not self.is_on:
            await self._async_turn_on(duration)

        if self.config[CONF_MODE_MUSIC] and not self._bulb.music_mode:
            await self.async_set_music_mode(True)

        await self.async_set_hs(hs_color, duration)
        await self.async_set_rgb(rgb, duration)
        await self.async_set_colortemp(colortemp, duration)
        await self.async_set_brightness(brightness, duration)
        await self.async_set_flash(flash)
        await self.async_set_effect(effect)

        # save the current state if we had a manual change.
        if self.config[CONF_SAVE_ON_CHANGE] and (brightness or colortemp or rgb):
            await self.async_set_default()

        self._async_schedule_state_check(True)

    @callback
    def _async_cancel_pending_state_check(self):
        """Cancel a pending state check."""
        if self._unexpected_state_check:
            self._unexpected_state_check()
            self._unexpected_state_check = None

    @callback
    def _async_schedule_state_check(self, expected_power_state):
        """Schedule a poll if the change failed to get pushed back to us.

        Some devices (mainly nightlights) will not send back the on state
        so we need to force a refresh.
        """
        self._async_cancel_pending_state_check()

        async def _async_update_if_state_unexpected(*_):
            self._unexpected_state_check = None
            if self.is_on != expected_power_state:
                await self.device.async_update(True)

        self._unexpected_state_check = async_call_later(
            self.hass, POWER_STATE_CHANGE_TIME, _async_update_if_state_unexpected
        )

    @_async_cmd
    async def _async_turn_off(self, duration) -> None:
        """Turn off with a given transition duration wrapped with _async_cmd."""
        await self._bulb.async_turn_off(duration=duration, light_type=self.light_type)

    async def async_turn_off(self, **kwargs: Any) -> None:
        """Turn off."""
        if not self.is_on:
            return

        duration = int(self.config[CONF_TRANSITION])  # in ms
        if ATTR_TRANSITION in kwargs:  # passed kwarg overrides config
            duration = int(kwargs[ATTR_TRANSITION] * 1000)  # kwarg in s

        await self._async_turn_off(duration)
        self._async_schedule_state_check(False)

    @_async_cmd
    async def async_set_mode(self, mode: str):
        """Set a power mode."""
        await self._bulb.async_set_power_mode(PowerMode[mode.upper()])
        self._async_schedule_state_check(True)

    @_async_cmd
    async def async_start_flow(self, transitions, count=0, action=ACTION_RECOVER):
        """Start flow."""
        flow = Flow(count=count, action=Flow.actions[action], transitions=transitions)
        await self._bulb.async_start_flow(flow, light_type=self.light_type)

    @_async_cmd
    async def async_set_scene(self, scene_class, *args):
        """Set the light directly to the specified state.

        If the light is off, it will first be turned on.
        """
        await self._bulb.async_set_scene(scene_class, *args)


class YeelightGenericLight(YeelightBaseLight):
    """Representation of a generic Yeelight."""

    _attr_name = None


class YeelightColorLightSupport(YeelightBaseLight):
    """Representation of a Color Yeelight light support."""

    _attr_supported_color_modes = {ColorMode.COLOR_TEMP, ColorMode.HS, ColorMode.RGB}

    @property
    def color_mode(self) -> ColorMode:
        """Return the color mode."""
        color_mode = int(self._get_property("color_mode"))
        if color_mode == 1:  # RGB
            return ColorMode.RGB
        if color_mode == 2:  # color temperature
            return ColorMode.COLOR_TEMP
        if color_mode == 3:  # hsv
            return ColorMode.HS
        _LOGGER.debug("Light reported unknown color mode: %s", color_mode)
        return ColorMode.UNKNOWN

    @property
    def _predefined_effects(self) -> list[str]:
        return YEELIGHT_COLOR_EFFECT_LIST


class YeelightWhiteTempLightSupport(YeelightBaseLight):
    """Representation of a White temp Yeelight light."""

    _attr_name = None
    _attr_color_mode = ColorMode.COLOR_TEMP
    _attr_supported_color_modes = {ColorMode.COLOR_TEMP}

    @property
    def _predefined_effects(self) -> list[str]:
        return YEELIGHT_TEMP_ONLY_EFFECT_LIST


class YeelightNightLightSupport:
    """Representation of a Yeelight nightlight support."""

    @property
    def _turn_on_power_mode(self) -> PowerMode:
        return PowerMode.NORMAL


class YeelightWithoutNightlightSwitchMixIn(YeelightBaseLight):
    """A mix-in for yeelights without a nightlight switch."""

    @property
    def _brightness_property(self) -> str:
        # If the nightlight is not active, we do not
        # want to "current_brightness" since it will check
        # "bg_power" and main light could still be on
        if self.device.is_nightlight_enabled:
            return "nl_br"
        return super()._brightness_property

    @property
    def color_temp_kelvin(self) -> int | None:
        """Return the color temperature value in Kelvin."""
        if self.device.is_nightlight_enabled:
            # Enabling the nightlight locks the colortemp to max
            return self.min_color_temp_kelvin
        return super().color_temp_kelvin


class YeelightColorLightWithoutNightlightSwitch(
    YeelightColorLightSupport, YeelightWithoutNightlightSwitchMixIn
):
    """Representation of a Color Yeelight light."""


class YeelightColorLightWithoutNightlightSwitchLight(
    YeelightColorLightWithoutNightlightSwitch
):
    """Representation of a Color Yeelight light."""

    _attr_name = None


class YeelightColorLightWithNightlightSwitch(
    YeelightNightLightSupport, YeelightColorLightSupport, YeelightBaseLight
):
    """Representation of a Yeelight with rgb support and nightlight.

    It represents case when nightlight switch is set to light.
    """

    _attr_name = None

    @property
    def is_on(self) -> bool:
        """Return true if device is on."""
        return super().is_on and not self.device.is_nightlight_enabled


class YeelightWhiteTempWithoutNightlightSwitch(
    YeelightWhiteTempLightSupport, YeelightWithoutNightlightSwitchMixIn
):
    """White temp light, when nightlight switch is not set to light."""

    _attr_name = None


class YeelightWithNightLight(
    YeelightNightLightSupport, YeelightWhiteTempLightSupport, YeelightBaseLight
):
    """Representation of a Yeelight with temp only support and nightlight.

    It represents case when nightlight switch is set to light.
    """

    _attr_name = None

    @property
    def is_on(self) -> bool:
        """Return true if device is on."""
        return super().is_on and not self.device.is_nightlight_enabled


class YeelightNightLightMode(YeelightBaseLight):
    """Representation of a Yeelight when in nightlight mode."""

    _attr_color_mode = ColorMode.BRIGHTNESS
    _attr_supported_color_modes = {ColorMode.BRIGHTNESS}
    _attr_translation_key = "nightlight"

    @property
    def unique_id(self) -> str:
        """Return a unique ID."""
        unique = super().unique_id
        return f"{unique}-nightlight"

    @property
    def is_on(self) -> bool:
        """Return true if device is on."""
        return super().is_on and self.device.is_nightlight_enabled

    @property
    def _brightness_property(self) -> str:
        return "nl_br"

    @property
    def _turn_on_power_mode(self) -> PowerMode:
        return PowerMode.MOONLIGHT

    @property
    def supported_features(self) -> LightEntityFeature:
        """Flag no supported features."""
        return LightEntityFeature(0)


class YeelightNightLightModeWithAmbientSupport(YeelightNightLightMode):
    """Representation of a Yeelight, with ambient support, when in nightlight mode."""

    @property
    def _power_property(self) -> str:
        return "main_power"


class YeelightNightLightModeWithoutBrightnessControl(YeelightNightLightMode):
    """Representation of a Yeelight, when in nightlight mode.

    It represents case when nightlight mode brightness control is not supported.
    """

    _attr_color_mode = ColorMode.ONOFF
    _attr_supported_color_modes = {ColorMode.ONOFF}


class YeelightWithAmbientWithoutNightlight(YeelightWhiteTempWithoutNightlightSwitch):
    """Representation of a Yeelight which has ambilight support.

    And nightlight switch type is none.
    """

    _attr_name = None

    @property
    def _power_property(self) -> str:
        return "main_power"


class YeelightWithAmbientAndNightlight(YeelightWithNightLight):
    """Representation of a Yeelight which has ambilight support.

    And nightlight switch type is set to light.
    """

    _attr_name = None

    @property
    def _power_property(self) -> str:
        return "main_power"


class YeelightAmbientLight(YeelightColorLightWithoutNightlightSwitch):
    """Representation of a Yeelight ambient light."""

    _attr_translation_key = "ambilight"

    PROPERTIES_MAPPING = {"color_mode": "bg_lmode"}

    def __init__(self, *args, **kwargs):
        """Initialize the Yeelight Ambient light."""
        super().__init__(*args, **kwargs)
        self._attr_max_color_temp_kelvin = 6500
        self._attr_min_color_temp_kelvin = 1700

        self._light_type = LightType.Ambient

    @property
    def unique_id(self) -> str:
        """Return a unique ID."""
        unique = super().unique_id
        return f"{unique}-ambilight"

    @property
    def _brightness_property(self) -> str:
        return "bright"

    def _get_property(self, prop: str, default=None):
        if not (bg_prop := self.PROPERTIES_MAPPING.get(prop)):
            bg_prop = f"bg_{prop}"

        return super()._get_property(bg_prop, default)
