"""
Alexa Devices Lights.

SPDX-License-Identifier: Apache-2.0

For more details about this platform, please refer to the documentation at
https://community.home-assistant.io/t/echo-devices-alexa-as-media-player-testers-needed/58639
"""

import datetime
import logging
from math import sqrt
from typing import Optional

from alexapy import AlexaAPI, hide_serial
from homeassistant.components.light import (
    ATTR_BRIGHTNESS,
    ATTR_COLOR_TEMP_KELVIN,
    ATTR_HS_COLOR,
    ColorMode,
    LightEntity,
)
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util.color import (
    color_hs_to_RGB,
    color_hsb_to_RGB,
    color_name_to_rgb,
    color_RGB_to_hs,
)

from . import (
    CONF_EMAIL,
    CONF_EXCLUDE_DEVICES,
    CONF_INCLUDE_DEVICES,
    DATA_ALEXAMEDIA,
    hide_email,
)
from .alexa_entity import (
    parse_brightness_from_coordinator,
    parse_color_from_coordinator,
    parse_color_temp_from_coordinator,
    parse_power_from_coordinator,
)
from .const import CONF_EXTENDED_ENTITY_DISCOVERY
from .helpers import add_devices, safe_get

_LOGGER = logging.getLogger(__name__)

LOCAL_TIMEZONE = datetime.datetime.now(datetime.timezone.utc).astimezone().tzinfo


async def async_setup_platform(hass, config, add_devices_callback, discovery_info=None):
    """Set up the Alexa sensor platform."""
    devices: list[LightEntity] = []
    account = None
    if config:
        account = config.get(CONF_EMAIL)
    if account is None and discovery_info:
        account = safe_get(discovery_info, ["config", CONF_EMAIL])
    if account is None:
        raise ConfigEntryNotReady
    account_dict = hass.data[DATA_ALEXAMEDIA]["accounts"][account]
    include_filter = config.get(CONF_INCLUDE_DEVICES, [])
    exclude_filter = config.get(CONF_EXCLUDE_DEVICES, [])
    coordinator = account_dict["coordinator"]
    hue_emulated_enabled = "emulated_hue" in hass.config.as_dict().get(
        "components", set()
    )
    light_entities = safe_get(account_dict, ["devices", "light"], [])
    if light_entities and account_dict["options"].get(CONF_EXTENDED_ENTITY_DISCOVERY):
        for light_entity in light_entities:
            if not (light_entity["is_hue_v1"] and hue_emulated_enabled):
                _LOGGER.debug(
                    "Creating entity %s for a light with name %s",
                    hide_serial(light_entity["id"]),
                    light_entity["name"],
                )
                light = AlexaLight(coordinator, account_dict["login_obj"], light_entity)
                account_dict["entities"]["light"].append(light)
                devices.append(light)
            else:
                _LOGGER.debug(
                    "Light '%s' has not been added because it may originate from emulated_hue",
                    light_entity["name"],
                )

    return await add_devices(
        hide_email(account),
        devices,
        add_devices_callback,
        include_filter,
        exclude_filter,
    )


async def async_setup_entry(hass, config_entry, async_add_devices):
    """Set up the Alexa sensor platform by config_entry."""
    return await async_setup_platform(
        hass, config_entry.data, async_add_devices, discovery_info=None
    )


async def async_unload_entry(hass, entry) -> bool:
    """Unload a config entry."""
    account = entry.data[CONF_EMAIL]
    account_dict = hass.data[DATA_ALEXAMEDIA]["accounts"][account]
    _LOGGER.debug("Attempting to unload lights")
    for light in account_dict["entities"]["light"]:
        await light.async_remove()
    return True


def color_modes(details) -> list:
    """Return list of color modes."""
    if details["color"] and details["color_temperature"]:
        return [ColorMode.HS, ColorMode.COLOR_TEMP]
    if details["color"]:
        return [ColorMode.HS]
    if details["color_temperature"]:
        return [ColorMode.COLOR_TEMP]
    if details["brightness"]:
        return [ColorMode.BRIGHTNESS]
    return [ColorMode.ONOFF]


