"""Support for Google Assistant Smart Home API."""
import asyncio
from itertools import product
import logging

from homeassistant.util.decorator import Registry

from homeassistant.const import ATTR_ENTITY_ID

from .const import (
    ERR_PROTOCOL_ERROR, ERR_DEVICE_OFFLINE, ERR_UNKNOWN_ERROR,
    EVENT_COMMAND_RECEIVED, EVENT_SYNC_RECEIVED, EVENT_QUERY_RECEIVED
)
from .helpers import RequestData, GoogleEntity, async_get_entities
from .error import SmartHomeError

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


async def async_handle_message(hass, config, user_id, message):
    """Handle incoming API messages."""
    request_id = message.get('requestId')  # type: str

    data = RequestData(config, user_id, request_id)

    response = await _process(hass, data, message)

    if response and 'errorCode' in response['payload']:
        _LOGGER.error('Error handling message %s: %s',
                      message, response['payload'])

    return response


async def _process(hass, data, message):
    """Process a message."""
    inputs = message.get('inputs')  # type: list

    if len(inputs) != 1:
        return {
            'requestId': data.request_id,
            'payload': {'errorCode': ERR_PROTOCOL_ERROR}
        }

    handler = HANDLERS.get(inputs[0].get('intent'))

    if handler is None:
        return {
            'requestId': data.request_id,
            'payload': {'errorCode': ERR_PROTOCOL_ERROR}
        }

    try:
        result = await handler(hass, data, inputs[0].get('payload'))
    except SmartHomeError as err:
        return {
            'requestId': data.request_id,
            'payload': {'errorCode': err.code}
        }
    except Exception:  # pylint: disable=broad-except
        _LOGGER.exception('Unexpected error')
        return {
            'requestId': data.request_id,
            'payload': {'errorCode': ERR_UNKNOWN_ERROR}
        }

    if result is None:
        return None
    return {'requestId': data.request_id, 'payload': result}


@HANDLERS.register('action.devices.SYNC')
async def async_devices_sync(hass, data, payload):
    """Handle action.devices.SYNC request.

    https://developers.google.com/actions/smarthome/create-app#actiondevicessync
    """
    hass.bus.async_fire(
        EVENT_SYNC_RECEIVED,
        {'request_id': data.request_id},
        context=data.context)

    devices = await asyncio.gather(*[
        entity.sync_serialize() for entity in
        async_get_entities(hass, data.config)
        if data.config.should_expose(entity.state)
    ])

    response = {
        'agentUserId': data.config.agent_user_id or data.context.user_id,
        'devices': devices,
    }

    return response


@HANDLERS.register('action.devices.QUERY')
async def async_devices_query(hass, data, payload):
    """Handle action.devices.QUERY request.

    https://developers.google.com/actions/smarthome/create-app#actiondevicesquery
    """
    devices = {}
    for device in payload.get('devices', []):
        devid = device['id']
        state = hass.states.get(devid)

        hass.bus.async_fire(
            EVENT_QUERY_RECEIVED,
            {
                'request_id': data.request_id,
                ATTR_ENTITY_ID: devid,
            },
            context=data.context)

        if not state:
            # If we can't find a state, the device is offline
            devices[devid] = {'online': False}
            continue

        entity = GoogleEntity(hass, data.config, state)
        devices[devid] = entity.query_serialize()

    return {'devices': devices}


@HANDLERS.register('action.devices.EXECUTE')
async def handle_devices_execute(hass, data, payload):
    """Handle action.devices.EXECUTE request.

    https://developers.google.com/actions/smarthome/create-app#actiondevicesexecute
    """
    entities = {}
    results = {}

    for command in payload['commands']:
        for device, execution in product(command['devices'],
                                         command['execution']):
            entity_id = device['id']

            hass.bus.async_fire(
                EVENT_COMMAND_RECEIVED,
                {
                    'request_id': data.request_id,
                    ATTR_ENTITY_ID: entity_id,
                    'execution': execution
                },
                context=data.context)

            # Happens if error occurred. Skip entity for further processing
            if entity_id in results:
                continue

            if entity_id not in entities:
                state = hass.states.get(entity_id)

                if state is None:
                    results[entity_id] = {
                        'ids': [entity_id],
                        'status': 'ERROR',
                        'errorCode': ERR_DEVICE_OFFLINE
                    }
                    continue

                entities[entity_id] = GoogleEntity(hass, data.config, state)

            try:
                await entities[entity_id].execute(data, execution)
            except SmartHomeError as err:
                results[entity_id] = {
                    'ids': [entity_id],
                    'status': 'ERROR',
                    **err.to_response()
                }

    final_results = list(results.values())

    for entity in entities.values():
        if entity.entity_id in results:
            continue

        entity.async_update()

        final_results.append({
            'ids': [entity.entity_id],
            'status': 'SUCCESS',
            'states': entity.query_serialize(),
        })

    return {'commands': final_results}


@HANDLERS.register('action.devices.DISCONNECT')
async def async_devices_disconnect(hass, data, payload):
    """Handle action.devices.DISCONNECT request.

    https://developers.google.com/actions/smarthome/create#actiondevicesdisconnect
    """
    return None


def turned_off_response(message):
    """Return a device turned off response."""
    return {
        'requestId': message.get('requestId'),
        'payload': {'errorCode': 'deviceTurnedOff'}
    }
