"""Support for a Hue API to control Home Assistant."""
import asyncio
import hashlib
from ipaddress import ip_address
import logging
import time

from homeassistant import core
from homeassistant.components import (
    climate,
    cover,
    fan,
    humidifier,
    light,
    media_player,
    scene,
    script,
)
from homeassistant.components.climate.const import (
    SERVICE_SET_TEMPERATURE,
    SUPPORT_TARGET_TEMPERATURE,
)
from homeassistant.components.cover import (
    ATTR_CURRENT_POSITION,
    ATTR_POSITION,
    SERVICE_SET_COVER_POSITION,
    SUPPORT_SET_POSITION,
)
from homeassistant.components.fan import (
    ATTR_SPEED,
    SPEED_HIGH,
    SPEED_LOW,
    SPEED_MEDIUM,
    SPEED_OFF,
    SUPPORT_SET_SPEED,
)
from homeassistant.components.http import HomeAssistantView
from homeassistant.components.humidifier.const import (
    ATTR_HUMIDITY,
    SERVICE_SET_HUMIDITY,
)
from homeassistant.components.light import (
    ATTR_BRIGHTNESS,
    ATTR_COLOR_TEMP,
    ATTR_HS_COLOR,
    ATTR_TRANSITION,
    ATTR_XY_COLOR,
    SUPPORT_TRANSITION,
)
from homeassistant.components.media_player.const import (
    ATTR_MEDIA_VOLUME_LEVEL,
    SUPPORT_VOLUME_SET,
)
from homeassistant.const import (
    ATTR_ENTITY_ID,
    ATTR_SUPPORTED_FEATURES,
    ATTR_TEMPERATURE,
    HTTP_BAD_REQUEST,
    HTTP_NOT_FOUND,
    HTTP_UNAUTHORIZED,
    SERVICE_CLOSE_COVER,
    SERVICE_OPEN_COVER,
    SERVICE_TURN_OFF,
    SERVICE_TURN_ON,
    SERVICE_VOLUME_SET,
    STATE_OFF,
    STATE_ON,
    STATE_UNAVAILABLE,
)
from homeassistant.helpers.event import async_track_state_change_event
from homeassistant.util.network import is_local

_LOGGER = logging.getLogger(__name__)

# How long to wait for a state change to happen
STATE_CHANGE_WAIT_TIMEOUT = 5.0
# How long an entry state's cache will be valid for in seconds.
STATE_CACHED_TIMEOUT = 2.0

STATE_BRIGHTNESS = "bri"
STATE_COLORMODE = "colormode"
STATE_HUE = "hue"
STATE_SATURATION = "sat"
STATE_COLOR_TEMP = "ct"
STATE_TRANSITON = "tt"
STATE_XY = "xy"

# Hue API states, defined separately in case they change
HUE_API_STATE_ON = "on"
HUE_API_STATE_BRI = "bri"
HUE_API_STATE_COLORMODE = "colormode"
HUE_API_STATE_HUE = "hue"
HUE_API_STATE_SAT = "sat"
HUE_API_STATE_CT = "ct"
HUE_API_STATE_XY = "xy"
HUE_API_STATE_EFFECT = "effect"
HUE_API_STATE_TRANSITION = "transitiontime"

# Hue API min/max values - https://developers.meethue.com/develop/hue-api/lights-api/
HUE_API_STATE_BRI_MIN = 1  # Brightness
HUE_API_STATE_BRI_MAX = 254
HUE_API_STATE_HUE_MIN = 0  # Hue
HUE_API_STATE_HUE_MAX = 65535
HUE_API_STATE_SAT_MIN = 0  # Saturation
HUE_API_STATE_SAT_MAX = 254
HUE_API_STATE_CT_MIN = 153  # Color temp
HUE_API_STATE_CT_MAX = 500

HUE_API_USERNAME = "nouser"
UNAUTHORIZED_USER = [
    {"error": {"address": "/", "description": "unauthorized user", "type": "1"}}
]


