import logging

import voluptuous as vol
from homeassistant.helpers import config_validation as cv
from homeassistant.components import websocket_api
from homeassistant.components.http import HomeAssistantView
from homeassistant.components.http.data_validator import RequestDataValidator
from homeassistant.core import callback
from homeassistant.components.websocket_api import decorators, async_register_command
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from . import const
from .store import ScheduleEntry

_LOGGER = logging.getLogger(__name__)

class SchedulesListView(HomeAssistantView):
    """Login to Home Assistant cloud."""

    url = "/api/{}/list".format(const.DOMAIN)
    name = "api:{}:list".format(const.DOMAIN)

    async def get(self, request):
        hass = request.app["hass"]
        coordinator = hass.data[const.DOMAIN]["coordinator"]
        schedules = coordinator.async_get_schedules()
        return self.json(schedules)


class SchedulesAddView(HomeAssistantView):
    """Login to Home Assistant cloud."""

    url = "/api/{}/add".format(const.DOMAIN)
    name = "api:{}:add".format(const.DOMAIN)

    @RequestDataValidator(const.ADD_SCHEDULE_SCHEMA)
    async def post(self, request, data):
        """Handle config update request."""
        hass = request.app["hass"]
        coordinator = hass.data[const.DOMAIN]["coordinator"]
        coordinator.async_create_schedule(data)
        return self.json({"success": True})


class SchedulesEditView(HomeAssistantView):
    """Login to Home Assistant cloud."""

    url = "/api/{}/edit".format(const.DOMAIN)
    name = "api:{}:edit".format(const.DOMAIN)

    @RequestDataValidator(
        const.EDIT_SCHEDULE_SCHEMA.extend(
            {vol.Required(const.ATTR_SCHEDULE_ID): cv.string}
        )
    )
    async def post(self, request, data):
        """Handle config update request."""
        hass = request.app["hass"]
        coordinator = hass.data[const.DOMAIN]["coordinator"]
        schedule_id = data[const.ATTR_SCHEDULE_ID]
        del data[const.ATTR_SCHEDULE_ID]
        coordinator.async_edit_schedule(schedule_id, data)
        return self.json({"success": True})


class SchedulesRemoveView(HomeAssistantView):
    """Login to Home Assistant cloud."""

    url = "/api/{}/remove".format(const.DOMAIN)
    name = "api:{}:remove".format(const.DOMAIN)

    @RequestDataValidator(vol.Schema({vol.Required(const.ATTR_SCHEDULE_ID): cv.string}))
    async def post(self, request, data):
        """Handle config update request."""
        hass = request.app["hass"]
        coordinator = hass.data[const.DOMAIN]["coordinator"]
        coordinator.async_delete_schedule(data[const.ATTR_SCHEDULE_ID])
        return self.json({"success": True})


@callback
def websocket_get_schedules(hass, connection, msg):
    """Publish scheduler list data."""
    coordinator = hass.data[const.DOMAIN]["coordinator"]
    schedules = coordinator.async_get_schedules()
    connection.send_result(msg["id"], schedules)


@callback
def websocket_get_schedule_item(hass, connection, msg):
    """Publish scheduler list data."""
    coordinator = hass.data[const.DOMAIN]["coordinator"]
    item = msg[const.ATTR_SCHEDULE_ID]
    data = coordinator.async_get_schedule(item)
    connection.send_result(msg["id"], data)


@callback
def websocket_get_tags(hass, connection, msg):
    """Publish tag list data."""
    coordinator = hass.data[const.DOMAIN]["coordinator"]
    tags = coordinator.async_get_tags()
    connection.send_result(msg["id"], tags)


