"""
homeassistant.components.api
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Provides a Rest API for Home Assistant.

For more details about the RESTful API, please refer to the documentation at
https://home-assistant.io/developers/api/
"""
import re
import logging
import threading
import json

import homeassistant.core as ha
from homeassistant.exceptions import TemplateError
from homeassistant.helpers.state import TrackStates
import homeassistant.remote as rem
from homeassistant.util import template
from homeassistant.bootstrap import ERROR_LOG_FILENAME
from homeassistant.const import (
    URL_API, URL_API_STATES, URL_API_EVENTS, URL_API_SERVICES, URL_API_STREAM,
    URL_API_EVENT_FORWARD, URL_API_STATES_ENTITY, URL_API_COMPONENTS,
    URL_API_CONFIG, URL_API_BOOTSTRAP, URL_API_ERROR_LOG, URL_API_LOG_OUT,
    URL_API_TEMPLATE, EVENT_TIME_CHANGED, EVENT_HOMEASSISTANT_STOP, MATCH_ALL,
    HTTP_OK, HTTP_CREATED, HTTP_BAD_REQUEST, HTTP_NOT_FOUND,
    HTTP_UNPROCESSABLE_ENTITY, HTTP_HEADER_CONTENT_TYPE,
    CONTENT_TYPE_TEXT_PLAIN)


DOMAIN = 'api'
DEPENDENCIES = ['http']

STREAM_PING_PAYLOAD = "ping"
STREAM_PING_INTERVAL = 50  # seconds

_LOGGER = logging.getLogger(__name__)


def setup(hass, config):
    """ Register the API with the HTTP interface. """

    # /api - for validation purposes
    hass.http.register_path('GET', URL_API, _handle_get_api)

    # /api/stream
    hass.http.register_path('GET', URL_API_STREAM, _handle_get_api_stream)

    # /api/config
    hass.http.register_path('GET', URL_API_CONFIG, _handle_get_api_config)

    # /api/bootstrap
    hass.http.register_path(
        'GET', URL_API_BOOTSTRAP, _handle_get_api_bootstrap)

    # /states
    hass.http.register_path('GET', URL_API_STATES, _handle_get_api_states)
    hass.http.register_path(
        'GET', re.compile(r'/api/states/(?P<entity_id>[a-zA-Z\._0-9]+)'),
        _handle_get_api_states_entity)
    hass.http.register_path(
        'POST', re.compile(r'/api/states/(?P<entity_id>[a-zA-Z\._0-9]+)'),
        _handle_post_state_entity)
    hass.http.register_path(
        'PUT', re.compile(r'/api/states/(?P<entity_id>[a-zA-Z\._0-9]+)'),
        _handle_post_state_entity)

    # /events
    hass.http.register_path('GET', URL_API_EVENTS, _handle_get_api_events)
    hass.http.register_path(
        'POST', re.compile(r'/api/events/(?P<event_type>[a-zA-Z\._0-9]+)'),
        _handle_api_post_events_event)

    # /services
    hass.http.register_path('GET', URL_API_SERVICES, _handle_get_api_services)
    hass.http.register_path(
        'POST',
        re.compile((r'/api/services/'
                    r'(?P<domain>[a-zA-Z\._0-9]+)/'
                    r'(?P<service>[a-zA-Z\._0-9]+)')),
        _handle_post_api_services_domain_service)

    # /event_forwarding
    hass.http.register_path(
        'POST', URL_API_EVENT_FORWARD, _handle_post_api_event_forward)
    hass.http.register_path(
        'DELETE', URL_API_EVENT_FORWARD, _handle_delete_api_event_forward)

    # /components
    hass.http.register_path(
        'GET', URL_API_COMPONENTS, _handle_get_api_components)

    hass.http.register_path('GET', URL_API_ERROR_LOG,
                            _handle_get_api_error_log)

    hass.http.register_path('POST', URL_API_LOG_OUT, _handle_post_api_log_out)

    hass.http.register_path('POST', URL_API_TEMPLATE,
                            _handle_post_api_template)

    return True