class HueUnauthorizedUser(HomeAssistantView):
    """Handle requests to find the emulated hue bridge."""

    url = "/api"
    name = "emulated_hue:api:unauthorized_user"
    extra_urls = ["/api/"]
    requires_auth = False

    async def get(self, request):
        """Handle a GET request."""
        return self.json(UNAUTHORIZED_USER)


class HueUsernameView(HomeAssistantView):
    """Handle requests to create a username for the emulated hue bridge."""

    url = "/api"
    name = "emulated_hue:api:create_username"
    extra_urls = ["/api/"]
    requires_auth = False

    async def post(self, request):
        """Handle a POST request."""
        if not is_local(ip_address(request.remote)):
            return self.json_message("Only local IPs allowed", HTTP_UNAUTHORIZED)

        try:
            data = await request.json()
        except ValueError:
            return self.json_message("Invalid JSON", HTTP_BAD_REQUEST)

        if "devicetype" not in data:
            return self.json_message("devicetype not specified", HTTP_BAD_REQUEST)

        return self.json([{"success": {"username": HUE_API_USERNAME}}])


class HueAllGroupsStateView(HomeAssistantView):
    """Handle requests for getting info about entity groups."""

    url = "/api/{username}/groups"
    name = "emulated_hue:all_groups:state"
    requires_auth = False

    def __init__(self, config):
        """Initialize the instance of the view."""
        self.config = config

    @core.callback
    def get(self, request, username):
        """Process a request to make the Brilliant Lightpad work."""
        if not is_local(ip_address(request.remote)):
            return self.json_message("Only local IPs allowed", HTTP_UNAUTHORIZED)

        return self.json({})


class HueGroupView(HomeAssistantView):
    """Group handler to get Logitech Pop working."""

    url = "/api/{username}/groups/0/action"
    name = "emulated_hue:groups:state"
    requires_auth = False

    def __init__(self, config):
        """Initialize the instance of the view."""
        self.config = config

    @core.callback
    def put(self, request, username):
        """Process a request to make the Logitech Pop working."""
        if not is_local(ip_address(request.remote)):
            return self.json_message("Only local IPs allowed", HTTP_UNAUTHORIZED)

        return self.json(
            [
                {
                    "error": {
                        "address": "/groups/0/action/scene",
                        "type": 7,
                        "description": "invalid value, dummy for parameter, scene",
                    }
                }
            ]
        )


class HueAllLightsStateView(HomeAssistantView):
    """Handle requests for getting info about all entities."""

    url = "/api/{username}/lights"
    name = "emulated_hue:lights:state"
    requires_auth = False

    def __init__(self, config):
        """Initialize the instance of the view."""
        self.config = config

    @core.callback
    def get(self, request, username):
        """Process a request to get the list of available lights."""
        if not is_local(ip_address(request.remote)):
            return self.json_message("Only local IPs allowed", HTTP_UNAUTHORIZED)

        return self.json(create_list_of_entities(self.config, request))


class HueFullStateView(HomeAssistantView):
    """Return full state view of emulated hue."""

    url = "/api/{username}"
    name = "emulated_hue:username:state"
    requires_auth = False

    def __init__(self, config):
        """Initialize the instance of the view."""
        self.config = config

    @core.callback
    def get(self, request, username):
        """Process a request to get the list of available lights."""
        if not is_local(ip_address(request.remote)):
            return self.json_message("only local IPs allowed", HTTP_UNAUTHORIZED)
        if username != HUE_API_USERNAME:
            return self.json(UNAUTHORIZED_USER)

        json_response = {
            "lights": create_list_of_entities(self.config, request),
            "config": create_config_model(self.config, request),
        }

        return self.json(json_response)


class HueConfigView(HomeAssistantView):
    """Return config view of emulated hue."""

    url = "/api/{username}/config"
    extra_urls = ["/api/config"]
    name = "emulated_hue:username:config"
    requires_auth = False

    def __init__(self, config):
        """Initialize the instance of the view."""
        self.config = config

    @core.callback
    def get(self, request, username=""):
        """Process a request to get the configuration."""
        if not is_local(ip_address(request.remote)):
            return self.json_message("only local IPs allowed", HTTP_UNAUTHORIZED)

        json_response = create_config_model(self.config, request)

        return self.json(json_response)


