"""Alexa message handlers."""
from datetime import datetime
import logging
import math

from homeassistant import core as ha
from homeassistant.components import cover, fan, group, light, media_player
from homeassistant.components.climate import const as climate
from homeassistant.const import (
    ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, SERVICE_LOCK,
    SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY,
    SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP,
    SERVICE_SET_COVER_POSITION, SERVICE_TURN_OFF, SERVICE_TURN_ON,
    SERVICE_UNLOCK, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_MUTE,
    SERVICE_VOLUME_SET, SERVICE_VOLUME_UP, TEMP_CELSIUS, TEMP_FAHRENHEIT)
import homeassistant.util.color as color_util
from homeassistant.util.decorator import Registry
from homeassistant.util.temperature import convert as convert_temperature

from .const import (
    API_TEMP_UNITS, API_THERMOSTAT_MODES, API_THERMOSTAT_PRESETS, Cause)
from .entities import async_get_entities
from .errors import (
    AlexaInvalidValueError, AlexaTempRangeError,
    AlexaUnsupportedThermostatModeError)
from .state_report import async_enable_proactive_mode

_LOGGER = logging.getLogger(__name__)
HANDLERS = Registry()


@HANDLERS.register(('Alexa.Discovery', 'Discover'))
async def async_api_discovery(hass, config, directive, context):
    """Create a API formatted discovery response.

    Async friendly.
    """
    discovery_endpoints = [
        alexa_entity.serialize_discovery()
        for alexa_entity in async_get_entities(hass, config)
        if config.should_expose(alexa_entity.entity_id)
    ]

    return directive.response(
        name='Discover.Response',
        namespace='Alexa.Discovery',
        payload={'endpoints': discovery_endpoints},
    )


@HANDLERS.register(('Alexa.Authorization', 'AcceptGrant'))
async def async_api_accept_grant(hass, config, directive, context):
    """Create a API formatted AcceptGrant response.

    Async friendly.
    """
    auth_code = directive.payload['grant']['code']
    _LOGGER.debug("AcceptGrant code: %s", auth_code)

    if config.supports_auth:
        await config.async_accept_grant(auth_code)

        if config.should_report_state:
            await async_enable_proactive_mode(hass, config)

    return directive.response(
        name='AcceptGrant.Response',
        namespace='Alexa.Authorization',
        payload={})


@HANDLERS.register(('Alexa.PowerController', 'TurnOn'))
async def async_api_turn_on(hass, config, directive, context):
    """Process a turn on request."""
    entity = directive.entity
    domain = entity.domain
    if domain == group.DOMAIN:
        domain = ha.DOMAIN

    service = SERVICE_TURN_ON
    if domain == cover.DOMAIN:
        service = cover.SERVICE_OPEN_COVER
    elif domain == media_player.DOMAIN:
        supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
        power_features = (media_player.SUPPORT_TURN_ON |
                          media_player.SUPPORT_TURN_OFF)
        if not supported & power_features:
            service = media_player.SERVICE_MEDIA_PLAY

    await hass.services.async_call(domain, service, {
        ATTR_ENTITY_ID: entity.entity_id
    }, blocking=False, context=context)

    return directive.response()


@HANDLERS.register(('Alexa.PowerController', 'TurnOff'))
async def async_api_turn_off(hass, config, directive, context):
    """Process a turn off request."""
    entity = directive.entity
    domain = entity.domain
    if entity.domain == group.DOMAIN:
        domain = ha.DOMAIN

    service = SERVICE_TURN_OFF
    if entity.domain == cover.DOMAIN:
        service = cover.SERVICE_CLOSE_COVER
    elif domain == media_player.DOMAIN:
        supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
        power_features = (media_player.SUPPORT_TURN_ON |
                          media_player.SUPPORT_TURN_OFF)
        if not supported & power_features:
            service = media_player.SERVICE_MEDIA_STOP

    await hass.services.async_call(domain, service, {
        ATTR_ENTITY_ID: entity.entity_id
    }, blocking=False, context=context)

    return directive.response()


