"""Rest API for Home Assistant."""
import asyncio
import json
import logging

from aiohttp import web
from aiohttp.web_exceptions import HTTPBadRequest
import async_timeout
import voluptuous as vol

from homeassistant.bootstrap import DATA_LOGGING
from homeassistant.components.http import HomeAssistantView
from homeassistant.const import (
    EVENT_HOMEASSISTANT_STOP, EVENT_TIME_CHANGED, HTTP_BAD_REQUEST,
    HTTP_CREATED, HTTP_NOT_FOUND, MATCH_ALL, URL_API, URL_API_COMPONENTS,
    URL_API_CONFIG, URL_API_DISCOVERY_INFO, URL_API_ERROR_LOG, URL_API_EVENTS,
    URL_API_SERVICES, URL_API_STATES, URL_API_STATES_ENTITY, URL_API_STREAM,
    URL_API_TEMPLATE, __version__)
import homeassistant.core as ha
from homeassistant.auth.permissions.const import POLICY_READ
from homeassistant.exceptions import (
    TemplateError, Unauthorized, ServiceNotFound)
from homeassistant.helpers import template
from homeassistant.helpers.service import async_get_all_descriptions
from homeassistant.helpers.state import AsyncTrackStates
from homeassistant.helpers.json import JSONEncoder

_LOGGER = logging.getLogger(__name__)

ATTR_BASE_URL = 'base_url'
ATTR_LOCATION_NAME = 'location_name'
ATTR_REQUIRES_API_PASSWORD = 'requires_api_password'
ATTR_VERSION = 'version'

DOMAIN = 'api'
STREAM_PING_PAYLOAD = 'ping'
STREAM_PING_INTERVAL = 50  # seconds


def setup(hass, config):
    """Register the API with the HTTP interface."""
    hass.http.register_view(APIStatusView)
    hass.http.register_view(APIEventStream)
    hass.http.register_view(APIConfigView)
    hass.http.register_view(APIDiscoveryView)
    hass.http.register_view(APIStatesView)
    hass.http.register_view(APIEntityStateView)
    hass.http.register_view(APIEventListenersView)
    hass.http.register_view(APIEventView)
    hass.http.register_view(APIServicesView)
    hass.http.register_view(APIDomainServicesView)
    hass.http.register_view(APIComponentsView)
    hass.http.register_view(APITemplateView)

    if DATA_LOGGING in hass.data:
        hass.http.register_view(APIErrorLog)

    return True


class APIStatusView(HomeAssistantView):
    """View to handle Status requests."""

    url = URL_API
    name = 'api:status'

    @ha.callback
    def get(self, request):
        """Retrieve if API is running."""
        return self.json_message("API running.")


class APIEventStream(HomeAssistantView):
    """View to handle EventStream requests."""

    url = URL_API_STREAM
    name = 'api:stream'

    async def get(self, request):
        """Provide a streaming interface for the event bus."""
        if not request['hass_user'].is_admin:
            raise Unauthorized()
        hass = request.app['hass']
        stop_obj = object()
        to_write = asyncio.Queue()

        restrict = request.query.get('restrict')
        if restrict:
            restrict = restrict.split(',') + [EVENT_HOMEASSISTANT_STOP]

        async def forward_events(event):
            """Forward events to the open request."""
            if event.event_type == EVENT_TIME_CHANGED:
                return

            if restrict and event.event_type not in restrict:
                return

            _LOGGER.debug("STREAM %s FORWARDING %s", id(stop_obj), event)

            if event.event_type == EVENT_HOMEASSISTANT_STOP:
                data = stop_obj
            else:
                data = json.dumps(event, cls=JSONEncoder)

            await to_write.put(data)

        response = web.StreamResponse()
        response.content_type = 'text/event-stream'
        await response.prepare(request)

        unsub_stream = hass.bus.async_listen(MATCH_ALL, forward_events)

        try:
            _LOGGER.debug("STREAM %s ATTACHED", id(stop_obj))

            # Fire off one message so browsers fire open event right away
            await to_write.put(STREAM_PING_PAYLOAD)

            while True:
                try:
                    with async_timeout.timeout(STREAM_PING_INTERVAL):
                        payload = await to_write.get()

                    if payload is stop_obj:
                        break

                    msg = "data: {}\n\n".format(payload)
                    _LOGGER.debug(
                        "STREAM %s WRITING %s", id(stop_obj), msg.strip())
                    await response.write(msg.encode('UTF-8'))
                except asyncio.TimeoutError:
                    await to_write.put(STREAM_PING_PAYLOAD)

        except asyncio.CancelledError:
            _LOGGER.debug("STREAM %s ABORT", id(stop_obj))

        finally:
            _LOGGER.debug("STREAM %s RESPONSE CLOSED", id(stop_obj))
            unsub_stream()

        return response