class HueOneLightStateView(HomeAssistantView):
    """Handle requests for getting info about a single entity."""

    url = "/api/{username}/lights/{entity_id}"
    name = "emulated_hue:light:state"
    requires_auth = False

    def __init__(self, config):
        """Initialize the instance of the view."""
        self.config = config

    @core.callback
    def get(self, request, username, entity_id):
        """Process a request to get the state of an individual light."""
        if not is_local(ip_address(request.remote)):
            return self.json_message("Only local IPs allowed", HTTP_UNAUTHORIZED)

        hass = request.app["hass"]
        hass_entity_id = self.config.number_to_entity_id(entity_id)

        if hass_entity_id is None:
            _LOGGER.error(
                "Unknown entity number: %s not found in emulated_hue_ids.json",
                entity_id,
            )
            return self.json_message("Entity not found", HTTP_NOT_FOUND)

        entity = hass.states.get(hass_entity_id)

        if entity is None:
            _LOGGER.error("Entity not found: %s", hass_entity_id)
            return self.json_message("Entity not found", HTTP_NOT_FOUND)

        if not self.config.is_entity_exposed(entity):
            _LOGGER.error("Entity not exposed: %s", entity_id)
            return self.json_message("Entity not exposed", HTTP_UNAUTHORIZED)

        json_response = entity_to_json(self.config, entity)

        return self.json(json_response)