@HANDLERS.register(('Alexa.BrightnessController', 'SetBrightness'))
async def async_api_set_brightness(hass, config, directive, context):
    """Process a set brightness request."""
    entity = directive.entity
    brightness = int(directive.payload['brightness'])

    await hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
        ATTR_ENTITY_ID: entity.entity_id,
        light.ATTR_BRIGHTNESS_PCT: brightness,
    }, blocking=False, context=context)

    return directive.response()


@HANDLERS.register(('Alexa.BrightnessController', 'AdjustBrightness'))
async def async_api_adjust_brightness(hass, config, directive, context):
    """Process an adjust brightness request."""
    entity = directive.entity
    brightness_delta = int(directive.payload['brightnessDelta'])

    # read current state
    try:
        current = math.floor(
            int(entity.attributes.get(light.ATTR_BRIGHTNESS)) / 255 * 100)
    except ZeroDivisionError:
        current = 0

    # set brightness
    brightness = max(0, brightness_delta + current)
    await hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
        ATTR_ENTITY_ID: entity.entity_id,
        light.ATTR_BRIGHTNESS_PCT: brightness,
    }, blocking=False, context=context)

    return directive.response()


@HANDLERS.register(('Alexa.ColorController', 'SetColor'))
async def async_api_set_color(hass, config, directive, context):
    """Process a set color request."""
    entity = directive.entity
    rgb = color_util.color_hsb_to_RGB(
        float(directive.payload['color']['hue']),
        float(directive.payload['color']['saturation']),
        float(directive.payload['color']['brightness'])
    )

    await hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
        ATTR_ENTITY_ID: entity.entity_id,
        light.ATTR_RGB_COLOR: rgb,
    }, blocking=False, context=context)

    return directive.response()


@HANDLERS.register(('Alexa.ColorTemperatureController', 'SetColorTemperature'))
async def async_api_set_color_temperature(hass, config, directive, context):
    """Process a set color temperature request."""
    entity = directive.entity
    kelvin = int(directive.payload['colorTemperatureInKelvin'])

    await hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
        ATTR_ENTITY_ID: entity.entity_id,
        light.ATTR_KELVIN: kelvin,
    }, blocking=False, context=context)

    return directive.response()


@HANDLERS.register(
    ('Alexa.ColorTemperatureController', 'DecreaseColorTemperature'))
async def async_api_decrease_color_temp(hass, config, directive, context):
    """Process a decrease color temperature request."""
    entity = directive.entity
    current = int(entity.attributes.get(light.ATTR_COLOR_TEMP))
    max_mireds = int(entity.attributes.get(light.ATTR_MAX_MIREDS))

    value = min(max_mireds, current + 50)
    await hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
        ATTR_ENTITY_ID: entity.entity_id,
        light.ATTR_COLOR_TEMP: value,
    }, blocking=False, context=context)

    return directive.response()


@HANDLERS.register(
    ('Alexa.ColorTemperatureController', 'IncreaseColorTemperature'))
async def async_api_increase_color_temp(hass, config, directive, context):
    """Process an increase color temperature request."""
    entity = directive.entity
    current = int(entity.attributes.get(light.ATTR_COLOR_TEMP))
    min_mireds = int(entity.attributes.get(light.ATTR_MIN_MIREDS))

    value = max(min_mireds, current - 50)
    await hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
        ATTR_ENTITY_ID: entity.entity_id,
        light.ATTR_COLOR_TEMP: value,
    }, blocking=False, context=context)

    return directive.response()


@HANDLERS.register(('Alexa.SceneController', 'Activate'))
async def async_api_activate(hass, config, directive, context):
    """Process an activate request."""
    entity = directive.entity
    domain = entity.domain

    await hass.services.async_call(domain, SERVICE_TURN_ON, {
        ATTR_ENTITY_ID: entity.entity_id
    }, blocking=False, context=context)

    payload = {
        'cause': {'type': Cause.VOICE_INTERACTION},
        'timestamp': '%sZ' % (datetime.utcnow().isoformat(),)
    }

    return directive.response(
        name='ActivationStarted',
        namespace='Alexa.SceneController',
        payload=payload,
    )