class APIConfigView(HomeAssistantView):
    """View to handle Configuration requests."""

    url = URL_API_CONFIG
    name = 'api:config'

    @ha.callback
    def get(self, request):
        """Get current configuration."""
        return self.json(request.app['hass'].config.as_dict())


class APIDiscoveryView(HomeAssistantView):
    """View to provide Discovery information."""

    requires_auth = False
    url = URL_API_DISCOVERY_INFO
    name = 'api:discovery'

    @ha.callback
    def get(self, request):
        """Get discovery information."""
        hass = request.app['hass']
        return self.json({
            ATTR_BASE_URL: hass.config.api.base_url,
            ATTR_LOCATION_NAME: hass.config.location_name,
            # always needs authentication
            ATTR_REQUIRES_API_PASSWORD: True,
            ATTR_VERSION: __version__,
        })


class APIStatesView(HomeAssistantView):
    """View to handle States requests."""

    url = URL_API_STATES
    name = "api:states"

    @ha.callback
    def get(self, request):
        """Get current states."""
        user = request['hass_user']
        entity_perm = user.permissions.check_entity
        states = [
            state for state in request.app['hass'].states.async_all()
            if entity_perm(state.entity_id, 'read')
        ]
        return self.json(states)


class APIEntityStateView(HomeAssistantView):
    """View to handle EntityState requests."""

    url = '/api/states/{entity_id}'
    name = 'api:entity-state'

    @ha.callback
    def get(self, request, entity_id):
        """Retrieve state of entity."""
        user = request['hass_user']
        if not user.permissions.check_entity(entity_id, POLICY_READ):
            raise Unauthorized(entity_id=entity_id)

        state = request.app['hass'].states.get(entity_id)
        if state:
            return self.json(state)
        return self.json_message("Entity not found.", HTTP_NOT_FOUND)

    async def post(self, request, entity_id):
        """Update state of entity."""
        if not request['hass_user'].is_admin:
            raise Unauthorized(entity_id=entity_id)
        hass = request.app['hass']
        try:
            data = await request.json()
        except ValueError:
            return self.json_message(
                "Invalid JSON specified.", HTTP_BAD_REQUEST)

        new_state = data.get('state')

        if new_state is None:
            return self.json_message("No state specified.", HTTP_BAD_REQUEST)

        attributes = data.get('attributes')
        force_update = data.get('force_update', False)

        is_new_state = hass.states.get(entity_id) is None

        # Write state
        hass.states.async_set(entity_id, new_state, attributes, force_update,
                              self.context(request))

        # Read the state back for our response
        status_code = HTTP_CREATED if is_new_state else 200
        resp = self.json(hass.states.get(entity_id), status_code)

        resp.headers.add('Location', URL_API_STATES_ENTITY.format(entity_id))

        return resp

    @ha.callback
    def delete(self, request, entity_id):
        """Remove entity."""
        if not request['hass_user'].is_admin:
            raise Unauthorized(entity_id=entity_id)
        if request.app['hass'].states.async_remove(entity_id):
            return self.json_message("Entity removed.")
        return self.json_message("Entity not found.", HTTP_NOT_FOUND)