class HueOneLightChangeView(HomeAssistantView):
    """Handle requests for setting info about entities."""

    url = "/api/{username}/lights/{entity_number}/state"
    name = "emulated_hue:light:state"
    requires_auth = False

    def __init__(self, config):
        """Initialize the instance of the view."""
        self.config = config

    async def put(self, request, username, entity_number):  # noqa: C901
        """Process a request to set the state of an individual light."""
        if not is_local(ip_address(request.remote)):
            return self.json_message("Only local IPs allowed", HTTP_UNAUTHORIZED)

        config = self.config
        hass = request.app["hass"]
        entity_id = config.number_to_entity_id(entity_number)

        if entity_id is None:
            _LOGGER.error("Unknown entity number: %s", entity_number)
            return self.json_message("Entity not found", HTTP_NOT_FOUND)

        entity = hass.states.get(entity_id)

        if entity is None:
            _LOGGER.error("Entity not found: %s", entity_id)
            return self.json_message("Entity not found", HTTP_NOT_FOUND)

        if not config.is_entity_exposed(entity):
            _LOGGER.error("Entity not exposed: %s", entity_id)
            return self.json_message("Entity not exposed", HTTP_UNAUTHORIZED)

        try:
            request_json = await request.json()
        except ValueError:
            _LOGGER.error("Received invalid json")
            return self.json_message("Invalid JSON", HTTP_BAD_REQUEST)

        # Get the entity's supported features
        entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
        if entity.domain == light.DOMAIN:
            color_modes = entity.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES, [])

        # Parse the request
        parsed = {
            STATE_ON: False,
            STATE_BRIGHTNESS: None,
            STATE_HUE: None,
            STATE_SATURATION: None,
            STATE_COLOR_TEMP: None,
            STATE_XY: None,
            STATE_TRANSITON: None,
        }

        if HUE_API_STATE_ON in request_json:
            if not isinstance(request_json[HUE_API_STATE_ON], bool):
                _LOGGER.error("Unable to parse data: %s", request_json)
                return self.json_message("Bad request", HTTP_BAD_REQUEST)
            parsed[STATE_ON] = request_json[HUE_API_STATE_ON]
        else:
            parsed[STATE_ON] = entity.state != STATE_OFF

        for (key, attr) in (
            (HUE_API_STATE_BRI, STATE_BRIGHTNESS),
            (HUE_API_STATE_HUE, STATE_HUE),
            (HUE_API_STATE_SAT, STATE_SATURATION),
            (HUE_API_STATE_CT, STATE_COLOR_TEMP),
            (HUE_API_STATE_TRANSITION, STATE_TRANSITON),
        ):
            if key in request_json:
                try:
                    parsed[attr] = int(request_json[key])
                except ValueError:
                    _LOGGER.error("Unable to parse data (2): %s", request_json)
                    return self.json_message("Bad request", HTTP_BAD_REQUEST)
        if HUE_API_STATE_XY in request_json:
            try:
                parsed[STATE_XY] = (
                    float(request_json[HUE_API_STATE_XY][0]),
                    float(request_json[HUE_API_STATE_XY][1]),
                )
            except ValueError:
                _LOGGER.error("Unable to parse data (2): %s", request_json)
                return self.json_message("Bad request", HTTP_BAD_REQUEST)

        if HUE_API_STATE_BRI in request_json:
            if entity.domain == light.DOMAIN:
                if light.brightness_supported(color_modes):
                    parsed[STATE_ON] = parsed[STATE_BRIGHTNESS] > 0
                else:
                    parsed[STATE_BRIGHTNESS] = None

            elif entity.domain == scene.DOMAIN:
                parsed[STATE_BRIGHTNESS] = None
                parsed[STATE_ON] = True

            elif entity.domain in [
                script.DOMAIN,
                media_player.DOMAIN,
                fan.DOMAIN,
                cover.DOMAIN,
                climate.DOMAIN,
                humidifier.DOMAIN,
            ]:
                # Convert 0-254 to 0-100
                level = (parsed[STATE_BRIGHTNESS] / HUE_API_STATE_BRI_MAX) * 100
                parsed[STATE_BRIGHTNESS] = round(level)
                parsed[STATE_ON] = True

        # Choose general HA domain
        domain = core.DOMAIN

        # Entity needs separate call to turn on
        turn_on_needed = False

        # Convert the resulting "on" status into the service we need to call
        service = SERVICE_TURN_ON if parsed[STATE_ON] else SERVICE_TURN_OFF

        # Construct what we need to send to the service
        data = {ATTR_ENTITY_ID: entity_id}

        # If the requested entity is a light, set the brightness, hue,
        # saturation and color temp
        if entity.domain == light.DOMAIN:
            if parsed[STATE_ON]:
                if (
                    light.brightness_supported(color_modes)
                    and parsed[STATE_BRIGHTNESS] is not None
                ):
                    data[ATTR_BRIGHTNESS] = hue_brightness_to_hass(
                        parsed[STATE_BRIGHTNESS]
                    )

                if light.color_supported(color_modes):
                    if any((parsed[STATE_HUE], parsed[STATE_SATURATION])):
                        if parsed[STATE_HUE] is not None:
                            hue = parsed[STATE_HUE]
                        else:
                            hue = 0

                        if parsed[STATE_SATURATION] is not None:
                            sat = parsed[STATE_SATURATION]
                        else:
                            sat = 0

                        # Convert hs values to hass hs values
                        hue = int((hue / HUE_API_STATE_HUE_MAX) * 360)
                        sat = int((sat / HUE_API_STATE_SAT_MAX) * 100)

                        data[ATTR_HS_COLOR] = (hue, sat)

                    if parsed[STATE_XY] is not None:
                        data[ATTR_XY_COLOR] = parsed[STATE_XY]

                if (
                    light.color_temp_supported(color_modes)
                    and parsed[STATE_COLOR_TEMP] is not None
                ):
                    data[ATTR_COLOR_TEMP] = parsed[STATE_COLOR_TEMP]

                if (
                    entity_features & SUPPORT_TRANSITION
                    and parsed[STATE_TRANSITON] is not None
                ):
                    data[ATTR_TRANSITION] = parsed[STATE_TRANSITON] / 10

        # If the requested entity is a script, add some variables
        elif entity.domain == script.DOMAIN:
            data["variables"] = {
                "requested_state": STATE_ON if parsed[STATE_ON] else STATE_OFF
            }

            if parsed[STATE_BRIGHTNESS] is not None:
                data["variables"]["requested_level"] = parsed[STATE_BRIGHTNESS]

        # If the requested entity is a climate, set the temperature
        elif entity.domain == climate.DOMAIN:
            # We don't support turning climate devices on or off,
            # only setting the temperature
            service = None

            if (
                entity_features & SUPPORT_TARGET_TEMPERATURE
                and parsed[STATE_BRIGHTNESS] is not None
            ):
                domain = entity.domain
                service = SERVICE_SET_TEMPERATURE
                data[ATTR_TEMPERATURE] = parsed[STATE_BRIGHTNESS]

        # If the requested entity is a humidifier, set the humidity
        elif entity.domain == humidifier.DOMAIN:
            if parsed[STATE_BRIGHTNESS] is not None:
                turn_on_needed = True
                domain = entity.domain
                service = SERVICE_SET_HUMIDITY
                data[ATTR_HUMIDITY] = parsed[STATE_BRIGHTNESS]

        # If the requested entity is a media player, convert to volume
        elif entity.domain == media_player.DOMAIN:
            if (
                entity_features & SUPPORT_VOLUME_SET
                and parsed[STATE_BRIGHTNESS] is not None
            ):
                turn_on_needed = True
                domain = entity.domain
                service = SERVICE_VOLUME_SET
                # Convert 0-100 to 0.0-1.0
                data[ATTR_MEDIA_VOLUME_LEVEL] = parsed[STATE_BRIGHTNESS] / 100.0

        # If the requested entity is a cover, convert to open_cover/close_cover
        elif entity.domain == cover.DOMAIN:
            domain = entity.domain
            if service == SERVICE_TURN_ON:
                service = SERVICE_OPEN_COVER
            else:
                service = SERVICE_CLOSE_COVER

            if (
                entity_features & SUPPORT_SET_POSITION
                and parsed[STATE_BRIGHTNESS] is not None
            ):
                domain = entity.domain
                service = SERVICE_SET_COVER_POSITION
                data[ATTR_POSITION] = parsed[STATE_BRIGHTNESS]

        # If the requested entity is a fan, convert to speed
        elif (
            entity.domain == fan.DOMAIN
            and entity_features & SUPPORT_SET_SPEED
            and parsed[STATE_BRIGHTNESS] is not None
        ):
            domain = entity.domain
            # Convert 0-100 to a fan speed
            brightness = parsed[STATE_BRIGHTNESS]
            if brightness == 0:
                data[ATTR_SPEED] = SPEED_OFF
            elif 0 < brightness <= 33.3:
                data[ATTR_SPEED] = SPEED_LOW
            elif 33.3 < brightness <= 66.6:
                data[ATTR_SPEED] = SPEED_MEDIUM
            elif 66.6 < brightness <= 100:
                data[ATTR_SPEED] = SPEED_HIGH

        # Map the off command to on
        if entity.domain in config.off_maps_to_on_domains:
            service = SERVICE_TURN_ON

        # Separate call to turn on needed
        if turn_on_needed:
            hass.async_create_task(
                hass.services.async_call(
                    core.DOMAIN,
                    SERVICE_TURN_ON,
                    {ATTR_ENTITY_ID: entity_id},
                    blocking=True,
                )
            )

        if service is not None:
            state_will_change = parsed[STATE_ON] != (entity.state != STATE_OFF)

            hass.async_create_task(
                hass.services.async_call(domain, service, data, blocking=True)
            )

            if state_will_change:
                # Wait for the state to change.
                await wait_for_state_change_or_timeout(
                    hass, entity_id, STATE_CACHED_TIMEOUT
                )

        # Create success responses for all received keys
        json_response = [
            create_hue_success_response(
                entity_number, HUE_API_STATE_ON, parsed[STATE_ON]
            )
        ]

        for (key, val) in (
            (STATE_BRIGHTNESS, HUE_API_STATE_BRI),
            (STATE_HUE, HUE_API_STATE_HUE),
            (STATE_SATURATION, HUE_API_STATE_SAT),
            (STATE_COLOR_TEMP, HUE_API_STATE_CT),
            (STATE_XY, HUE_API_STATE_XY),
            (STATE_TRANSITON, HUE_API_STATE_TRANSITION),
        ):
            if parsed[key] is not None:
                json_response.append(
                    create_hue_success_response(entity_number, val, parsed[key])
                )

        if entity.domain in config.off_maps_to_on_domains:
            # Caching is required because things like scripts and scenes won't
            # report as "off" to Alexa if an "off" command is received, because
            # they'll map to "on". Thus, instead of reporting its actual
            # status, we report what Alexa will want to see, which is the same
            # as the actual requested command.
            config.cached_states[entity_id] = [parsed, None]
        else:
            config.cached_states[entity_id] = [parsed, time.time()]

        return self.json(json_response)