@HANDLERS.register(('Alexa.SceneController', 'Deactivate'))
async def async_api_deactivate(hass, config, directive, context):
    """Process a deactivate request."""
    entity = directive.entity
    domain = entity.domain

    await hass.services.async_call(domain, SERVICE_TURN_OFF, {
        ATTR_ENTITY_ID: entity.entity_id
    }, blocking=False, context=context)

    payload = {
        'cause': {'type': Cause.VOICE_INTERACTION},
        'timestamp': '%sZ' % (datetime.utcnow().isoformat(),)
    }

    return directive.response(
        name='DeactivationStarted',
        namespace='Alexa.SceneController',
        payload=payload,
    )


@HANDLERS.register(('Alexa.PercentageController', 'SetPercentage'))
async def async_api_set_percentage(hass, config, directive, context):
    """Process a set percentage request."""
    entity = directive.entity
    percentage = int(directive.payload['percentage'])
    service = None
    data = {ATTR_ENTITY_ID: entity.entity_id}

    if entity.domain == fan.DOMAIN:
        service = fan.SERVICE_SET_SPEED
        speed = "off"

        if percentage <= 33:
            speed = "low"
        elif percentage <= 66:
            speed = "medium"
        elif percentage <= 100:
            speed = "high"
        data[fan.ATTR_SPEED] = speed

    elif entity.domain == cover.DOMAIN:
        service = SERVICE_SET_COVER_POSITION
        data[cover.ATTR_POSITION] = percentage

    await hass.services.async_call(
        entity.domain, service, data, blocking=False, context=context)

    return directive.response()


@HANDLERS.register(('Alexa.PercentageController', 'AdjustPercentage'))
async def async_api_adjust_percentage(hass, config, directive, context):
    """Process an adjust percentage request."""
    entity = directive.entity
    percentage_delta = int(directive.payload['percentageDelta'])
    service = None
    data = {ATTR_ENTITY_ID: entity.entity_id}

    if entity.domain == fan.DOMAIN:
        service = fan.SERVICE_SET_SPEED
        speed = entity.attributes.get(fan.ATTR_SPEED)

        if speed == "off":
            current = 0
        elif speed == "low":
            current = 33
        elif speed == "medium":
            current = 66
        elif speed == "high":
            current = 100

        # set percentage
        percentage = max(0, percentage_delta + current)
        speed = "off"

        if percentage <= 33:
            speed = "low"
        elif percentage <= 66:
            speed = "medium"
        elif percentage <= 100:
            speed = "high"

        data[fan.ATTR_SPEED] = speed

    elif entity.domain == cover.DOMAIN:
        service = SERVICE_SET_COVER_POSITION

        current = entity.attributes.get(cover.ATTR_POSITION)

        data[cover.ATTR_POSITION] = max(0, percentage_delta + current)

    await hass.services.async_call(
        entity.domain, service, data, blocking=False, context=context)

    return directive.response()


@HANDLERS.register(('Alexa.LockController', 'Lock'))
async def async_api_lock(hass, config, directive, context):
    """Process a lock request."""
    entity = directive.entity
    await hass.services.async_call(entity.domain, SERVICE_LOCK, {
        ATTR_ENTITY_ID: entity.entity_id
    }, blocking=False, context=context)

    response = directive.response()
    response.add_context_property({
        'name': 'lockState',
        'namespace': 'Alexa.LockController',
        'value': 'LOCKED'
    })
    return response


# Not supported by Alexa yet
@HANDLERS.register(('Alexa.LockController', 'Unlock'))
async def async_api_unlock(hass, config, directive, context):
    """Process an unlock request."""
    entity = directive.entity
    await hass.services.async_call(entity.domain, SERVICE_UNLOCK, {
        ATTR_ENTITY_ID: entity.entity_id
    }, blocking=False, context=context)

    return directive.response()