class AlexaLight(CoordinatorEntity, LightEntity):
    """A light controlled by an Echo."""

    def __init__(self, coordinator, login, details):
        """Initialize alexa light entity."""
        super().__init__(coordinator)
        self.alexa_entity_id = details["id"]
        self._name = details["name"]
        self._login = login
        self._attr_supported_color_modes = color_modes(details)
        self._attr_min_color_temp_kelvin = 2200
        self._attr_max_color_temp_kelvin = 6500

        # Store the requested state from the last call to _set_state
        # This is so that no new network call is needed just to get values that are already known
        # This is useful because refreshing the full state can take a bit when many lights are in play.
        # Especially since Alexa actually polls the lights and that appears to be error-prone with some Zigbee lights.
        # That delay(1-5s in practice) causes the UI controls to jump all over the place after _set_state
        self._requested_state_at = None  # When was state last set in UTC
        self._requested_power = None
        self._requested_ha_brightness = None
        self._requested_kelvin = None
        self._requested_hs = None

    @property
    def name(self):
        """Return name."""
        return self._name

    @property
    def unique_id(self):
        """Return unique id."""
        return self.alexa_entity_id

    @property
    def color_mode(self):
        """Return color mode."""
        if (
            ColorMode.HS in self._attr_supported_color_modes
            and ColorMode.COLOR_TEMP in self._attr_supported_color_modes
        ):
            hs_color = self.hs_color
            if hs_color is None or (hs_color[0] == 0 and hs_color[1] == 0):
                # (0,0) is white. When white, color temp is the better plan.
                return ColorMode.COLOR_TEMP
            return ColorMode.HS
        return self._attr_supported_color_modes[0]

    @property
    def is_on(self):
        """Return whether on."""
        power = parse_power_from_coordinator(
            self.coordinator, self.alexa_entity_id, self._requested_state_at
        )
        if power is None:
            return self._requested_power if self._requested_power is not None else False
        return power == "ON"

    @property
    def brightness(self):
        """Return brightness."""
        bright = parse_brightness_from_coordinator(
            self.coordinator, self.alexa_entity_id, self._requested_state_at
        )
        if bright is None:
            return self._requested_ha_brightness
        return alexa_brightness_to_ha(bright)

    @property
    def color_temp_kelvin(self):
        """Return color temperature."""
        kelvin = parse_color_temp_from_coordinator(
            self.coordinator, self.alexa_entity_id, self._requested_state_at
        )
        if kelvin is None:
            return self._requested_kelvin
        return kelvin_to_alexa(kelvin)[0]

    @property
    def hs_color(self):
        """Return hs color."""
        hsb = parse_color_from_coordinator(
            self.coordinator, self.alexa_entity_id, self._requested_state_at
        )
        if hsb is None:
            return self._requested_hs
        (
            adjusted_hs,
            color_name,  # pylint:disable=unused-variable
        ) = hsb_to_alexa_color(hsb)
        return adjusted_hs

    @property
    def assumed_state(self) -> bool:
        """Return whether state is assumed."""
        last_refresh_success = (
            self.coordinator.data and self.alexa_entity_id in self.coordinator.data
        )
        return not last_refresh_success

    async def _set_state(self, power_on, brightness=None, kelvin=None, hs_color=None):
        # This is "rounding" on kelvin to the closest value Alexa is willing to acknowledge the existence of.
        # The alternative implementation would be to use effects instead.
        # That is far more non-standard, and would lock users out of things like the Flux integration.
        # The downsides to this approach is that the UI is giving the user a slider
        # When the user picks a slider value, the UI will "jump" to the closest possible value.
        # This trade-off doesn't feel as bad in practice as it sounds.
        adjusted_kelvin, color_temperature_name = kelvin_to_alexa(kelvin)
        if color_temperature_name is None:
            # This is "rounding" on HS color to closest value Alexa supports.
            # The alexa color list is short, but covers a pretty broad spectrum.
            # Like for kelvin above, this sounds bad but works ok in practice.
            adjusted_hs, color_name = hs_to_alexa_color(hs_color)
        else:
            # If a color temperature is being set, it is not possible to also adjust the color.
            adjusted_hs = None
            color_name = None

        response = await AlexaAPI.set_light_state(
            self._login,
            self.alexa_entity_id,
            power_on,
            brightness=ha_brightness_to_alexa(brightness),
            color_temperature_name=color_temperature_name,
            color_name=color_name,
        )
        if not isinstance(response, dict):
            return await self.coordinator.async_request_refresh()
        control_responses = response.get("controlResponses", [])
        for response in control_responses:
            if not response.get("code") == "SUCCESS":
                # If something failed any state is possible, fallback to a full refresh
                return await self.coordinator.async_request_refresh()
        self._requested_power = power_on
        self._requested_ha_brightness = (
            brightness if brightness is not None else self.brightness
        )
        self._requested_kelvin = (
            adjusted_kelvin if adjusted_kelvin is not None else self.color_temp_kelvin
        )
        if adjusted_hs is not None:
            self._requested_hs = adjusted_hs
        elif adjusted_kelvin is not None:
            # If a kelvin value was set, it is critical that color is cleared out so that color mode is set properly
            self._requested_hs = None
        else:
            self._requested_hs = self.hs_color
        self._requested_state_at = datetime.datetime.now(
            datetime.timezone.utc
        )  # must be set last so that previous getters work properly
        self.schedule_update_ha_state()

        # Confirm quickly, but debounce to avoid spamming during slider drags.
        account = self.hass.data[DATA_ALEXAMEDIA]["accounts"].get(self._login.email)
        if account:
            debouncer = account.get("confirm_refresh_debouncer")
            if debouncer:
                await debouncer.async_call()

    async def async_turn_on(self, **kwargs):
        """Turn on."""
        brightness = None
        kelvin = None
        hs_color = None
        if (
            ColorMode.ONOFF not in self._attr_supported_color_modes
            and ATTR_BRIGHTNESS in kwargs
        ):
            brightness = kwargs[ATTR_BRIGHTNESS]
        if (
            ColorMode.COLOR_TEMP in self._attr_supported_color_modes
            and ATTR_COLOR_TEMP_KELVIN in kwargs
        ):
            kelvin = kwargs[ATTR_COLOR_TEMP_KELVIN]
        if ColorMode.HS in self._attr_supported_color_modes and ATTR_HS_COLOR in kwargs:
            hs_color = kwargs[ATTR_HS_COLOR]
        await self._set_state(True, brightness, kelvin, hs_color)

    async def async_turn_off(self, **kwargs):  # pylint:disable=unused-argument
        """Turn off."""
        await self._set_state(False)