def get_entity_state(config, entity):
    """Retrieve and convert state and brightness values for an entity."""
    cached_state_entry = config.cached_states.get(entity.entity_id, None)
    cached_state = None

    # Check if we have a cached entry, and if so if it hasn't expired.
    if cached_state_entry is not None:
        entry_state, entry_time = cached_state_entry
        if entry_time is None:
            # Handle the case where the entity is listed in config.off_maps_to_on_domains.
            cached_state = entry_state
        elif time.time() - entry_time < STATE_CACHED_TIMEOUT and entry_state[
            STATE_ON
        ] == (entity.state != STATE_OFF):
            # We only want to use the cache if the actual state of the entity
            # is in sync so that it can be detected as an error by Alexa.
            cached_state = entry_state
        else:
            # Remove the now stale cached entry.
            config.cached_states.pop(entity.entity_id)

    data = {
        STATE_ON: False,
        STATE_BRIGHTNESS: None,
        STATE_HUE: None,
        STATE_SATURATION: None,
        STATE_COLOR_TEMP: None,
    }

    if cached_state is None:
        data[STATE_ON] = entity.state != STATE_OFF

        if data[STATE_ON]:
            data[STATE_BRIGHTNESS] = hass_to_hue_brightness(
                entity.attributes.get(ATTR_BRIGHTNESS, 0)
            )
            hue_sat = entity.attributes.get(ATTR_HS_COLOR)
            if hue_sat is not None:
                hue = hue_sat[0]
                sat = hue_sat[1]
                # Convert hass hs values back to hue hs values
                data[STATE_HUE] = int((hue / 360.0) * HUE_API_STATE_HUE_MAX)
                data[STATE_SATURATION] = int((sat / 100.0) * HUE_API_STATE_SAT_MAX)
            else:
                data[STATE_HUE] = HUE_API_STATE_HUE_MIN
                data[STATE_SATURATION] = HUE_API_STATE_SAT_MIN
            data[STATE_COLOR_TEMP] = entity.attributes.get(ATTR_COLOR_TEMP, 0)

        else:
            data[STATE_BRIGHTNESS] = 0
            data[STATE_HUE] = 0
            data[STATE_SATURATION] = 0
            data[STATE_COLOR_TEMP] = 0

        if entity.domain == climate.DOMAIN:
            temperature = entity.attributes.get(ATTR_TEMPERATURE, 0)
            # Convert 0-100 to 0-254
            data[STATE_BRIGHTNESS] = round(temperature * HUE_API_STATE_BRI_MAX / 100)
        elif entity.domain == humidifier.DOMAIN:
            humidity = entity.attributes.get(ATTR_HUMIDITY, 0)
            # Convert 0-100 to 0-254
            data[STATE_BRIGHTNESS] = round(humidity * HUE_API_STATE_BRI_MAX / 100)
        elif entity.domain == media_player.DOMAIN:
            level = entity.attributes.get(
                ATTR_MEDIA_VOLUME_LEVEL, 1.0 if data[STATE_ON] else 0.0
            )
            # Convert 0.0-1.0 to 0-254
            data[STATE_BRIGHTNESS] = round(min(1.0, level) * HUE_API_STATE_BRI_MAX)
        elif entity.domain == fan.DOMAIN:
            speed = entity.attributes.get(ATTR_SPEED, 0)
            # Convert 0.0-1.0 to 0-254
            data[STATE_BRIGHTNESS] = 0
            if speed == SPEED_LOW:
                data[STATE_BRIGHTNESS] = 85
            elif speed == SPEED_MEDIUM:
                data[STATE_BRIGHTNESS] = 170
            elif speed == SPEED_HIGH:
                data[STATE_BRIGHTNESS] = HUE_API_STATE_BRI_MAX
        elif entity.domain == cover.DOMAIN:
            level = entity.attributes.get(ATTR_CURRENT_POSITION, 0)
            data[STATE_BRIGHTNESS] = round(level / 100 * HUE_API_STATE_BRI_MAX)
    else:
        data = cached_state
        # Make sure brightness is valid
        if data[STATE_BRIGHTNESS] is None:
            data[STATE_BRIGHTNESS] = HUE_API_STATE_BRI_MAX if data[STATE_ON] else 0

        # Make sure hue/saturation are valid
        if (data[STATE_HUE] is None) or (data[STATE_SATURATION] is None):
            data[STATE_HUE] = 0
            data[STATE_SATURATION] = 0

        # If the light is off, set the color to off
        if data[STATE_BRIGHTNESS] == 0:
            data[STATE_HUE] = 0
            data[STATE_SATURATION] = 0

    # Clamp brightness, hue, saturation, and color temp to valid values
    for (key, v_min, v_max) in (
        (STATE_BRIGHTNESS, HUE_API_STATE_BRI_MIN, HUE_API_STATE_BRI_MAX),
        (STATE_HUE, HUE_API_STATE_HUE_MIN, HUE_API_STATE_HUE_MAX),
        (STATE_SATURATION, HUE_API_STATE_SAT_MIN, HUE_API_STATE_SAT_MAX),
        (STATE_COLOR_TEMP, HUE_API_STATE_CT_MIN, HUE_API_STATE_CT_MAX),
    ):
        if data[key] is not None:
            data[key] = max(v_min, min(data[key], v_max))

    return data