@HANDLERS.register(('Alexa.Speaker', 'SetVolume'))
async def async_api_set_volume(hass, config, directive, context):
    """Process a set volume request."""
    volume = round(float(directive.payload['volume'] / 100), 2)
    entity = directive.entity

    data = {
        ATTR_ENTITY_ID: entity.entity_id,
        media_player.const.ATTR_MEDIA_VOLUME_LEVEL: volume,
    }

    await hass.services.async_call(
        entity.domain, SERVICE_VOLUME_SET,
        data, blocking=False, context=context)

    return directive.response()


@HANDLERS.register(('Alexa.InputController', 'SelectInput'))
async def async_api_select_input(hass, config, directive, context):
    """Process a set input request."""
    media_input = directive.payload['input']
    entity = directive.entity

    # attempt to map the ALL UPPERCASE payload name to a source
    source_list = entity.attributes[
        media_player.const.ATTR_INPUT_SOURCE_LIST] or []
    for source in source_list:
        # response will always be space separated, so format the source in the
        # most likely way to find a match
        formatted_source = source.lower().replace('-', ' ').replace('_', ' ')
        if formatted_source in media_input.lower():
            media_input = source
            break
    else:
        msg = 'failed to map input {} to a media source on {}'.format(
            media_input, entity.entity_id)
        raise AlexaInvalidValueError(msg)

    data = {
        ATTR_ENTITY_ID: entity.entity_id,
        media_player.const.ATTR_INPUT_SOURCE: media_input,
    }

    await hass.services.async_call(
        entity.domain, media_player.SERVICE_SELECT_SOURCE,
        data, blocking=False, context=context)

    return directive.response()


@HANDLERS.register(('Alexa.Speaker', 'AdjustVolume'))
async def async_api_adjust_volume(hass, config, directive, context):
    """Process an adjust volume request."""
    volume_delta = int(directive.payload['volume'])

    entity = directive.entity
    current_level = entity.attributes.get(
        media_player.const.ATTR_MEDIA_VOLUME_LEVEL)

    # read current state
    try:
        current = math.floor(int(current_level * 100))
    except ZeroDivisionError:
        current = 0

    volume = float(max(0, volume_delta + current) / 100)

    data = {
        ATTR_ENTITY_ID: entity.entity_id,
        media_player.const.ATTR_MEDIA_VOLUME_LEVEL: volume,
    }

    await hass.services.async_call(
        entity.domain, SERVICE_VOLUME_SET,
        data, blocking=False, context=context)

    return directive.response()


@HANDLERS.register(('Alexa.StepSpeaker', 'AdjustVolume'))
async def async_api_adjust_volume_step(hass, config, directive, context):
    """Process an adjust volume step request."""
    # media_player volume up/down service does not support specifying steps
    # each component handles it differently e.g. via config.
    # For now we use the volumeSteps returned to figure out if we
    # should step up/down
    volume_step = directive.payload['volumeSteps']
    entity = directive.entity

    data = {
        ATTR_ENTITY_ID: entity.entity_id,
    }

    if volume_step > 0:
        await hass.services.async_call(
            entity.domain, SERVICE_VOLUME_UP,
            data, blocking=False, context=context)
    elif volume_step < 0:
        await hass.services.async_call(
            entity.domain, SERVICE_VOLUME_DOWN,
            data, blocking=False, context=context)

    return directive.response()


@HANDLERS.register(('Alexa.StepSpeaker', 'SetMute'))
@HANDLERS.register(('Alexa.Speaker', 'SetMute'))
async def async_api_set_mute(hass, config, directive, context):
    """Process a set mute request."""
    mute = bool(directive.payload['mute'])
    entity = directive.entity

    data = {
        ATTR_ENTITY_ID: entity.entity_id,
        media_player.const.ATTR_MEDIA_VOLUME_MUTED: mute,
    }

    await hass.services.async_call(
        entity.domain, SERVICE_VOLUME_MUTE,
        data, blocking=False, context=context)

    return directive.response()


