"""Support for alexa Smart Home Skill API."""
import asyncio
import logging
import math
from uuid import uuid4

import homeassistant.core as ha
from homeassistant.const import (
    ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, 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_SET)
from homeassistant.components import (
    alert, automation, cover, fan, group, input_boolean, light, lock,
    media_player, scene, script, switch)
import homeassistant.util.color as color_util
from homeassistant.util.decorator import Registry

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

API_DIRECTIVE = 'directive'
API_ENDPOINT = 'endpoint'
API_EVENT = 'event'
API_HEADER = 'header'
API_PAYLOAD = 'payload'

CONF_DESCRIPTION = 'description'
CONF_DISPLAY_CATEGORIES = 'display_categories'
CONF_NAME = 'name'


MAPPING_COMPONENT = {
    alert.DOMAIN: ['OTHER', ('Alexa.PowerController',), None],
    automation.DOMAIN: ['OTHER', ('Alexa.PowerController',), None],
    cover.DOMAIN: [
        'DOOR', ('Alexa.PowerController',), {
            cover.SUPPORT_SET_POSITION: 'Alexa.PercentageController',
        }
    ],
    fan.DOMAIN: [
        'OTHER', ('Alexa.PowerController',), {
            fan.SUPPORT_SET_SPEED: 'Alexa.PercentageController',
        }
    ],
    group.DOMAIN: ['OTHER', ('Alexa.PowerController',), None],
    input_boolean.DOMAIN: ['OTHER', ('Alexa.PowerController',), None],
    light.DOMAIN: [
        'LIGHT', ('Alexa.PowerController',), {
            light.SUPPORT_BRIGHTNESS: 'Alexa.BrightnessController',
            light.SUPPORT_RGB_COLOR: 'Alexa.ColorController',
            light.SUPPORT_XY_COLOR: 'Alexa.ColorController',
            light.SUPPORT_COLOR_TEMP: 'Alexa.ColorTemperatureController',
        }
    ],
    lock.DOMAIN: ['SMARTLOCK', ('Alexa.LockController',), None],
    media_player.DOMAIN: [
        'TV', ('Alexa.PowerController',), {
            media_player.SUPPORT_VOLUME_SET: 'Alexa.Speaker',
            media_player.SUPPORT_PLAY: 'Alexa.PlaybackController',
            media_player.SUPPORT_PAUSE: 'Alexa.PlaybackController',
            media_player.SUPPORT_STOP: 'Alexa.PlaybackController',
            media_player.SUPPORT_NEXT_TRACK: 'Alexa.PlaybackController',
            media_player.SUPPORT_PREVIOUS_TRACK: 'Alexa.PlaybackController',
        }
    ],
    scene.DOMAIN: ['ACTIVITY_TRIGGER', ('Alexa.SceneController',), None],
    script.DOMAIN: ['OTHER', ('Alexa.PowerController',), None],
    switch.DOMAIN: ['SWITCH', ('Alexa.PowerController',), None],
}


class Config:
    """Hold the configuration for Alexa."""

    def __init__(self, should_expose, entity_config=None):
        """Initialize the configuration."""
        self.should_expose = should_expose
        self.entity_config = entity_config or {}


@asyncio.coroutine
def async_handle_message(hass, config, message):
    """Handle incoming API messages."""
    assert message[API_DIRECTIVE][API_HEADER]['payloadVersion'] == '3'

    # Read head data
    message = message[API_DIRECTIVE]
    namespace = message[API_HEADER]['namespace']
    name = message[API_HEADER]['name']

    # Do we support this API request?
    funct_ref = HANDLERS.get((namespace, name))
    if not funct_ref:
        _LOGGER.warning(
            "Unsupported API request %s/%s", namespace, name)
        return api_error(message)

    return (yield from funct_ref(hass, config, message))


def api_message(request, name='Response', namespace='Alexa', payload=None):
    """Create a API formatted response message.

    Async friendly.
    """
    payload = payload or {}

    response = {
        API_EVENT: {
            API_HEADER: {
                'namespace': namespace,
                'name': name,
                'messageId': str(uuid4()),
                'payloadVersion': '3',
            },
            API_PAYLOAD: payload,
        }
    }

    # If a correlation token exsits, add it to header / Need by Async requests
    token = request[API_HEADER].get('correlationToken')
    if token:
        response[API_EVENT][API_HEADER]['correlationToken'] = token

    # Extend event with endpoint object / Need by Async requests
    if API_ENDPOINT in request:
        response[API_EVENT][API_ENDPOINT] = request[API_ENDPOINT].copy()

    return response


def api_error(request, error_type='INTERNAL_ERROR', error_message=""):
    """Create a API formatted error response.

    Async friendly.
    """
    payload = {
        'type': error_type,
        'message': error_message,
    }

    return api_message(request, name='ErrorResponse', payload=payload)