def entity_to_json(config, entity):
    """Convert an entity to its Hue bridge JSON representation."""
    entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
    color_modes = entity.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES, [])
    unique_id = hashlib.md5(entity.entity_id.encode()).hexdigest()
    unique_id = f"00:{unique_id[0:2]}:{unique_id[2:4]}:{unique_id[4:6]}:{unique_id[6:8]}:{unique_id[8:10]}:{unique_id[10:12]}:{unique_id[12:14]}-{unique_id[14:16]}"

    state = get_entity_state(config, entity)

    retval = {
        "state": {
            HUE_API_STATE_ON: state[STATE_ON],
            "reachable": entity.state != STATE_UNAVAILABLE,
            "mode": "homeautomation",
        },
        "name": config.get_entity_name(entity),
        "uniqueid": unique_id,
        "manufacturername": "Home Assistant",
        "swversion": "123",
    }

    if light.color_supported(color_modes) and light.color_temp_supported(color_modes):
        # Extended Color light (Zigbee Device ID: 0x0210)
        # Same as Color light, but which supports additional setting of color temperature
        retval["type"] = "Extended color light"
        retval["modelid"] = "HASS231"
        retval["state"].update(
            {
                HUE_API_STATE_BRI: state[STATE_BRIGHTNESS],
                HUE_API_STATE_HUE: state[STATE_HUE],
                HUE_API_STATE_SAT: state[STATE_SATURATION],
                HUE_API_STATE_CT: state[STATE_COLOR_TEMP],
                HUE_API_STATE_EFFECT: "none",
            }
        )
        if state[STATE_HUE] > 0 or state[STATE_SATURATION] > 0:
            retval["state"][HUE_API_STATE_COLORMODE] = "hs"
        else:
            retval["state"][HUE_API_STATE_COLORMODE] = "ct"
    elif light.color_supported(color_modes):
        # Color light (Zigbee Device ID: 0x0200)
        # Supports on/off, dimming and color control (hue/saturation, enhanced hue, color loop and XY)
        retval["type"] = "Color light"
        retval["modelid"] = "HASS213"
        retval["state"].update(
            {
                HUE_API_STATE_BRI: state[STATE_BRIGHTNESS],
                HUE_API_STATE_COLORMODE: "hs",
                HUE_API_STATE_HUE: state[STATE_HUE],
                HUE_API_STATE_SAT: state[STATE_SATURATION],
                HUE_API_STATE_EFFECT: "none",
            }
        )
    elif light.color_temp_supported(color_modes):
        # Color temperature light (Zigbee Device ID: 0x0220)
        # Supports groups, scenes, on/off, dimming, and setting of a color temperature
        retval["type"] = "Color temperature light"
        retval["modelid"] = "HASS312"
        retval["state"].update(
            {
                HUE_API_STATE_COLORMODE: "ct",
                HUE_API_STATE_CT: state[STATE_COLOR_TEMP],
                HUE_API_STATE_BRI: state[STATE_BRIGHTNESS],
            }
        )
    elif entity_features & (
        SUPPORT_SET_POSITION
        | SUPPORT_SET_SPEED
        | SUPPORT_VOLUME_SET
        | SUPPORT_TARGET_TEMPERATURE
    ) or light.brightness_supported(color_modes):
        # Dimmable light (Zigbee Device ID: 0x0100)
        # Supports groups, scenes, on/off and dimming
        retval["type"] = "Dimmable light"
        retval["modelid"] = "HASS123"
        retval["state"].update({HUE_API_STATE_BRI: state[STATE_BRIGHTNESS]})
    elif not config.lights_all_dimmable:
        # On/Off light (ZigBee Device ID: 0x0000)
        # Supports groups, scenes and on/off control
        retval["type"] = "On/Off light"
        retval["productname"] = "On/Off light"
        retval["modelid"] = "HASS321"
    else:
        # Dimmable light (Zigbee Device ID: 0x0100)
        # Supports groups, scenes, on/off and dimming
        # Reports fixed brightness for compatibility with Alexa.
        retval["type"] = "Dimmable light"
        retval["modelid"] = "HASS123"
        retval["state"].update({HUE_API_STATE_BRI: HUE_API_STATE_BRI_MAX})

    return retval