def _handle_get_api(handler, path_match, data):
    """ Renders the debug interface. """
    handler.write_json_message("API running.")


def _handle_get_api_stream(handler, path_match, data):
    """ Provide a streaming interface for the event bus. """
    gracefully_closed = False
    hass = handler.server.hass
    wfile = handler.wfile
    write_lock = threading.Lock()
    block = threading.Event()
    session_id = None

    restrict = data.get('restrict')
    if restrict:
        restrict = restrict.split(',')

    def write_message(payload):
        """ Writes a message to the output. """
        with write_lock:
            msg = "data: {}\n\n".format(payload)

            try:
                wfile.write(msg.encode("UTF-8"))
                wfile.flush()
            except (IOError, ValueError):
                # IOError: socket errors
                # ValueError: raised when 'I/O operation on closed file'
                block.set()

    def forward_events(event):
        """ Forwards events to the open request. """
        nonlocal gracefully_closed

        if block.is_set() or event.event_type == EVENT_TIME_CHANGED:
            return
        elif event.event_type == EVENT_HOMEASSISTANT_STOP:
            gracefully_closed = True
            block.set()
            return

        handler.server.sessions.extend_validation(session_id)
        write_message(json.dumps(event, cls=rem.JSONEncoder))

    handler.send_response(HTTP_OK)
    handler.send_header('Content-type', 'text/event-stream')
    session_id = handler.set_session_cookie_header()
    handler.end_headers()

    if restrict:
        for event in restrict:
            hass.bus.listen(event, forward_events)
    else:
        hass.bus.listen(MATCH_ALL, forward_events)

    while True:
        write_message(STREAM_PING_PAYLOAD)

        block.wait(STREAM_PING_INTERVAL)

        if block.is_set():
            break

    if not gracefully_closed:
        _LOGGER.info("Found broken event stream to %s, cleaning up",
                     handler.client_address[0])

    if restrict:
        for event in restrict:
            hass.bus.remove_listener(event, forward_events)
    else:
        hass.bus.remove_listener(MATCH_ALL, forward_events)


def _handle_get_api_config(handler, path_match, data):
    """ Returns the Home Assistant config. """
    handler.write_json(handler.server.hass.config.as_dict())


def _handle_get_api_bootstrap(handler, path_match, data):
    """ Returns all data needed to bootstrap Home Assistant. """
    hass = handler.server.hass

    handler.write_json({
        'config': hass.config.as_dict(),
        'states': hass.states.all(),
        'events': _events_json(hass),
        'services': _services_json(hass),
    })


def _handle_get_api_states(handler, path_match, data):
    """ Returns a dict containing all entity ids and their state. """
    handler.write_json(handler.server.hass.states.all())


def _handle_get_api_states_entity(handler, path_match, data):
    """ Returns the state of a specific entity. """
    entity_id = path_match.group('entity_id')

    state = handler.server.hass.states.get(entity_id)

    if state:
        handler.write_json(state)
    else:
        handler.write_json_message("State does not exist.", HTTP_NOT_FOUND)


def _handle_post_state_entity(handler, path_match, data):
    """ Handles updating the state of an entity.

    This handles the following paths:
    /api/states/<entity_id>
    """
    entity_id = path_match.group('entity_id')

    try:
        new_state = data['state']
    except KeyError:
        handler.write_json_message("state not specified", HTTP_BAD_REQUEST)
        return

    attributes = data['attributes'] if 'attributes' in data else None

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

    # Write state
    handler.server.hass.states.set(entity_id, new_state, attributes)

    state = handler.server.hass.states.get(entity_id)

    status_code = HTTP_CREATED if is_new_state else HTTP_OK

    handler.write_json(
        state.as_dict(),
        status_code=status_code,
        location=URL_API_STATES_ENTITY.format(entity_id))


def _handle_get_api_events(handler, path_match, data):
    """ Handles getting overview of event listeners. """
    handler.write_json(_events_json(handler.server.hass))


