"""
Support for repeating alerts when conditions are met.

For more details about this component, please refer to the documentation at
https://home-assistant.io/components/alert/
"""
import asyncio
from datetime import datetime, timedelta
import logging
import os

import voluptuous as vol

from homeassistant.core import callback
from homeassistant.config import load_yaml_config_file
from homeassistant.const import (
    CONF_ENTITY_ID, STATE_IDLE, CONF_NAME, CONF_STATE, STATE_ON, STATE_OFF,
    SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, ATTR_ENTITY_ID)
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.helpers import service, event
from homeassistant.util.async import run_callback_threadsafe
import homeassistant.helpers.config_validation as cv

_LOGGER = logging.getLogger(__name__)

DOMAIN = 'alert'
ENTITY_ID_FORMAT = DOMAIN + '.{}'

CONF_CAN_ACK = 'can_acknowledge'
CONF_NOTIFIERS = 'notifiers'
CONF_REPEAT = 'repeat'
CONF_SKIP_FIRST = 'skip_first'

DEFAULT_CAN_ACK = True
DEFAULT_SKIP_FIRST = False

ALERT_SCHEMA = vol.Schema({
    vol.Required(CONF_NAME): cv.string,
    vol.Required(CONF_ENTITY_ID): cv.entity_id,
    vol.Required(CONF_STATE, default=STATE_ON): cv.string,
    vol.Required(CONF_REPEAT): vol.All(cv.ensure_list, [vol.Coerce(float)]),
    vol.Required(CONF_CAN_ACK, default=DEFAULT_CAN_ACK): cv.boolean,
    vol.Required(CONF_SKIP_FIRST, default=DEFAULT_SKIP_FIRST): cv.boolean,
    vol.Required(CONF_NOTIFIERS): cv.ensure_list})

CONFIG_SCHEMA = vol.Schema({
    DOMAIN: vol.Schema({
        cv.slug: ALERT_SCHEMA,
    }),
}, extra=vol.ALLOW_EXTRA)


ALERT_SERVICE_SCHEMA = vol.Schema({
    vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
})


def is_on(hass, entity_id):
    """Return if the alert is firing and not acknowledged."""
    return hass.states.is_state(entity_id, STATE_ON)


def turn_on(hass, entity_id):
    """Reset the alert."""
    run_callback_threadsafe(
        hass.loop, async_turn_on, hass, entity_id).result()


@callback
def async_turn_on(hass, entity_id):
    """Async reset the alert."""
    data = {ATTR_ENTITY_ID: entity_id}
    hass.async_add_job(
        hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data))


def turn_off(hass, entity_id):
    """Acknowledge alert."""
    run_callback_threadsafe(
        hass.loop, async_turn_off, hass, entity_id).result()


@callback
def async_turn_off(hass, entity_id):
    """Async acknowledge the alert."""
    data = {ATTR_ENTITY_ID: entity_id}
    hass.async_add_job(
        hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, data))


def toggle(hass, entity_id):
    """Toggle acknowledgement of alert."""
    run_callback_threadsafe(hass.loop, async_toggle, hass, entity_id)


@callback
def async_toggle(hass, entity_id):
    """Async toggle acknowledgement of alert."""
    data = {ATTR_ENTITY_ID: entity_id}
    hass.async_add_job(
        hass.services.async_call(DOMAIN, SERVICE_TOGGLE, data))


@asyncio.coroutine
def async_setup(hass, config):
    """Set up the Alert component."""
    alerts = config.get(DOMAIN)
    all_alerts = {}

    @asyncio.coroutine
    def async_handle_alert_service(service_call):
        """Handle calls to alert services."""
        alert_ids = service.extract_entity_ids(hass, service_call)

        for alert_id in alert_ids:
            alert = all_alerts[alert_id]
            if service_call.service == SERVICE_TURN_ON:
                yield from alert.async_turn_on()
            elif service_call.service == SERVICE_TOGGLE:
                yield from alert.async_toggle()
            else:
                yield from alert.async_turn_off()

    # Setup alerts
    for entity_id, alert in alerts.items():
        entity = Alert(hass, entity_id,
                       alert[CONF_NAME], alert[CONF_ENTITY_ID],
                       alert[CONF_STATE], alert[CONF_REPEAT],
                       alert[CONF_SKIP_FIRST], alert[CONF_NOTIFIERS],
                       alert[CONF_CAN_ACK])
        all_alerts[entity.entity_id] = entity

    # Read descriptions
    descriptions = yield from hass.loop.run_in_executor(
        None, load_yaml_config_file, os.path.join(
            os.path.dirname(__file__), 'services.yaml'))
    descriptions = descriptions.get(DOMAIN, {})

    # Setup service calls
    hass.services.async_register(
        DOMAIN, SERVICE_TURN_OFF, async_handle_alert_service,
        descriptions.get(SERVICE_TURN_OFF), schema=ALERT_SERVICE_SCHEMA)
    hass.services.async_register(
        DOMAIN, SERVICE_TURN_ON, async_handle_alert_service,
        descriptions.get(SERVICE_TURN_ON), schema=ALERT_SERVICE_SCHEMA)
    hass.services.async_register(
        DOMAIN, SERVICE_TOGGLE, async_handle_alert_service,
        descriptions.get(SERVICE_TOGGLE), schema=ALERT_SERVICE_SCHEMA)

    tasks = [alert.async_update_ha_state() for alert in all_alerts.values()]
    if tasks:
        yield from asyncio.wait(tasks, loop=hass.loop)

    return True