@HANDLERS.register(('Alexa.PlaybackController', 'Play'))
async def async_api_play(hass, config, directive, context):
    """Process a play request."""
    entity = directive.entity
    data = {
        ATTR_ENTITY_ID: entity.entity_id
    }

    await hass.services.async_call(
        entity.domain, SERVICE_MEDIA_PLAY,
        data, blocking=False, context=context)

    return directive.response()


@HANDLERS.register(('Alexa.PlaybackController', 'Pause'))
async def async_api_pause(hass, config, directive, context):
    """Process a pause request."""
    entity = directive.entity
    data = {
        ATTR_ENTITY_ID: entity.entity_id
    }

    await hass.services.async_call(
        entity.domain, SERVICE_MEDIA_PAUSE,
        data, blocking=False, context=context)

    return directive.response()


@HANDLERS.register(('Alexa.PlaybackController', 'Stop'))
async def async_api_stop(hass, config, directive, context):
    """Process a stop request."""
    entity = directive.entity
    data = {
        ATTR_ENTITY_ID: entity.entity_id
    }

    await hass.services.async_call(
        entity.domain, SERVICE_MEDIA_STOP,
        data, blocking=False, context=context)

    return directive.response()


@HANDLERS.register(('Alexa.PlaybackController', 'Next'))
async def async_api_next(hass, config, directive, context):
    """Process a next request."""
    entity = directive.entity
    data = {
        ATTR_ENTITY_ID: entity.entity_id
    }

    await hass.services.async_call(
        entity.domain, SERVICE_MEDIA_NEXT_TRACK,
        data, blocking=False, context=context)

    return directive.response()


@HANDLERS.register(('Alexa.PlaybackController', 'Previous'))
async def async_api_previous(hass, config, directive, context):
    """Process a previous request."""
    entity = directive.entity
    data = {
        ATTR_ENTITY_ID: entity.entity_id
    }

    await hass.services.async_call(
        entity.domain, SERVICE_MEDIA_PREVIOUS_TRACK,
        data, blocking=False, context=context)

    return directive.response()


def temperature_from_object(hass, temp_obj, interval=False):
    """Get temperature from Temperature object in requested unit."""
    to_unit = hass.config.units.temperature_unit
    from_unit = TEMP_CELSIUS
    temp = float(temp_obj['value'])

    if temp_obj['scale'] == 'FAHRENHEIT':
        from_unit = TEMP_FAHRENHEIT
    elif temp_obj['scale'] == 'KELVIN':
        # convert to Celsius if absolute temperature
        if not interval:
            temp -= 273.15

    return convert_temperature(temp, from_unit, to_unit, interval)


@HANDLERS.register(('Alexa.ThermostatController', 'SetTargetTemperature'))
async def async_api_set_target_temp(hass, config, directive, context):
    """Process a set target temperature request."""
    entity = directive.entity
    min_temp = entity.attributes.get(climate.ATTR_MIN_TEMP)
    max_temp = entity.attributes.get(climate.ATTR_MAX_TEMP)
    unit = hass.config.units.temperature_unit

    data = {
        ATTR_ENTITY_ID: entity.entity_id
    }

    payload = directive.payload
    response = directive.response()
    if 'targetSetpoint' in payload:
        temp = temperature_from_object(hass, payload['targetSetpoint'])
        if temp < min_temp or temp > max_temp:
            raise AlexaTempRangeError(hass, temp, min_temp, max_temp)
        data[ATTR_TEMPERATURE] = temp
        response.add_context_property({
            'name': 'targetSetpoint',
            'namespace': 'Alexa.ThermostatController',
            'value': {'value': temp, 'scale': API_TEMP_UNITS[unit]},
        })
    if 'lowerSetpoint' in payload:
        temp_low = temperature_from_object(hass, payload['lowerSetpoint'])
        if temp_low < min_temp or temp_low > max_temp:
            raise AlexaTempRangeError(hass, temp_low, min_temp, max_temp)
        data[climate.ATTR_TARGET_TEMP_LOW] = temp_low
        response.add_context_property({
            'name': 'lowerSetpoint',
            'namespace': 'Alexa.ThermostatController',
            'value': {'value': temp_low, 'scale': API_TEMP_UNITS[unit]},
        })
    if 'upperSetpoint' in payload:
        temp_high = temperature_from_object(hass, payload['upperSetpoint'])
        if temp_high < min_temp or temp_high > max_temp:
            raise AlexaTempRangeError(hass, temp_high, min_temp, max_temp)
        data[climate.ATTR_TARGET_TEMP_HIGH] = temp_high
        response.add_context_property({
            'name': 'upperSetpoint',
            'namespace': 'Alexa.ThermostatController',
            'value': {'value': temp_high, 'scale': API_TEMP_UNITS[unit]},
        })

    await hass.services.async_call(
        entity.domain, climate.SERVICE_SET_TEMPERATURE, data, blocking=False,
        context=context)

    return response