def kelvin_to_alexa(kelvin: Optional[float]) -> tuple[Optional[float], Optional[str]]:
    """Convert a given color temperature in kelvin to the closest available value that Alexa has support for."""
    if kelvin is None:
        return None, None
    if kelvin <= 2400:
        return 2200, "warm_white"
    if kelvin <= 3200:
        return 2700, "soft_white"
    if kelvin <= 4400:
        return 4000, "white"
    if kelvin <= 6000:
        return 5400, "daylight_white"
    return 6500, "cool_white"


def ha_brightness_to_alexa(ha_brightness: Optional[float]) -> Optional[float]:
    """Convert HA brightness to alexa brightness."""
    return (ha_brightness / 255 * 100) if ha_brightness is not None else None


def alexa_brightness_to_ha(alexa: Optional[float]) -> Optional[float]:
    """Convert Alexa brightness to HA brightness."""
    return (alexa / 100 * 255) if alexa is not None else None


# This is a fairly complete list of all the colors that Alexa will respond to and their associated RGB value.
ALEXA_COLORS = {
    "alice_blue": (240, 248, 255),
    "antique_white": (250, 235, 215),
    "aqua": (0, 255, 255),
    "aquamarine": (127, 255, 212),
    "azure": (240, 255, 255),
    "beige": (245, 245, 220),
    "bisque": (255, 228, 196),
    "black": (0, 0, 0),
    "blanched_almond": (255, 235, 205),
    "blue": (0, 0, 255),
    "blue_violet": (138, 43, 226),
    "brown": (165, 42, 42),
    "burlywood": (222, 184, 135),
    "cadet_blue": (95, 158, 160),
    "chartreuse": (127, 255, 0),
    "chocolate": (210, 105, 30),
    "coral": (255, 127, 80),
    "cornflower_blue": (100, 149, 237),
    "cornsilk": (255, 248, 220),
    "crimson": (220, 20, 60),
    "cyan": (0, 255, 255),
    "dark_blue": (0, 0, 139),
    "dark_cyan": (0, 139, 139),
    "dark_goldenrod": (184, 134, 11),
    "dark_green": (0, 100, 0),
    "dark_grey": (169, 169, 169),
    "dark_khaki": (189, 183, 107),
    "dark_magenta": (139, 0, 139),
    "dark_olive_green": (85, 107, 47),
    "dark_orange": (255, 140, 0),
    "dark_orchid": (153, 50, 204),
    "dark_red": (139, 0, 0),
    "dark_salmon": (233, 150, 122),
    "dark_sea_green": (143, 188, 143),
    "dark_slate_blue": (72, 61, 139),
    "dark_slate_grey": (47, 79, 79),
    "dark_turquoise": (0, 206, 209),
    "dark_violet": (148, 0, 211),
    "deep_pink": (255, 20, 147),
    "deep_sky_blue": (0, 191, 255),
    "dim_grey": (105, 105, 105),
    "dodger_blue": (30, 144, 255),
    "firebrick": (178, 34, 34),
    "floral_white": (255, 250, 240),
    "forest_green": (34, 139, 34),
    "fuchsia": (255, 0, 255),
    "gainsboro": (220, 220, 220),
    "ghost_white": (248, 248, 255),
    "gold": (255, 215, 0),
    "goldenrod": (218, 165, 32),
    "green": (0, 128, 0),
    "green_yellow": (173, 255, 47),
    "grey": (128, 128, 128),
    "honey_dew": (240, 255, 240),
    "hot_pink": (255, 105, 180),
    "indian_red": (205, 92, 92),
    "indigo": (75, 0, 130),
    "ivory": (255, 255, 240),
    "khaki": (240, 230, 140),
    "lavender": (230, 230, 250),
    "lavender_blush": (255, 240, 245),
    "lawn_green": (124, 252, 0),
    "lemon_chiffon": (255, 250, 205),
    "light_blue": (173, 216, 230),
    "light_coral": (240, 128, 128),
    "light_cyan": (224, 255, 255),
    "light_goldenrod_yellow": (250, 250, 210),
    "light_green": (144, 238, 144),
    "light_grey": (211, 211, 211),
    "light_pink": (255, 182, 193),
    "light_salmon": (255, 160, 122),
    "light_sea_green": (32, 178, 170),
    "light_sky_blue": (135, 206, 250),
    "light_slate_grey": (119, 136, 153),
    "light_steel_blue": (176, 196, 222),
    "light_yellow": (255, 255, 224),
    "lime": (0, 255, 0),
    "lime_green": (50, 205, 50),
    "linen": (250, 240, 230),
    "magenta": (255, 0, 255),
    "maroon": (128, 0, 0),
    "medium_aqua_marine": (102, 205, 170),
    "medium_blue": (0, 0, 205),
    "medium_orchid": (186, 85, 211),
    "medium_purple": (147, 112, 219),
    "medium_sea_green": (60, 179, 113),
    "medium_slate_blue": (123, 104, 238),
    "medium_spring_green": (0, 250, 154),
    "medium_turquoise": (72, 209, 204),
    "medium_violet_red": (199, 21, 133),
    "midnight_blue": (25, 25, 112),
    "mint_cream": (245, 255, 250),
    "misty_rose": (255, 228, 225),
    "moccasin": (255, 228, 181),
    "navajo_white": (255, 222, 173),
    "navy": (0, 0, 128),
    "old_lace": (253, 245, 230),
    "olive": (128, 128, 0),
    "olive_drab": (107, 142, 35),
    "orange": (255, 165, 0),
    "orange_red": (255, 69, 0),
    "orchid": (218, 112, 214),
    "pale_goldenrod": (238, 232, 170),
    "pale_green": (152, 251, 152),
    "pale_turquoise": (175, 238, 238),
    "pale_violet_red": (219, 112, 147),
    "papaya_whip": (255, 239, 213),
    "peach_puff": (255, 218, 185),
    "peru": (205, 133, 63),
    "pink": (255, 192, 203),
    "plum": (221, 160, 221),
    "powder_blue": (176, 224, 230),
    "purple": (128, 0, 128),
    "rebecca_purple": (102, 51, 153),
    "red": (255, 0, 0),
    "rosy_brown": (188, 143, 143),
    "royal_blue": (65, 105, 225),
    "saddle_brown": (139, 69, 19),
    "salmon": (250, 128, 114),
    "sandy_brown": (244, 164, 96),
    "sea_green": (46, 139, 87),
    "sea_shell": (255, 245, 238),
    "sienna": (160, 82, 45),
    "silver": (192, 192, 192),
    "sky_blue": (135, 206, 235),
    "slate_blue": (106, 90, 205),
    "slate_grey": (112, 128, 144),
    "snow": (255, 250, 250),
    "spring_green": (0, 255, 127),
    "steel_blue": (70, 130, 180),
    "tan": (210, 180, 140),
    "teal": (0, 128, 128),
    "thistle": (216, 191, 216),
    "tomato": (255, 99, 71),
    "turquoise": (64, 224, 208),
    "violet": (238, 130, 238),
    "wheat": (245, 222, 179),
    "white": (255, 255, 255),
    "white_smoke": (245, 245, 245),
    "yellow": (255, 255, 0),
    "yellow_green": (154, 205, 50),
}