@callback
@decorators.websocket_command(
    {
        vol.Required("type"): const.EVENT,
    }
)
@decorators.async_response
async def handle_subscribe_updates(hass, connection, msg):
    """subscribe listeners when frontend connection is opened"""

    listeners = []

    @callback
    def async_handle_event_item_created(schedule: ScheduleEntry):
        """pass data to frontend when backend changes"""
        connection.send_message(
            {
                "id": msg["id"],
                "type": "event",
                "event": {  # data to pass with event
                    "event": const.EVENT_ITEM_CREATED,
                    "schedule_id": schedule.schedule_id,
                },
            }
        )

    listeners.append(
        async_dispatcher_connect(
            hass, const.EVENT_ITEM_CREATED, async_handle_event_item_created
        )
    )

    @callback
    def async_handle_event_item_updated(schedule_id: str):
        """pass data to frontend when backend changes"""
        connection.send_message(
            {
                "id": msg["id"],
                "type": "event",
                "event": {  # data to pass with event
                    "event": const.EVENT_ITEM_UPDATED,
                    "schedule_id": schedule_id,
                },
            }
        )

    listeners.append(
        async_dispatcher_connect(
            hass, const.EVENT_ITEM_UPDATED, async_handle_event_item_updated
        )
    )

    @callback
    def async_handle_event_item_removed(schedule_id: str):
        """pass data to frontend when backend changes"""
        connection.send_message(
            {
                "id": msg["id"],
                "type": "event",
                "event": {  # data to pass with event
                    "event": const.EVENT_ITEM_REMOVED,
                    "schedule_id": schedule_id,
                },
            }
        )

    listeners.append(
        async_dispatcher_connect(
            hass, const.EVENT_ITEM_REMOVED, async_handle_event_item_removed
        )
    )

    @callback
    def async_handle_event_timer_updated(schedule_id: str):
        """pass data to frontend when backend changes"""
        connection.send_message(
            {
                "id": msg["id"],
                "type": "event",
                "event": {  # data to pass with event
                    "event": const.EVENT_TIMER_UPDATED,
                    "schedule_id": schedule_id,
                },
            }
        )

    listeners.append(
        async_dispatcher_connect(
            hass, const.EVENT_TIMER_UPDATED, async_handle_event_timer_updated
        )
    )

    @callback
    def async_handle_event_timer_finished(schedule_id: str):
        """pass data to frontend when backend changes"""
        connection.send_message(
            {
                "id": msg["id"],
                "type": "event",
                "event": {  # data to pass with event
                    "event": const.EVENT_TIMER_FINISHED,
                    "schedule_id": schedule_id,
                },
            }
        )

    listeners.append(
        async_dispatcher_connect(
            hass, const.EVENT_TIMER_FINISHED, async_handle_event_timer_finished
        )
    )

    def unsubscribe_listeners():
        """unsubscribe listeners when frontend connection closes"""
        while len(listeners):
            listeners.pop()()

    connection.subscriptions[msg["id"]] = unsubscribe_listeners
    connection.send_result(msg["id"])


async def async_register_websockets(hass):

    # expose services
    hass.http.register_view(SchedulesAddView)
    hass.http.register_view(SchedulesEditView)
    hass.http.register_view(SchedulesRemoveView)
    hass.http.register_view(SchedulesListView)

    # pass list of schedules to frontend
    websocket_api.async_register_command(
        hass,
        const.DOMAIN,
        websocket_get_schedules,
        websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
            {
                vol.Required("type"): const.DOMAIN,
            }
        ),
    )

    # pass single schedule to frontend
    websocket_api.async_register_command(
        hass,
        "{}/item".format(const.DOMAIN),
        websocket_get_schedule_item,
        websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
            {
                vol.Required("type"): "{}/item".format(const.DOMAIN),
                vol.Required(const.ATTR_SCHEDULE_ID): cv.string,
            }
        ),
    )

    # pass list of tags to frontend
    websocket_api.async_register_command(
        hass,
        "{}/tags".format(const.DOMAIN),
        websocket_get_tags,
        websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
            {
                vol.Required("type"): "{}/tags".format(const.DOMAIN),
            }
        ),
    )

    # instantiate listener for sending event to frontend on backend change
    async_register_command(hass, handle_subscribe_updates)