@HANDLERS.register(('Alexa.ThermostatController', 'AdjustTargetTemperature'))
async def async_api_adjust_target_temp(hass, config, directive, context):
    """Process an adjust target temperature request."""
    entity = directive.entity
    min_temp = entity.attributes.get(climate.ATTR_MIN_TEMP)
    max_temp = entity.attributes.get(climate.ATTR_MAX_TEMP)
    unit = hass.config.units.temperature_unit

    temp_delta = temperature_from_object(
        hass, directive.payload['targetSetpointDelta'], interval=True)
    target_temp = float(entity.attributes.get(ATTR_TEMPERATURE)) + temp_delta

    if target_temp < min_temp or target_temp > max_temp:
        raise AlexaTempRangeError(hass, target_temp, min_temp, max_temp)

    data = {
        ATTR_ENTITY_ID: entity.entity_id,
        ATTR_TEMPERATURE: target_temp,
    }

    response = directive.response()
    await hass.services.async_call(
        entity.domain, climate.SERVICE_SET_TEMPERATURE, data, blocking=False,
        context=context)
    response.add_context_property({
        'name': 'targetSetpoint',
        'namespace': 'Alexa.ThermostatController',
        'value': {'value': target_temp, 'scale': API_TEMP_UNITS[unit]},
    })

    return response


@HANDLERS.register(('Alexa.ThermostatController', 'SetThermostatMode'))
async def async_api_set_thermostat_mode(hass, config, directive, context):
    """Process a set thermostat mode request."""
    entity = directive.entity
    mode = directive.payload['thermostatMode']
    mode = mode if isinstance(mode, str) else mode['value']

    data = {
        ATTR_ENTITY_ID: entity.entity_id,
    }

    ha_preset = next(
        (k for k, v in API_THERMOSTAT_PRESETS.items() if v == mode),
        None
    )

    if ha_preset:
        presets = entity.attributes.get(climate.ATTR_PRESET_MODES, [])

        if ha_preset not in presets:
            msg = 'The requested thermostat mode {} is not supported'.format(
                ha_preset
            )
            raise AlexaUnsupportedThermostatModeError(msg)

        service = climate.SERVICE_SET_PRESET_MODE
        data[climate.ATTR_PRESET_MODE] = climate.PRESET_ECO

    else:
        operation_list = entity.attributes.get(climate.ATTR_HVAC_MODES)
        ha_mode = next(
            (k for k, v in API_THERMOSTAT_MODES.items() if v == mode),
            None
        )
        if ha_mode not in operation_list:
            msg = 'The requested thermostat mode {} is not supported'.format(
                mode
            )
            raise AlexaUnsupportedThermostatModeError(msg)

        service = climate.SERVICE_SET_HVAC_MODE
        data[climate.ATTR_HVAC_MODE] = ha_mode

    response = directive.response()
    await hass.services.async_call(
        climate.DOMAIN, service, data,
        blocking=False, context=context)
    response.add_context_property({
        'name': 'thermostatMode',
        'namespace': 'Alexa.ThermostatController',
        'value': mode,
    })

    return response


@HANDLERS.register(('Alexa', 'ReportState'))
async def async_api_reportstate(hass, config, directive, context):
    """Process a ReportState request."""
    return directive.response(name='StateReport')
