"""Support for Z-Wave lights."""

from __future__ import annotations

from typing import TYPE_CHECKING, Any, cast

from zwave_js_server.client import Client as ZwaveClient
from zwave_js_server.const import (
    TARGET_VALUE_PROPERTY,
    TRANSITION_DURATION_OPTION,
    CommandClass,
)
from zwave_js_server.const.command_class.color_switch import (
    COLOR_SWITCH_COMBINED_AMBER,
    COLOR_SWITCH_COMBINED_BLUE,
    COLOR_SWITCH_COMBINED_COLD_WHITE,
    COLOR_SWITCH_COMBINED_CYAN,
    COLOR_SWITCH_COMBINED_GREEN,
    COLOR_SWITCH_COMBINED_PURPLE,
    COLOR_SWITCH_COMBINED_RED,
    COLOR_SWITCH_COMBINED_WARM_WHITE,
    CURRENT_COLOR_PROPERTY,
    TARGET_COLOR_PROPERTY,
    ColorComponent,
)
from zwave_js_server.const.command_class.multilevel_switch import SET_TO_PREVIOUS_VALUE
from zwave_js_server.model.driver import Driver
from zwave_js_server.model.value import Value

from homeassistant.components.light import (
    ATTR_BRIGHTNESS,
    ATTR_COLOR_TEMP_KELVIN,
    ATTR_HS_COLOR,
    ATTR_RGBW_COLOR,
    ATTR_TRANSITION,
    DOMAIN as LIGHT_DOMAIN,
    ColorMode,
    LightEntity,
    LightEntityFeature,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util import color as color_util

from .const import DATA_CLIENT, DOMAIN
from .discovery import ZwaveDiscoveryInfo
from .entity import ZWaveBaseEntity

PARALLEL_UPDATES = 0

MULTI_COLOR_MAP = {
    ColorComponent.WARM_WHITE: COLOR_SWITCH_COMBINED_WARM_WHITE,
    ColorComponent.COLD_WHITE: COLOR_SWITCH_COMBINED_COLD_WHITE,
    ColorComponent.RED: COLOR_SWITCH_COMBINED_RED,
    ColorComponent.GREEN: COLOR_SWITCH_COMBINED_GREEN,
    ColorComponent.BLUE: COLOR_SWITCH_COMBINED_BLUE,
    ColorComponent.AMBER: COLOR_SWITCH_COMBINED_AMBER,
    ColorComponent.CYAN: COLOR_SWITCH_COMBINED_CYAN,
    ColorComponent.PURPLE: COLOR_SWITCH_COMBINED_PURPLE,
}
MIN_MIREDS = 153  # 6500K as a safe default
MAX_MIREDS = 370  # 2700K as a safe default


async def async_setup_entry(
    hass: HomeAssistant,
    config_entry: ConfigEntry,
    async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
    """Set up Z-Wave Light from Config Entry."""
    client: ZwaveClient = config_entry.runtime_data[DATA_CLIENT]

    @callback
    def async_add_light(info: ZwaveDiscoveryInfo) -> None:
        """Add Z-Wave Light."""
        driver = client.driver
        assert driver is not None  # Driver is ready before platforms are loaded.

        if info.platform_hint == "color_onoff":
            async_add_entities([ZwaveColorOnOffLight(config_entry, driver, info)])
        else:
            async_add_entities([ZwaveLight(config_entry, driver, info)])

    config_entry.async_on_unload(
        async_dispatcher_connect(
            hass,
            f"{DOMAIN}_{config_entry.entry_id}_add_{LIGHT_DOMAIN}",
            async_add_light,
        )
    )


def byte_to_zwave_brightness(value: int) -> int:
    """Convert brightness in 0-255 scale to 0-99 scale.

    `value` -- (int) Brightness byte value from 0-255.
    """
    if value > 0:
        return max(1, round((value / 255) * 99))
    return 0


class ZwaveLight(ZWaveBaseEntity, LightEntity):
    """Representation of a Z-Wave light."""

    _attr_min_color_temp_kelvin = 2700  # 370 mireds as a safe default
    _attr_max_color_temp_kelvin = 6500  # 153 mireds as a safe default

    def __init__(
        self, config_entry: ConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo
    ) -> None:
        """Initialize the light."""
        super().__init__(config_entry, driver, info)
        self._supports_color = False
        self._supports_rgbw = False
        self._supports_color_temp = False
        self._supports_dimming = False
        self._color_mode: str | None = None
        self._hs_color: tuple[float, float] | None = None
        self._rgbw_color: tuple[int, int, int, int] | None = None
        self._color_temp: int | None = None
        self._warm_white = self.get_zwave_value(
            TARGET_COLOR_PROPERTY,
            CommandClass.SWITCH_COLOR,
            value_property_key=ColorComponent.WARM_WHITE,
        )
        self._cold_white = self.get_zwave_value(
            TARGET_COLOR_PROPERTY,
            CommandClass.SWITCH_COLOR,
            value_property_key=ColorComponent.COLD_WHITE,
        )
        self._supported_color_modes: set[ColorMode] = set()

        self._target_brightness: Value | None = None

        # get additional (optional) values and set features
        if self.info.primary_value.command_class == CommandClass.SWITCH_BINARY:
            # This light can not be dimmed separately from the color channels
            self._target_brightness = self.get_zwave_value(
                TARGET_VALUE_PROPERTY,
                CommandClass.SWITCH_BINARY,
                add_to_watched_value_ids=False,
            )
            self._supports_dimming = False
        elif self.info.primary_value.command_class == CommandClass.SWITCH_MULTILEVEL:
            # This light can be dimmed separately from the color channels
            self._target_brightness = self.get_zwave_value(
                TARGET_VALUE_PROPERTY,
                CommandClass.SWITCH_MULTILEVEL,
                add_to_watched_value_ids=False,
            )
            self._supports_dimming = True
        elif self.info.primary_value.command_class == CommandClass.BASIC:
            # If the command class is Basic, we must generate a name that includes
            # the command class name to avoid ambiguity
            self._attr_name = self.generate_name(
                include_value_name=True, alternate_value_name="Basic"
            )
            self._target_brightness = self.get_zwave_value(
                TARGET_VALUE_PROPERTY,
                CommandClass.BASIC,
                add_to_watched_value_ids=False,
            )
            self._supports_dimming = True

        self._current_color = self.get_zwave_value(
            CURRENT_COLOR_PROPERTY,
            CommandClass.SWITCH_COLOR,
            value_property_key=None,
        )
        self._target_color = self.get_zwave_value(
            TARGET_COLOR_PROPERTY,
            CommandClass.SWITCH_COLOR,
            add_to_watched_value_ids=False,
        )

        self._calculate_color_support()
        if self._supports_rgbw:
            self._supported_color_modes.add(ColorMode.RGBW)
        elif self._supports_color:
            self._supported_color_modes.add(ColorMode.HS)
        if self._supports_color_temp:
            self._supported_color_modes.add(ColorMode.COLOR_TEMP)
        if not self._supported_color_modes:
            self._supported_color_modes.add(ColorMode.BRIGHTNESS)
        self._calculate_color_values()

        # Entity class attributes
        self.supports_brightness_transition = bool(
            self._target_brightness is not None
            and TRANSITION_DURATION_OPTION
            in self._target_brightness.metadata.value_change_options
        )
        self.supports_color_transition = bool(
            self._target_color is not None
            and TRANSITION_DURATION_OPTION
            in self._target_color.metadata.value_change_options
        )

        if self.supports_brightness_transition or self.supports_color_transition:
            self._attr_supported_features |= LightEntityFeature.TRANSITION

        self._set_optimistic_state: bool = False

    @callback
    def on_value_update(self) -> None:
        """Call when a watched value is added or updated."""
        self._calculate_color_values()

    @property
    def brightness(self) -> int | None:
        """Return the brightness of this light between 0..255.

        Z-Wave multilevel switches use a range of [0, 99] to control brightness.
        """
        if self.info.primary_value.value is None:
            return None
        return round((cast(int, self.info.primary_value.value) / 99) * 255)

    @property
    def color_mode(self) -> str | None:
        """Return the color mode of the light."""
        return self._color_mode

    @property
    def is_on(self) -> bool | None:
        """Return true if device is on (brightness above 0)."""
        if self._set_optimistic_state:
            self._set_optimistic_state = False
            return True
        brightness = self.brightness
        return brightness > 0 if brightness is not None else None

    @property
    def hs_color(self) -> tuple[float, float] | None:
        """Return the hs color."""
        return self._hs_color

    @property
    def rgbw_color(self) -> tuple[int, int, int, int] | None:
        """Return the RGBW color."""
        return self._rgbw_color

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

    @property
    def supported_color_modes(self) -> set[ColorMode] | None:
        """Flag supported features."""
        return self._supported_color_modes

    async def async_turn_on(self, **kwargs: Any) -> None:
        """Turn the device on."""

        transition = kwargs.get(ATTR_TRANSITION)
        brightness = kwargs.get(ATTR_BRIGHTNESS)

        hs_color = kwargs.get(ATTR_HS_COLOR)
        color_temp_k = kwargs.get(ATTR_COLOR_TEMP_KELVIN)
        rgbw = kwargs.get(ATTR_RGBW_COLOR)

        new_colors = self._get_new_colors(hs_color, color_temp_k, rgbw)
        if new_colors is not None:
            await self._async_set_colors(new_colors, transition)

        # set brightness (or turn on if dimming is not supported)
        await self._async_set_brightness(brightness, transition)

    async def async_turn_off(self, **kwargs: Any) -> None:
        """Turn the light off."""
        await self._async_set_brightness(0, kwargs.get(ATTR_TRANSITION))

    def _get_new_colors(
        self,
        hs_color: tuple[float, float] | None,
        color_temp_k: int | None,
        rgbw: tuple[int, int, int, int] | None,
        brightness_scale: float | None = None,
    ) -> dict[ColorComponent, int] | None:
        """Determine the new color dict to set."""

        # RGB/HS color
        if hs_color is not None and self._supports_color:
            red, green, blue = color_util.color_hs_to_RGB(*hs_color)
            if brightness_scale is not None:
                red = round(red * brightness_scale)
                green = round(green * brightness_scale)
                blue = round(blue * brightness_scale)
            colors = {
                ColorComponent.RED: red,
                ColorComponent.GREEN: green,
                ColorComponent.BLUE: blue,
            }
            if self._supports_color_temp:
                # turn of white leds when setting rgb
                colors[ColorComponent.WARM_WHITE] = 0
                colors[ColorComponent.COLD_WHITE] = 0
            return colors

        # Color temperature
        if color_temp_k is not None and self._supports_color_temp:
            # Limit color temp to min/max values
            color_temp = color_util.color_temperature_kelvin_to_mired(color_temp_k)
            cold = max(
                0,
                min(
                    255,
                    round((MAX_MIREDS - color_temp) / (MAX_MIREDS - MIN_MIREDS) * 255),
                ),
            )
            warm = 255 - cold
            colors = {
                ColorComponent.WARM_WHITE: warm,
                ColorComponent.COLD_WHITE: cold,
            }
            if self._supports_color:
                # turn off color leds when setting color temperature
                colors[ColorComponent.RED] = 0
                colors[ColorComponent.GREEN] = 0
                colors[ColorComponent.BLUE] = 0
            return colors

        # RGBW
        if rgbw is not None and self._supports_rgbw:
            rgbw_channels = {
                ColorComponent.RED: rgbw[0],
                ColorComponent.GREEN: rgbw[1],
                ColorComponent.BLUE: rgbw[2],
            }
            if self._warm_white:
                rgbw_channels[ColorComponent.WARM_WHITE] = rgbw[3]

            if self._cold_white:
                rgbw_channels[ColorComponent.COLD_WHITE] = rgbw[3]

            return rgbw_channels

        return None

    async def _async_set_colors(
        self,
        colors: dict[ColorComponent, int],
        transition: float | None = None,
    ) -> None:
        """Set (multiple) defined colors to given value(s)."""
        # prefer the (new) combined color property
        # https://github.com/zwave-js/node-zwave-js/pull/1782
        # Setting colors is only done if there's a target color value.
        combined_color_val = cast(
            Value,
            self.get_zwave_value(
                "targetColor",
                CommandClass.SWITCH_COLOR,
                value_property_key=None,
            ),
        )
        zwave_transition = None

        if self.supports_color_transition:
            if transition is not None:
                zwave_transition = {TRANSITION_DURATION_OPTION: f"{int(transition)}s"}
            else:
                zwave_transition = {TRANSITION_DURATION_OPTION: "default"}

        colors_dict = {}
        for color, value in colors.items():
            color_name = MULTI_COLOR_MAP[color]
            colors_dict[color_name] = value
        # set updated color object
        await self._async_set_value(combined_color_val, colors_dict, zwave_transition)

    async def _async_set_brightness(
        self, brightness: int | None, transition: float | None = None
    ) -> None:
        """Set new brightness to light."""
        # If we have no target brightness value, there is nothing to do
        if not self._target_brightness:
            return
        if brightness is None:
            zwave_brightness = SET_TO_PREVIOUS_VALUE
        else:
            # Zwave multilevel switches use a range of [0, 99] to control brightness.
            zwave_brightness = byte_to_zwave_brightness(brightness)

        # set transition value before sending new brightness
        zwave_transition = None
        if self.supports_brightness_transition:
            if transition is not None:
                zwave_transition = {TRANSITION_DURATION_OPTION: f"{int(transition)}s"}
            else:
                zwave_transition = {TRANSITION_DURATION_OPTION: "default"}

        # setting a value requires setting targetValue
        if self._supports_dimming:
            await self._async_set_value(
                self._target_brightness, zwave_brightness, zwave_transition
            )
        else:
            await self._async_set_value(
                self._target_brightness, zwave_brightness > 0, zwave_transition
            )
        # We do an optimistic state update when setting to a previous value
        # to avoid waiting for the value to be updated from the device which is
        # typically delayed and causes a confusing UX.
        if (
            zwave_brightness == SET_TO_PREVIOUS_VALUE
            and self.info.primary_value.command_class
            in (CommandClass.BASIC, CommandClass.SWITCH_MULTILEVEL)
        ):
            self._set_optimistic_state = True
            self.async_write_ha_state()

    @callback
    def _get_color_values(self) -> tuple[Value | None, ...]:
        """Get light colors."""
        # NOTE: We lookup all values here (instead of relying on the multicolor one)
        # to find out what colors are supported
        # as this is a simple lookup by key, this not heavy
        red_val = self.get_zwave_value(
            CURRENT_COLOR_PROPERTY,
            CommandClass.SWITCH_COLOR,
            value_property_key=ColorComponent.RED.value,
        )
        green_val = self.get_zwave_value(
            CURRENT_COLOR_PROPERTY,
            CommandClass.SWITCH_COLOR,
            value_property_key=ColorComponent.GREEN.value,
        )
        blue_val = self.get_zwave_value(
            CURRENT_COLOR_PROPERTY,
            CommandClass.SWITCH_COLOR,
            value_property_key=ColorComponent.BLUE.value,
        )
        ww_val = self.get_zwave_value(
            CURRENT_COLOR_PROPERTY,
            CommandClass.SWITCH_COLOR,
            value_property_key=ColorComponent.WARM_WHITE.value,
        )
        cw_val = self.get_zwave_value(
            CURRENT_COLOR_PROPERTY,
            CommandClass.SWITCH_COLOR,
            value_property_key=ColorComponent.COLD_WHITE.value,
        )
        return (red_val, green_val, blue_val, ww_val, cw_val)

    @callback
    def _calculate_color_support(self) -> None:
        """Calculate light colors."""
        (red, green, blue, warm_white, cool_white) = self._get_color_values()
        # RGB support
        if red and green and blue:
            self._supports_color = True
        # color temperature support
        if warm_white and cool_white:
            self._supports_color_temp = True
        # only one white channel (warm white or cool white) = rgbw support
        elif (red and green and blue and warm_white) or cool_white:
            self._supports_rgbw = True

    @callback
    def _calculate_color_values(self) -> None:
        """Calculate light colors."""
        (red_val, green_val, blue_val, ww_val, cw_val) = self._get_color_values()

        if self._current_color and isinstance(self._current_color.value, dict):
            multi_color = self._current_color.value
        else:
            multi_color = {}

        # Default: Brightness (no color) or Unknown
        if self.supported_color_modes == {ColorMode.BRIGHTNESS}:
            self._color_mode = ColorMode.BRIGHTNESS
        else:
            self._color_mode = ColorMode.UNKNOWN

        # RGB support
        if red_val and green_val and blue_val:
            # prefer values from the multicolor property
            red = multi_color.get(COLOR_SWITCH_COMBINED_RED, red_val.value)
            green = multi_color.get(COLOR_SWITCH_COMBINED_GREEN, green_val.value)
            blue = multi_color.get(COLOR_SWITCH_COMBINED_BLUE, blue_val.value)
            if red is not None and green is not None and blue is not None:
                # convert to HS
                self._hs_color = color_util.color_RGB_to_hs(red, green, blue)
                # Light supports color, set color mode to hs
                self._color_mode = ColorMode.HS

        # color temperature support
        if ww_val and cw_val:
            warm_white = multi_color.get(COLOR_SWITCH_COMBINED_WARM_WHITE, ww_val.value)
            cold_white = multi_color.get(COLOR_SWITCH_COMBINED_COLD_WHITE, cw_val.value)
            # Calculate color temps based on whites
            if cold_white or warm_white:
                self._color_temp = color_util.color_temperature_mired_to_kelvin(
                    MAX_MIREDS
                    - ((cast(int, cold_white) / 255) * (MAX_MIREDS - MIN_MIREDS))
                )
                # White channels turned on, set color mode to color_temp
                self._color_mode = ColorMode.COLOR_TEMP
            else:
                self._color_temp = None
        # only one white channel (warm white) = rgbw support
        elif red_val and green_val and blue_val and ww_val:
            white = multi_color.get(COLOR_SWITCH_COMBINED_WARM_WHITE, ww_val.value)
            if TYPE_CHECKING:
                assert (
                    red is not None
                    and green is not None
                    and blue is not None
                    and white is not None
                )
            self._rgbw_color = (red, green, blue, white)
            # Light supports rgbw, set color mode to rgbw
            self._color_mode = ColorMode.RGBW
        # only one white channel (cool white) = rgbw support
        elif cw_val:
            self._supports_rgbw = True
            white = multi_color.get(COLOR_SWITCH_COMBINED_COLD_WHITE, cw_val.value)
            if TYPE_CHECKING:
                assert (
                    red is not None
                    and green is not None
                    and blue is not None
                    and white is not None
                )
            self._rgbw_color = (red, green, blue, white)
            # Light supports rgbw, set color mode to rgbw
            self._color_mode = ColorMode.RGBW


class ZwaveColorOnOffLight(ZwaveLight):
    """Representation of a colored Z-Wave light with an optional binary switch to turn on/off.

    Dimming for RGB lights is realized by scaling the color channels.
    """

    def __init__(
        self, config_entry: ConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo
    ) -> None:
        """Initialize the light."""
        super().__init__(config_entry, driver, info)

        self._last_on_color: dict[ColorComponent, int] | None = None
        self._last_brightness: int | None = None

    @property
    def brightness(self) -> int | None:
        """Return the brightness of this light between 0..255.

        Z-Wave multilevel switches use a range of [0, 99] to control brightness.
        """
        if self.info.primary_value.value is None:
            return None
        if self._target_brightness and self.info.primary_value.value is False:
            # Binary switch exists and is turned off
            return 0

        # Brightness is encoded in the color channels by scaling them lower than 255
        color_values = [
            v.value
            for v in self._get_color_values()
            if v is not None and v.value is not None
        ]
        return max(color_values) if color_values else 0

    async def async_turn_on(self, **kwargs: Any) -> None:
        """Turn the device on."""

        if (
            kwargs.get(ATTR_RGBW_COLOR) is not None
            or kwargs.get(ATTR_COLOR_TEMP_KELVIN) is not None
        ):
            # RGBW and color temp are not supported in this mode,
            # delegate to the parent class
            await super().async_turn_on(**kwargs)
            return

        transition = kwargs.get(ATTR_TRANSITION)
        brightness = kwargs.get(ATTR_BRIGHTNESS)
        hs_color = kwargs.get(ATTR_HS_COLOR)
        new_colors: dict[ColorComponent, int] | None = None
        scale: float | None = None

        if brightness is None and hs_color is None:
            # Turned on without specifying brightness or color
            if self._last_on_color is not None:
                if self._target_brightness:
                    # Color is already set, use the binary switch to turn on
                    await self._async_set_brightness(None, transition)
                    return

                # Preserve the previous color
                new_colors = self._last_on_color
            elif self._supports_color:
                # Turned on for the first time. Make it white
                new_colors = {
                    ColorComponent.RED: 255,
                    ColorComponent.GREEN: 255,
                    ColorComponent.BLUE: 255,
                }
        elif brightness is not None:
            # If brightness gets set, preserve the color and mix it with the new brightness
            if self.color_mode == ColorMode.HS:
                scale = brightness / 255
            if (
                self._last_on_color is not None
                and None not in self._last_on_color.values()
            ):
                # Changed brightness from 0 to >0
                old_brightness = max(self._last_on_color.values())
                new_scale = brightness / old_brightness
                scale = new_scale
                new_colors = {}
                for color, value in self._last_on_color.items():
                    new_colors[color] = round(value * new_scale)
            elif hs_color is None and self._color_mode == ColorMode.HS:
                hs_color = self._hs_color
        elif hs_color is not None and brightness is None:
            # Turned on by using the color controls
            current_brightness = self.brightness
            if current_brightness == 0 and self._last_brightness is not None:
                # Use the last brightness value if the light is currently off
                scale = self._last_brightness / 255
            elif current_brightness is not None:
                scale = current_brightness / 255

        # Reset last color until turning off again
        self._last_on_color = None

        if new_colors is None:
            new_colors = self._get_new_colors(
                hs_color=hs_color, color_temp_k=None, rgbw=None, brightness_scale=scale
            )

        if new_colors is not None:
            await self._async_set_colors(new_colors, transition)

        # Turn the binary switch on if there is one
        await self._async_set_brightness(brightness, transition)

    async def async_turn_off(self, **kwargs: Any) -> None:
        """Turn the light off."""

        # Remember last color and brightness to restore it when turning on
        self._last_brightness = self.brightness
        if self._current_color and isinstance(self._current_color.value, dict):
            red = self._current_color.value.get(COLOR_SWITCH_COMBINED_RED)
            green = self._current_color.value.get(COLOR_SWITCH_COMBINED_GREEN)
            blue = self._current_color.value.get(COLOR_SWITCH_COMBINED_BLUE)

            last_color: dict[ColorComponent, int] = {}
            if red is not None:
                last_color[ColorComponent.RED] = red
            if green is not None:
                last_color[ColorComponent.GREEN] = green
            if blue is not None:
                last_color[ColorComponent.BLUE] = blue

            if last_color:
                self._last_on_color = last_color

        if self._target_brightness:
            # Turn off the binary switch only
            await self._async_set_brightness(0, kwargs.get(ATTR_TRANSITION))
        else:
            # turn off all color channels
            colors = {
                ColorComponent.RED: 0,
                ColorComponent.GREEN: 0,
                ColorComponent.BLUE: 0,
            }

            await self._async_set_colors(
                colors,
                kwargs.get(ATTR_TRANSITION),
            )