"""OpenRGB light platform."""

import asyncio
from typing import Any, override

from openrgb.orgb import Device
from openrgb.utils import ModeColors, ModeData, RGBColor

from homeassistant.components.light import (
    ATTR_BRIGHTNESS,
    ATTR_EFFECT,
    ATTR_RGB_COLOR,
    EFFECT_OFF,
    ColorMode,
    LightEntity,
    LightEntityFeature,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util import slugify
from homeassistant.util.color import color_hs_to_RGB, color_RGB_to_hsv

from .const import (
    CONNECTION_ERRORS,
    DEFAULT_BRIGHTNESS,
    DEFAULT_COLOR,
    DEVICE_TYPE_ICONS,
    DOMAIN,
    EFFECT_OFF_OPENRGB_MODES,
    OFF_COLOR,
    OpenRGBMode,
)
from .coordinator import OpenRGBConfigEntry, OpenRGBCoordinator

PARALLEL_UPDATES = 0


async def async_setup_entry(
    hass: HomeAssistant,
    config_entry: OpenRGBConfigEntry,
    async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
    """Set up the OpenRGB light platform."""
    coordinator = config_entry.runtime_data
    known_device_keys: set[str] = set()

    def _check_device() -> None:
        """Add entities for newly discovered OpenRGB devices."""
        nonlocal known_device_keys
        current_keys = set(coordinator.data.keys())
        new_keys = current_keys - known_device_keys
        if new_keys:
            known_device_keys.update(new_keys)
            async_add_entities(
                [OpenRGBLight(coordinator, device_key) for device_key in new_keys]
            )

    _check_device()
    config_entry.async_on_unload(coordinator.async_add_listener(_check_device))


class OpenRGBLight(CoordinatorEntity[OpenRGBCoordinator], LightEntity):
    """Representation of an OpenRGB light."""

    _attr_has_entity_name = True
    _attr_name = None  # Use the device name
    _attr_translation_key = "openrgb_light"

    _mode: str | None = None

    _supports_color_modes: list[str]
    _preferred_no_effect_mode: str
    _supports_off_mode: bool
    _supports_effects: bool

    _previous_brightness: int | None = None
    _previous_rgb_color: tuple[int, int, int] | None = None
    _previous_mode: str | None = None

    _update_events: list[asyncio.Event] = []

    _effect_to_mode: dict[str, str]

    def __init__(self, coordinator: OpenRGBCoordinator, device_key: str) -> None:
        """Initialize the OpenRGB light."""
        super().__init__(coordinator)
        self.device_key = device_key
        self._attr_unique_id = device_key

        device_name = coordinator.get_device_name(device_key)

        self._attr_device_info = DeviceInfo(
            identifiers={(DOMAIN, device_key)},
            name=device_name,
            manufacturer=self.device.metadata.vendor,
            model=f"{self.device.metadata.description} ({self.device.type.name})",
            sw_version=self.device.metadata.version,
            serial_number=self.device.metadata.serial,
            via_device_id=dr.async_get_device_id_by_identifier(
                coordinator.hass,
                (DOMAIN, coordinator.entry_id),
                config_entry_id=coordinator.entry_id,
            ),
        )

        modes = [mode.name for mode in self.device.modes]

        if self.device.metadata.description == "ASRock Polychrome USB Device":
            # https://gitlab.com/CalcProgrammer1/OpenRGB/-/issues/5145
            self._preferred_no_effect_mode = OpenRGBMode.STATIC
        else:
            # https://gitlab.com/CalcProgrammer1/OpenRGB/-/blob/c71cc4f18a54f83d388165ef2ab4c4ad3e980b89/RGBController/RGBController.cpp#L2075-2081
            self._preferred_no_effect_mode = (
                OpenRGBMode.DIRECT
                if OpenRGBMode.DIRECT in modes
                else OpenRGBMode.CUSTOM
                if OpenRGBMode.CUSTOM in modes
                else OpenRGBMode.STATIC
            )
        # Determine if the device supports being turned off through modes
        self._supports_off_mode = OpenRGBMode.OFF in modes
        # Determine which modes supports color
        self._supports_color_modes = [
            mode.name
            for mode in self.device.modes
            if check_if_mode_supports_color(mode)
        ]

        # Initialize effects from modes
        self._effect_to_mode = {}
        effects = []
        for mode in modes:
            if mode != OpenRGBMode.OFF and mode not in EFFECT_OFF_OPENRGB_MODES:
                effect_name = slugify(mode)
                effects.append(effect_name)
                self._effect_to_mode[effect_name] = mode

        if len(effects) > 0:
            self._supports_effects = True
            self._attr_supported_features = LightEntityFeature.EFFECT
            self._attr_effect_list = [EFFECT_OFF, *effects]
        else:
            self._supports_effects = False

        self._attr_icon = DEVICE_TYPE_ICONS.get(self.device.type)

        self._update_attrs()

    @callback
    def _update_attrs(self) -> None:
        """Update the attributes based on the current device state."""
        mode_data = self.device.modes[self.device.active_mode]
        mode = mode_data.name
        if mode == OpenRGBMode.OFF:
            mode = None
            mode_supports_colors = False
        else:
            mode_supports_colors = check_if_mode_supports_color(mode_data)

        color_mode = None
        rgb_color = None
        brightness = None
        on_by_color = True
        if mode_supports_colors:
            # Consider the first non-black LED color as the device color
            openrgb_off_color = RGBColor(*OFF_COLOR)
            openrgb_color = next(
                (color for color in self.device.colors if color != openrgb_off_color),
                openrgb_off_color,
            )

            if openrgb_color == openrgb_off_color:
                on_by_color = False
            else:
                rgb_color = (
                    openrgb_color.red,
                    openrgb_color.green,
                    openrgb_color.blue,
                )
                # Derive color and brightness from the scaled color
                hsv_color = color_RGB_to_hsv(*rgb_color)
                rgb_color = color_hs_to_RGB(hsv_color[0], hsv_color[1])
                brightness = round(255.0 * (hsv_color[2] / 100.0))

        elif mode is None:
            # If mode is Off, retain previous color mode to avoid changing the UI
            color_mode = self._attr_color_mode
        else:
            # If the current mode is not Off and does not support
            # color, change to ON/OFF mode
            color_mode = ColorMode.ONOFF

        if not on_by_color:
            # If Off by color, retain previous color mode to avoid changing the UI
            color_mode = self._attr_color_mode

        if color_mode is None:
            # If color mode is still unknown, default to RGB
            color_mode = ColorMode.RGB

        if self._attr_brightness is not None and self._attr_brightness != brightness:
            self._previous_brightness = self._attr_brightness
        if self._attr_rgb_color is not None and self._attr_rgb_color != rgb_color:
            self._previous_rgb_color = self._attr_rgb_color
        if self._mode is not None and self._mode != mode:
            self._previous_mode = self._mode

        self._attr_color_mode = color_mode
        self._attr_supported_color_modes = {color_mode}
        self._attr_rgb_color = rgb_color
        self._attr_brightness = brightness
        if not self._supports_effects or mode is None:
            self._attr_effect = None
        elif mode in EFFECT_OFF_OPENRGB_MODES:
            self._attr_effect = EFFECT_OFF
        else:
            self._attr_effect = slugify(mode)
        self._mode = mode

        if mode is None:
            # If the mode is Off, the light is off
            self._attr_is_on = False
        else:
            self._attr_is_on = on_by_color

    @callback
    @override
    def _handle_coordinator_update(self) -> None:
        """Handle updated data from the coordinator."""
        if self.available:
            self._update_attrs()
        super()._handle_coordinator_update()

        # Signal that the update has completed for all waiting events
        for event in self._update_events:
            event.set()
        self._update_events.clear()

    @property
    @override
    def available(self) -> bool:
        """Return if the light is available."""
        return super().available and self.device_key in self.coordinator.data

    @property
    def device(self) -> Device:
        """Return the OpenRGB device."""
        return self.coordinator.data[self.device_key]

    async def _async_refresh_data(self) -> None:
        """Request a data refresh from the coordinator and wait for it to complete."""
        update_event = asyncio.Event()
        self._update_events.append(update_event)
        await self.coordinator.async_request_refresh()
        await update_event.wait()

    async def _async_apply_color(
        self, rgb_color: tuple[int, int, int], brightness: int
    ) -> None:
        """Apply the RGB color and brightness to the device."""
        brightness_factor = brightness / 255.0
        scaled_color = RGBColor(
            int(rgb_color[0] * brightness_factor),
            int(rgb_color[1] * brightness_factor),
            int(rgb_color[2] * brightness_factor),
        )

        async with self.coordinator.client_lock:
            try:
                await self.hass.async_add_executor_job(
                    self.device.set_color, scaled_color, True
                )
            except CONNECTION_ERRORS as err:
                raise HomeAssistantError(
                    translation_domain=DOMAIN,
                    translation_key="communication_error",
                    translation_placeholders={
                        "server_address": self.coordinator.server_address,
                        "error": str(err),
                    },
                ) from err
            except ValueError as err:
                raise HomeAssistantError(
                    translation_domain=DOMAIN,
                    translation_key="openrgb_error",
                    translation_placeholders={
                        "error": str(err),
                    },
                ) from err

    async def _async_apply_mode(self, mode: str) -> None:
        """Apply the given mode to the device."""
        async with self.coordinator.client_lock:
            try:
                await self.hass.async_add_executor_job(self.device.set_mode, mode)
            except CONNECTION_ERRORS as err:
                raise HomeAssistantError(
                    translation_domain=DOMAIN,
                    translation_key="communication_error",
                    translation_placeholders={
                        "server_address": self.coordinator.server_address,
                        "error": str(err),
                    },
                ) from err
            except ValueError as err:
                raise HomeAssistantError(
                    translation_domain=DOMAIN,
                    translation_key="openrgb_error",
                    translation_placeholders={
                        "error": str(err),
                    },
                ) from err

    @override
    async def async_turn_on(self, **kwargs: Any) -> None:
        """Turn on the light."""
        mode = None
        if ATTR_EFFECT in kwargs:
            effect = kwargs[ATTR_EFFECT]
            if self._attr_effect_list is None or effect not in self._attr_effect_list:
                raise ServiceValidationError(
                    translation_domain=DOMAIN,
                    translation_key="unsupported_effect",
                    translation_placeholders={
                        "effect": effect,
                        "device_name": self.device.name,
                    },
                )
            if effect == EFFECT_OFF:
                mode = self._preferred_no_effect_mode
            else:
                mode = self._effect_to_mode[effect]
        elif self._mode is None or (
            self._attr_rgb_color is None and self._attr_brightness is None
        ):
            # Restore previous mode when turning on from Off mode or black color
            mode = self._previous_mode or self._preferred_no_effect_mode

        # Check if current or new mode supports colors
        if mode is None:
            # When not applying a new mode, check if the current mode supports color
            mode_supports_color = self._mode in self._supports_color_modes
        else:
            mode_supports_color = mode in self._supports_color_modes

        color_or_brightness_requested = (
            ATTR_RGB_COLOR in kwargs or ATTR_BRIGHTNESS in kwargs
        )
        if color_or_brightness_requested and not mode_supports_color:
            raise ServiceValidationError(
                translation_domain=DOMAIN,
                translation_key="effect_no_color_support",
                translation_placeholders={
                    "effect": slugify(mode or self._mode or ""),
                    "device_name": self.device.name,
                },
            )

        # Apply color even if switching from Off mode to a color-capable mode
        # because there is no guarantee that the device won't go back to black
        need_to_apply_color = color_or_brightness_requested or (
            mode_supports_color
            and (self._attr_brightness is None or self._attr_rgb_color is None)
        )

        # If color/brightness restoration require color support
        # but mode doesn't support it, switch to a color-capable
        # mode
        if need_to_apply_color and not mode_supports_color:
            mode = self._preferred_no_effect_mode

        if mode is not None:
            await self._async_apply_mode(mode)

        if need_to_apply_color:
            brightness = None
            if ATTR_BRIGHTNESS in kwargs:
                brightness = kwargs[ATTR_BRIGHTNESS]
            elif self._attr_brightness is None:
                # Restore previous brightness when turning on
                brightness = self._previous_brightness
            if brightness is None:
                # Retain current brightness or use default if still None
                brightness = self._attr_brightness or DEFAULT_BRIGHTNESS

            color = None
            if ATTR_RGB_COLOR in kwargs:
                color = kwargs[ATTR_RGB_COLOR]
            elif self._attr_rgb_color is None:
                # Restore previous color when turning on
                color = self._previous_rgb_color
            if color is None:
                # Retain current color or use default if still None
                color = self._attr_rgb_color or DEFAULT_COLOR

            await self._async_apply_color(color, brightness)

        await self._async_refresh_data()

    @override
    async def async_turn_off(self, **kwargs: Any) -> None:
        """Turn off the light."""
        if self._supports_off_mode:
            await self._async_apply_mode(OpenRGBMode.OFF)
        else:
            # If the device does not support Off mode, set color to black
            await self._async_apply_color(OFF_COLOR, 0)

        await self._async_refresh_data()


def check_if_mode_supports_color(mode: ModeData) -> bool:
    """Return True if the mode supports colors."""
    return mode.color_mode == ModeColors.PER_LED