def red_mean(color1: tuple[int, int, int], color2: tuple[int, int, int]) -> float:
    """Get an approximate 'distance' between two colors using red mean.

    Wikipedia says this method is "one of the better low-cost approximations".
    """
    r_avg = (color2[0] + color1[0]) / 2
    r_delta = color2[0] - color1[0]
    g_delta = color2[1] - color1[1]
    b_delta = color2[2] - color1[2]
    r_term = (2 + r_avg / 256) * pow(r_delta, 2)
    g_term = 4 * pow(g_delta, 2)
    b_term = (2 + (255 - r_avg) / 256) * pow(b_delta, 2)
    return sqrt(r_term + g_term + b_term)


def alexa_color_name_to_rgb(color_name: str) -> tuple[int, int, int]:
    """Convert an alexa color name into RGB."""
    return color_name_to_rgb(color_name.replace("_", ""))


def rgb_to_alexa_color(
    rgb: tuple[int, int, int],
) -> tuple[Optional[tuple[float, float]], Optional[str]]:
    """Convert a given RGB value into the closest Alexa color."""
    name, alexa_rgb = min(
        ALEXA_COLORS.items(),
        key=lambda alexa_color: red_mean(alexa_color[1], rgb),
    )
    red, green, blue = alexa_rgb
    return color_RGB_to_hs(red, green, blue), name


def hs_to_alexa_color(
    hs_color: Optional[tuple[float, float]],
) -> tuple[Optional[tuple[float, float]], Optional[str]]:
    """Convert a given hue/saturation value into the closest Alexa color."""
    if hs_color is None:
        return None, None
    hue, saturation = hs_color
    return rgb_to_alexa_color(color_hs_to_RGB(hue, saturation))


def hsb_to_alexa_color(
    hsb: Optional[tuple[float, float, float]],
) -> tuple[Optional[tuple[float, float]], Optional[str]]:
    """Convert a given hue/saturation/brightness value into the closest Alexa color."""
    if hsb is None:
        return None, None
    hue, saturation, brightness = hsb
    return rgb_to_alexa_color(color_hsb_to_RGB(hue, saturation, brightness))