def create_hue_success_response(entity_number, attr, value):
    """Create a success response for an attribute set on a light."""
    success_key = f"/lights/{entity_number}/state/{attr}"
    return {"success": {success_key: value}}


def create_config_model(config, request):
    """Create a config resource."""
    return {
        "mac": "00:00:00:00:00:00",
        "swversion": "01003542",
        "apiversion": "1.17.0",
        "whitelist": {HUE_API_USERNAME: {"name": "HASS BRIDGE"}},
        "ipaddress": f"{config.advertise_ip}:{config.advertise_port}",
        "linkbutton": True,
    }


def create_list_of_entities(config, request):
    """Create a list of all entities."""
    hass = request.app["hass"]
    json_response = {}

    for entity in config.filter_exposed_entities(hass.states.async_all()):
        number = config.entity_id_to_number(entity.entity_id)
        json_response[number] = entity_to_json(config, entity)

    return json_response


def hue_brightness_to_hass(value):
    """Convert hue brightness 1..254 to hass format 0..255."""
    return min(255, round((value / HUE_API_STATE_BRI_MAX) * 255))


def hass_to_hue_brightness(value):
    """Convert hass brightness 0..255 to hue 1..254 scale."""
    return max(1, round((value / 255) * HUE_API_STATE_BRI_MAX))


async def wait_for_state_change_or_timeout(hass, entity_id, timeout):
    """Wait for an entity to change state."""
    ev = asyncio.Event()

    @core.callback
    def _async_event_changed(_):
        ev.set()

    unsub = async_track_state_change_event(hass, [entity_id], _async_event_changed)

    try:
        await asyncio.wait_for(ev.wait(), timeout=STATE_CHANGE_WAIT_TIMEOUT)
    except asyncio.TimeoutError:
        pass
    finally:
        unsub()