@HANDLERS.register(('Alexa.Discovery', 'Discover'))
@asyncio.coroutine
def async_api_discovery(hass, config, request):
    """Create a API formatted discovery response.

    Async friendly.
    """
    discovery_endpoints = []

    for entity in hass.states.async_all():
        if not config.should_expose(entity.entity_id):
            _LOGGER.debug("Not exposing %s because filtered by config",
                          entity.entity_id)
            continue

        class_data = MAPPING_COMPONENT.get(entity.domain)

        if not class_data:
            continue

        entity_conf = config.entity_config.get(entity.entity_id, {})

        friendly_name = entity_conf.get(CONF_NAME, entity.name)
        description = entity_conf.get(CONF_DESCRIPTION, entity.entity_id)

        # Required description as per Amazon Scene docs
        if entity.domain == scene.DOMAIN:
            scene_fmt = '{} (Scene connected via Home Assistant)'
            description = scene_fmt.format(description)

        display_categories = entity_conf.get(CONF_DISPLAY_CATEGORIES,
                                             class_data[0])

        endpoint = {
            'displayCategories': [display_categories],
            'additionalApplianceDetails': {},
            'endpointId': entity.entity_id.replace('.', '#'),
            'friendlyName': friendly_name,
            'description': description,
            'manufacturerName': 'Home Assistant',
        }
        actions = set()

        # static actions
        if class_data[1]:
            actions |= set(class_data[1])

        # dynamic actions
        if class_data[2]:
            supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
            for feature, action_name in class_data[2].items():
                if feature & supported > 0:
                    actions.add(action_name)

        # Write action into capabilities
        capabilities = []
        for action in actions:
            capabilities.append({
                'type': 'AlexaInterface',
                'interface': action,
                'version': 3,
            })

        endpoint['capabilities'] = capabilities
        discovery_endpoints.append(endpoint)

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


def extract_entity(funct):
    """Decorator for extract entity object from request."""
    @asyncio.coroutine
    def async_api_entity_wrapper(hass, config, request):
        """Process a turn on request."""
        entity_id = request[API_ENDPOINT]['endpointId'].replace('#', '.')

        # extract state object
        entity = hass.states.get(entity_id)
        if not entity:
            _LOGGER.error("Can't process %s for %s",
                          request[API_HEADER]['name'], entity_id)
            return api_error(request, error_type='NO_SUCH_ENDPOINT')

        return (yield from funct(hass, config, request, entity))

    return async_api_entity_wrapper


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

    service = SERVICE_TURN_ON
    if entity.domain == cover.DOMAIN:
        service = cover.SERVICE_OPEN_COVER

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

    return api_message(request)


@HANDLERS.register(('Alexa.PowerController', 'TurnOff'))
@extract_entity
@asyncio.coroutine
def async_api_turn_off(hass, config, request, entity):
    """Process a turn off request."""
    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

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

    return api_message(request)


@HANDLERS.register(('Alexa.BrightnessController', 'SetBrightness'))
@extract_entity
@asyncio.coroutine
def async_api_set_brightness(hass, config, request, entity):
    """Process a set brightness request."""
    brightness = int(request[API_PAYLOAD]['brightness'])

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

    return api_message(request)


@HANDLERS.register(('Alexa.BrightnessController', 'AdjustBrightness'))
@extract_entity
@asyncio.coroutine
def async_api_adjust_brightness(hass, config, request, entity):
    """Process a adjust brightness request."""
    brightness_delta = int(request[API_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)
    yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
        ATTR_ENTITY_ID: entity.entity_id,
        light.ATTR_BRIGHTNESS_PCT: brightness,
    }, blocking=False)

    return api_message(request)


@HANDLERS.register(('Alexa.ColorController', 'SetColor'))
@extract_entity
@asyncio.coroutine
def async_api_set_color(hass, config, request, entity):
    """Process a set color request."""
    supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES)
    rgb = color_util.color_hsb_to_RGB(
        float(request[API_PAYLOAD]['color']['hue']),
        float(request[API_PAYLOAD]['color']['saturation']),
        float(request[API_PAYLOAD]['color']['brightness'])
    )

    if supported & light.SUPPORT_RGB_COLOR > 0:
        yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
            ATTR_ENTITY_ID: entity.entity_id,
            light.ATTR_RGB_COLOR: rgb,
        }, blocking=False)
    else:
        xyz = color_util.color_RGB_to_xy(*rgb)
        yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
            ATTR_ENTITY_ID: entity.entity_id,
            light.ATTR_XY_COLOR: (xyz[0], xyz[1]),
            light.ATTR_BRIGHTNESS: xyz[2],
        }, blocking=False)

    return api_message(request)


@HANDLERS.register(('Alexa.ColorTemperatureController', 'SetColorTemperature'))
@extract_entity
@asyncio.coroutine
def async_api_set_color_temperature(hass, config, request, entity):
    """Process a set color temperature request."""
    kelvin = int(request[API_PAYLOAD]['colorTemperatureInKelvin'])

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

    return api_message(request)