class APIEventListenersView(HomeAssistantView):
    """View to handle EventListeners requests."""

    url = URL_API_EVENTS
    name = 'api:event-listeners'

    @ha.callback
    def get(self, request):
        """Get event listeners."""
        return self.json(async_events_json(request.app['hass']))


class APIEventView(HomeAssistantView):
    """View to handle Event requests."""

    url = '/api/events/{event_type}'
    name = 'api:event'

    async def post(self, request, event_type):
        """Fire events."""
        if not request['hass_user'].is_admin:
            raise Unauthorized()
        body = await request.text()
        try:
            event_data = json.loads(body) if body else None
        except ValueError:
            return self.json_message(
                "Event data should be valid JSON.", HTTP_BAD_REQUEST)

        if event_data is not None and not isinstance(event_data, dict):
            return self.json_message(
                "Event data should be a JSON object", HTTP_BAD_REQUEST)

        # Special case handling for event STATE_CHANGED
        # We will try to convert state dicts back to State objects
        if event_type == ha.EVENT_STATE_CHANGED and event_data:
            for key in ('old_state', 'new_state'):
                state = ha.State.from_dict(event_data.get(key))

                if state:
                    event_data[key] = state

        request.app['hass'].bus.async_fire(
            event_type, event_data, ha.EventOrigin.remote,
            self.context(request))

        return self.json_message("Event {} fired.".format(event_type))


class APIServicesView(HomeAssistantView):
    """View to handle Services requests."""

    url = URL_API_SERVICES
    name = 'api:services'

    async def get(self, request):
        """Get registered services."""
        services = await async_services_json(request.app['hass'])
        return self.json(services)


class APIDomainServicesView(HomeAssistantView):
    """View to handle DomainServices requests."""

    url = '/api/services/{domain}/{service}'
    name = 'api:domain-services'

    async def post(self, request, domain, service):
        """Call a service.

        Returns a list of changed states.
        """
        hass = request.app['hass']
        body = await request.text()
        try:
            data = json.loads(body) if body else None
        except ValueError:
            return self.json_message(
                "Data should be valid JSON.", HTTP_BAD_REQUEST)

        with AsyncTrackStates(hass) as changed_states:
            try:
                await hass.services.async_call(
                    domain, service, data, True, self.context(request))
            except (vol.Invalid, ServiceNotFound):
                raise HTTPBadRequest()

        return self.json(changed_states)


class APIComponentsView(HomeAssistantView):
    """View to handle Components requests."""

    url = URL_API_COMPONENTS
    name = 'api:components'

    @ha.callback
    def get(self, request):
        """Get current loaded components."""
        return self.json(request.app['hass'].config.components)


class APITemplateView(HomeAssistantView):
    """View to handle Template requests."""

    url = URL_API_TEMPLATE
    name = 'api:template'

    async def post(self, request):
        """Render a template."""
        if not request['hass_user'].is_admin:
            raise Unauthorized()
        try:
            data = await request.json()
            tpl = template.Template(data['template'], request.app['hass'])
            return tpl.async_render(data.get('variables'))
        except (ValueError, TemplateError) as ex:
            return self.json_message(
                "Error rendering template: {}".format(ex), HTTP_BAD_REQUEST)


class APIErrorLog(HomeAssistantView):
    """View to fetch the API error log."""

    url = URL_API_ERROR_LOG
    name = 'api:error_log'

    async def get(self, request):
        """Retrieve API error log."""
        if not request['hass_user'].is_admin:
            raise Unauthorized()
        return web.FileResponse(request.app['hass'].data[DATA_LOGGING])


async def async_services_json(hass):
    """Generate services data to JSONify."""
    descriptions = await async_get_all_descriptions(hass)
    return [{'domain': key, 'services': value}
            for key, value in descriptions.items()]


def async_events_json(hass):
    """Generate event data to JSONify."""
    return [{'event': key, 'listener_count': value}
            for key, value in hass.bus.async_listeners().items()]
