"""
Support for Alexa skill service end point.

For more details about this component, please refer to the documentation at
https://home-assistant.io/components/alexa/
"""
import enum
import logging

from homeassistant.components import http
from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import intent
from homeassistant.util.decorator import Registry

from .const import DOMAIN, SYN_RESOLUTION_MATCH

_LOGGER = logging.getLogger(__name__)

HANDLERS = Registry()

INTENTS_API_ENDPOINT = '/api/alexa'


class SpeechType(enum.Enum):
    """The Alexa speech types."""

    plaintext = 'PlainText'
    ssml = 'SSML'


SPEECH_MAPPINGS = {
    'plain': SpeechType.plaintext,
    'ssml': SpeechType.ssml,
}


class CardType(enum.Enum):
    """The Alexa card types."""

    simple = 'Simple'
    link_account = 'LinkAccount'


@callback
def async_setup(hass):
    """Activate Alexa component."""
    hass.http.register_view(AlexaIntentsView)


class UnknownRequest(HomeAssistantError):
    """When an unknown Alexa request is passed in."""


class AlexaIntentsView(http.HomeAssistantView):
    """Handle Alexa requests."""

    url = INTENTS_API_ENDPOINT
    name = 'api:alexa'

    async def post(self, request):
        """Handle Alexa."""
        hass = request.app['hass']
        message = await request.json()

        _LOGGER.debug("Received Alexa request: %s", message)

        try:
            response = await async_handle_message(hass, message)
            return b'' if response is None else self.json(response)
        except UnknownRequest as err:
            _LOGGER.warning(str(err))
            return self.json(intent_error_response(
                hass, message, str(err)))

        except intent.UnknownIntent as err:
            _LOGGER.warning(str(err))
            return self.json(intent_error_response(
                hass, message,
                "This intent is not yet configured within Home Assistant."))

        except intent.InvalidSlotInfo as err:
            _LOGGER.error("Received invalid slot data from Alexa: %s", err)
            return self.json(intent_error_response(
                hass, message,
                "Invalid slot information received for this intent."))

        except intent.IntentError as err:
            _LOGGER.exception(str(err))
            return self.json(intent_error_response(
                hass, message, "Error handling intent."))


def intent_error_response(hass, message, error):
    """Return an Alexa response that will speak the error message."""
    alexa_intent_info = message.get('request').get('intent')
    alexa_response = AlexaResponse(hass, alexa_intent_info)
    alexa_response.add_speech(SpeechType.plaintext, error)
    return alexa_response.as_dict()


async def async_handle_message(hass, message):
    """Handle an Alexa intent.

    Raises:
     - UnknownRequest
     - intent.UnknownIntent
     - intent.InvalidSlotInfo
     - intent.IntentError

    """
    req = message.get('request')
    req_type = req['type']

    handler = HANDLERS.get(req_type)

    if not handler:
        raise UnknownRequest('Received unknown request {}'.format(req_type))

    return await handler(hass, message)


@HANDLERS.register('SessionEndedRequest')
async def async_handle_session_end(hass, message):
    """Handle a session end request."""
    return None


@HANDLERS.register('IntentRequest')
@HANDLERS.register('LaunchRequest')
async def async_handle_intent(hass, message):
    """Handle an intent request.

    Raises:
     - intent.UnknownIntent
     - intent.InvalidSlotInfo
     - intent.IntentError

    """
    req = message.get('request')
    alexa_intent_info = req.get('intent')
    alexa_response = AlexaResponse(hass, alexa_intent_info)

    if req['type'] == 'LaunchRequest':
        intent_name = message.get('session', {})  \
                             .get('application', {})  \
                             .get('applicationId')
    else:
        intent_name = alexa_intent_info['name']

    intent_response = await intent.async_handle(
        hass, DOMAIN, intent_name,
        {key: {'value': value} for key, value
         in alexa_response.variables.items()})

    for intent_speech, alexa_speech in SPEECH_MAPPINGS.items():
        if intent_speech in intent_response.speech:
            alexa_response.add_speech(
                alexa_speech,
                intent_response.speech[intent_speech]['speech'])
            break

    if 'simple' in intent_response.card:
        alexa_response.add_card(
            CardType.simple, intent_response.card['simple']['title'],
            intent_response.card['simple']['content'])

    return alexa_response.as_dict()


def resolve_slot_synonyms(key, request):
    """Check slot request for synonym resolutions."""
    # Default to the spoken slot value if more than one or none are found. For
    # reference to the request object structure, see the Alexa docs:
    # https://tinyurl.com/ybvm7jhs
    resolved_value = request['value']

    if ('resolutions' in request and
            'resolutionsPerAuthority' in request['resolutions'] and
            len(request['resolutions']['resolutionsPerAuthority']) >= 1):

        # Extract all of the possible values from each authority with a
        # successful match
        possible_values = []

        for entry in request['resolutions']['resolutionsPerAuthority']:
            if entry['status']['code'] != SYN_RESOLUTION_MATCH:
                continue

            possible_values.extend([item['value']['name']
                                    for item
                                    in entry['values']])

        # If there is only one match use the resolved value, otherwise the
        # resolution cannot be determined, so use the spoken slot value
        if len(possible_values) == 1:
            resolved_value = possible_values[0]
        else:
            _LOGGER.debug(
                'Found multiple synonym resolutions for slot value: {%s: %s}',
                key,
                request['value']
            )

    return resolved_value


class AlexaResponse:
    """Help generating the response for Alexa."""

    def __init__(self, hass, intent_info):
        """Initialize the response."""
        self.hass = hass
        self.speech = None
        self.card = None
        self.reprompt = None
        self.session_attributes = {}
        self.should_end_session = True
        self.variables = {}

        # Intent is None if request was a LaunchRequest or SessionEndedRequest
        if intent_info is not None:
            for key, value in intent_info.get('slots', {}).items():
                # Only include slots with values
                if 'value' not in value:
                    continue

                _key = key.replace('.', '_')

                self.variables[_key] = resolve_slot_synonyms(key, value)

    def add_card(self, card_type, title, content):
        """Add a card to the response."""
        assert self.card is None

        card = {
            "type": card_type.value
        }

        if card_type == CardType.link_account:
            self.card = card
            return

        card["title"] = title
        card["content"] = content
        self.card = card

    def add_speech(self, speech_type, text):
        """Add speech to the response."""
        assert self.speech is None

        key = 'ssml' if speech_type == SpeechType.ssml else 'text'

        self.speech = {
            'type': speech_type.value,
            key: text
        }

    def add_reprompt(self, speech_type, text):
        """Add reprompt if user does not answer."""
        assert self.reprompt is None

        key = 'ssml' if speech_type == SpeechType.ssml else 'text'

        self.reprompt = {
            'type': speech_type.value,
            key: text.async_render(self.variables)
        }

    def as_dict(self):
        """Return response in an Alexa valid dict."""
        response = {
            'shouldEndSession': self.should_end_session
        }

        if self.card is not None:
            response['card'] = self.card

        if self.speech is not None:
            response['outputSpeech'] = self.speech

        if self.reprompt is not None:
            response['reprompt'] = {
                'outputSpeech': self.reprompt
            }

        return {
            'version': '1.0',
            'sessionAttributes': self.session_attributes,
            'response': response,
        }