@HANDLERS.register(
    ('Alexa.ColorTemperatureController', 'DecreaseColorTemperature'))
@extract_entity
@asyncio.coroutine
def async_api_decrease_color_temp(hass, config, request, entity):
    """Process a decrease color temperature request."""
    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)
    yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
        ATTR_ENTITY_ID: entity.entity_id,
        light.ATTR_COLOR_TEMP: value,
    }, blocking=False)

    return api_message(request)


@HANDLERS.register(
    ('Alexa.ColorTemperatureController', 'IncreaseColorTemperature'))
@extract_entity
@asyncio.coroutine
def async_api_increase_color_temp(hass, config, request, entity):
    """Process a increase color temperature request."""
    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)
    yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
        ATTR_ENTITY_ID: entity.entity_id,
        light.ATTR_COLOR_TEMP: value,
    }, blocking=False)

    return api_message(request)


@HANDLERS.register(('Alexa.SceneController', 'Activate'))
@extract_entity
@asyncio.coroutine
def async_api_activate(hass, config, request, entity):
    """Process a activate request."""
    yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
        ATTR_ENTITY_ID: entity.entity_id
    }, blocking=False)

    return api_message(request)


@HANDLERS.register(('Alexa.PercentageController', 'SetPercentage'))
@extract_entity
@asyncio.coroutine
def async_api_set_percentage(hass, config, request, entity):
    """Process a set percentage request."""
    percentage = int(request[API_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

    yield from hass.services.async_call(
        entity.domain, service, data, blocking=False)

    return api_message(request)


@HANDLERS.register(('Alexa.PercentageController', 'AdjustPercentage'))
@extract_entity
@asyncio.coroutine
def async_api_adjust_percentage(hass, config, request, entity):
    """Process a adjust percentage request."""
    percentage_delta = int(request[API_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)

    yield from hass.services.async_call(
        entity.domain, service, data, blocking=False)

    return api_message(request)


@HANDLERS.register(('Alexa.LockController', 'Lock'))
@extract_entity
@asyncio.coroutine
def async_api_lock(hass, config, request, entity):
    """Process a lock request."""
    yield from hass.services.async_call(entity.domain, SERVICE_LOCK, {
        ATTR_ENTITY_ID: entity.entity_id
    }, blocking=False)

    return api_message(request)


# Not supported by Alexa yet
@HANDLERS.register(('Alexa.LockController', 'Unlock'))
@extract_entity
@asyncio.coroutine
def async_api_unlock(hass, config, request, entity):
    """Process a unlock request."""
    yield from hass.services.async_call(entity.domain, SERVICE_UNLOCK, {
        ATTR_ENTITY_ID: entity.entity_id
    }, blocking=False)

    return api_message(request)


@HANDLERS.register(('Alexa.Speaker', 'SetVolume'))
@extract_entity
@asyncio.coroutine
def async_api_set_volume(hass, config, request, entity):
    """Process a set volume request."""
    volume = round(float(request[API_PAYLOAD]['volume'] / 100), 2)

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

    yield from hass.services.async_call(
        entity.domain, SERVICE_VOLUME_SET,
        data, blocking=False)

    return api_message(request)


@HANDLERS.register(('Alexa.Speaker', 'AdjustVolume'))
@extract_entity
@asyncio.coroutine
def async_api_adjust_volume(hass, config, request, entity):
    """Process a adjust volume request."""
    volume_delta = int(request[API_PAYLOAD]['volume'])

    current_level = entity.attributes.get(media_player.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.ATTR_MEDIA_VOLUME_LEVEL: volume,
    }

    yield from hass.services.async_call(
        entity.domain, media_player.SERVICE_VOLUME_SET,
        data, blocking=False)

    return api_message(request)


@HANDLERS.register(('Alexa.Speaker', 'SetMute'))
@extract_entity
@asyncio.coroutine
def async_api_set_mute(hass, config, request, entity):
    """Process a set mute request."""
    mute = bool(request[API_PAYLOAD]['mute'])

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

    yield from hass.services.async_call(
        entity.domain, media_player.SERVICE_VOLUME_MUTE,
        data, blocking=False)

    return api_message(request)


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

    yield from hass.services.async_call(
        entity.domain, SERVICE_MEDIA_PLAY,
        data, blocking=False)

    return api_message(request)


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

    yield from hass.services.async_call(
        entity.domain, SERVICE_MEDIA_PAUSE,
        data, blocking=False)

    return api_message(request)


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

    yield from hass.services.async_call(
        entity.domain, SERVICE_MEDIA_STOP,
        data, blocking=False)

    return api_message(request)


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

    yield from hass.services.async_call(
        entity.domain, SERVICE_MEDIA_NEXT_TRACK,
        data, blocking=False)

    return api_message(request)


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

    yield from hass.services.async_call(
        entity.domain, SERVICE_MEDIA_PREVIOUS_TRACK,
        data, blocking=False)

    return api_message(request)