def _handle_api_post_events_event(handler, path_match, event_data):
    """ Handles firing of an event.

    This handles the following paths:
    /api/events/<event_type>

    Events from /api are threated as remote events.
    """
    event_type = path_match.group('event_type')

    if event_data is not None and not isinstance(event_data, dict):
        handler.write_json_message(
            "event_data should be an object", HTTP_UNPROCESSABLE_ENTITY)

    event_origin = ha.EventOrigin.remote

    # 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

    handler.server.hass.bus.fire(event_type, event_data, event_origin)

    handler.write_json_message("Event {} fired.".format(event_type))


def _handle_get_api_services(handler, path_match, data):
    """ Handles getting overview of services. """
    handler.write_json(_services_json(handler.server.hass))


# pylint: disable=invalid-name
def _handle_post_api_services_domain_service(handler, path_match, data):
    """ Handles calling a service.

    This handles the following paths:
    /api/services/<domain>/<service>
    """
    domain = path_match.group('domain')
    service = path_match.group('service')

    with TrackStates(handler.server.hass) as changed_states:
        handler.server.hass.services.call(domain, service, data, True)

    handler.write_json(changed_states)


# pylint: disable=invalid-name
def _handle_post_api_event_forward(handler, path_match, data):
    """ Handles adding an event forwarding target. """

    try:
        host = data['host']
        api_password = data['api_password']
    except KeyError:
        handler.write_json_message(
            "No host or api_password received.", HTTP_BAD_REQUEST)
        return

    try:
        port = int(data['port']) if 'port' in data else None
    except ValueError:
        handler.write_json_message(
            "Invalid value received for port", HTTP_UNPROCESSABLE_ENTITY)
        return

    api = rem.API(host, api_password, port)

    if not api.validate_api():
        handler.write_json_message(
            "Unable to validate API", HTTP_UNPROCESSABLE_ENTITY)
        return

    if handler.server.event_forwarder is None:
        handler.server.event_forwarder = \
            rem.EventForwarder(handler.server.hass)

    handler.server.event_forwarder.connect(api)

    handler.write_json_message("Event forwarding setup.")


def _handle_delete_api_event_forward(handler, path_match, data):
    """ Handles deleting an event forwarding target. """

    try:
        host = data['host']
    except KeyError:
        handler.write_json_message("No host received.", HTTP_BAD_REQUEST)
        return

    try:
        port = int(data['port']) if 'port' in data else None
    except ValueError:
        handler.write_json_message(
            "Invalid value received for port", HTTP_UNPROCESSABLE_ENTITY)
        return

    if handler.server.event_forwarder is not None:
        api = rem.API(host, None, port)

        handler.server.event_forwarder.disconnect(api)

    handler.write_json_message("Event forwarding cancelled.")


def _handle_get_api_components(handler, path_match, data):
    """ Returns all the loaded components. """

    handler.write_json(handler.server.hass.config.components)


def _handle_get_api_error_log(handler, path_match, data):
    """ Returns the logged errors for this session. """
    handler.write_file(handler.server.hass.config.path(ERROR_LOG_FILENAME),
                       False)


def _handle_post_api_log_out(handler, path_match, data):
    """ Log user out. """
    handler.send_response(HTTP_OK)
    handler.destroy_session()
    handler.end_headers()


def _handle_post_api_template(handler, path_match, data):
    """ Log user out. """
    template_string = data.get('template', '')

    try:
        rendered = template.render(handler.server.hass, template_string)

        handler.send_response(HTTP_OK)
        handler.send_header(HTTP_HEADER_CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN)
        handler.end_headers()
        handler.wfile.write(rendered.encode('utf-8'))
    except TemplateError as e:
        handler.write_json_message(str(e), HTTP_UNPROCESSABLE_ENTITY)
        return


def _services_json(hass):
    """ Generate services data to JSONify. """
    return [{"domain": key, "services": value}
            for key, value in hass.services.services.items()]


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