class Alert(ToggleEntity):
    """Representation of an alert."""

    def __init__(self, hass, entity_id, name, watched_entity_id, state,
                 repeat, skip_first, notifiers, can_ack):
        """Initialize the alert."""
        self.hass = hass
        self._name = name
        self._alert_state = state
        self._skip_first = skip_first
        self._notifiers = notifiers
        self._can_ack = can_ack

        self._delay = [timedelta(minutes=val) for val in repeat]
        self._next_delay = 0

        self._firing = False
        self._ack = False
        self._cancel = None
        self.entity_id = ENTITY_ID_FORMAT.format(entity_id)

        event.async_track_state_change(
            hass, watched_entity_id, self.watched_entity_change)

    @property
    def name(self):
        """Return the name of the alert."""
        return self._name

    @property
    def should_poll(self):
        """HASS need not poll these entities."""
        return False

    @property
    def state(self):
        """Return the alert status."""
        if self._firing:
            if self._ack:
                return STATE_OFF
            return STATE_ON
        return STATE_IDLE

    @property
    def hidden(self):
        """Hide the alert when it is not firing."""
        return not self._can_ack or not self._firing

    @asyncio.coroutine
    def watched_entity_change(self, entity, from_state, to_state):
        """Determine if the alert should start or stop."""
        _LOGGER.debug("Watched entity (%s) has changed", entity)
        if to_state.state == self._alert_state and not self._firing:
            yield from self.begin_alerting()
        if to_state.state != self._alert_state and self._firing:
            yield from self.end_alerting()

    @asyncio.coroutine
    def begin_alerting(self):
        """Begin the alert procedures."""
        _LOGGER.debug("Beginning Alert: %s", self._name)
        self._ack = False
        self._firing = True
        self._next_delay = 0

        if not self._skip_first:
            yield from self._notify()
        else:
            yield from self._schedule_notify()

        self.hass.async_add_job(self.async_update_ha_state)

    @asyncio.coroutine
    def end_alerting(self):
        """End the alert procedures."""
        _LOGGER.debug("Ending Alert: %s", self._name)
        self._cancel()
        self._ack = False
        self._firing = False
        self.hass.async_add_job(self.async_update_ha_state)

    @asyncio.coroutine
    def _schedule_notify(self):
        """Schedule a notification."""
        delay = self._delay[self._next_delay]
        next_msg = datetime.now() + delay
        self._cancel = \
            event.async_track_point_in_time(self.hass, self._notify, next_msg)
        self._next_delay = min(self._next_delay + 1, len(self._delay) - 1)

    @asyncio.coroutine
    def _notify(self, *args):
        """Send the alert notification."""
        if not self._firing:
            return

        if not self._ack:
            _LOGGER.info("Alerting: %s", self._name)
            for target in self._notifiers:
                yield from self.hass.services.async_call(
                    'notify', target, {'message': self._name})
        yield from self._schedule_notify()

    @asyncio.coroutine
    def async_turn_on(self):
        """Async Unacknowledge alert."""
        _LOGGER.debug("Reset Alert: %s", self._name)
        self._ack = False
        yield from self.async_update_ha_state()

    @asyncio.coroutine
    def async_turn_off(self):
        """Async Acknowledge alert."""
        _LOGGER.debug("Acknowledged Alert: %s", self._name)
        self._ack = True
        yield from self.async_update_ha_state()

    @asyncio.coroutine
    def async_toggle(self):
        """Async toggle alert."""
        if self._ack:
            return self.async_turn_on()
        return self.async_turn_off()
