"""Allow to set up simple automation rules via the config file."""
import asyncio
from functools import partial
import importlib
import logging

import voluptuous as vol

from homeassistant.const import (
    ATTR_ENTITY_ID, ATTR_NAME, CONF_ID, CONF_PLATFORM,
    EVENT_AUTOMATION_TRIGGERED, EVENT_HOMEASSISTANT_START, SERVICE_RELOAD,
    SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_ON)
from homeassistant.core import Context, CoreState
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import condition, extract_domain_configs, script
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.loader import bind_hass
from homeassistant.util.dt import parse_datetime, utcnow

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

GROUP_NAME_ALL_AUTOMATIONS = 'all automations'

CONF_ALIAS = 'alias'
CONF_HIDE_ENTITY = 'hide_entity'

CONF_CONDITION = 'condition'
CONF_ACTION = 'action'
CONF_TRIGGER = 'trigger'
CONF_CONDITION_TYPE = 'condition_type'
CONF_INITIAL_STATE = 'initial_state'

CONDITION_USE_TRIGGER_VALUES = 'use_trigger_values'
CONDITION_TYPE_AND = 'and'
CONDITION_TYPE_OR = 'or'

DEFAULT_CONDITION_TYPE = CONDITION_TYPE_AND
DEFAULT_HIDE_ENTITY = False
DEFAULT_INITIAL_STATE = True

ATTR_LAST_TRIGGERED = 'last_triggered'
ATTR_VARIABLES = 'variables'
SERVICE_TRIGGER = 'trigger'

_LOGGER = logging.getLogger(__name__)


def _platform_validator(config):
    """Validate it is a valid platform."""
    try:
        platform = importlib.import_module('.{}'.format(config[CONF_PLATFORM]),
                                           __name__)
    except ImportError:
        raise vol.Invalid('Invalid platform specified') from None

    return platform.TRIGGER_SCHEMA(config)


_TRIGGER_SCHEMA = vol.All(
    cv.ensure_list,
    [
        vol.All(
            vol.Schema({
                vol.Required(CONF_PLATFORM): str
            }, extra=vol.ALLOW_EXTRA),
            _platform_validator
        ),
    ]
)

_CONDITION_SCHEMA = vol.All(cv.ensure_list, [cv.CONDITION_SCHEMA])

PLATFORM_SCHEMA = vol.Schema({
    # str on purpose
    CONF_ID: str,
    CONF_ALIAS: cv.string,
    vol.Optional(CONF_INITIAL_STATE): cv.boolean,
    vol.Optional(CONF_HIDE_ENTITY, default=DEFAULT_HIDE_ENTITY): cv.boolean,
    vol.Required(CONF_TRIGGER): _TRIGGER_SCHEMA,
    vol.Optional(CONF_CONDITION): _CONDITION_SCHEMA,
    vol.Required(CONF_ACTION): cv.SCRIPT_SCHEMA,
})

SERVICE_SCHEMA = vol.Schema({
    vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids,
})

TRIGGER_SERVICE_SCHEMA = vol.Schema({
    vol.Required(ATTR_ENTITY_ID): cv.comp_entity_ids,
    vol.Optional(ATTR_VARIABLES, default={}): dict,
})

RELOAD_SERVICE_SCHEMA = vol.Schema({})


@bind_hass
def is_on(hass, entity_id):
    """
    Return true if specified automation entity_id is on.

    Async friendly.
    """
    return hass.states.is_state(entity_id, STATE_ON)


async def async_setup(hass, config):
    """Set up the automation."""
    component = EntityComponent(_LOGGER, DOMAIN, hass,
                                group_name=GROUP_NAME_ALL_AUTOMATIONS)

    await _async_process_config(hass, config, component)

    async def trigger_service_handler(service_call):
        """Handle automation triggers."""
        tasks = []
        for entity in await component.async_extract_from_service(service_call):
            tasks.append(entity.async_trigger(
                service_call.data.get(ATTR_VARIABLES),
                skip_condition=True,
                context=service_call.context))

        if tasks:
            await asyncio.wait(tasks)

    async def turn_onoff_service_handler(service_call):
        """Handle automation turn on/off service calls."""
        tasks = []
        method = 'async_{}'.format(service_call.service)
        for entity in await component.async_extract_from_service(service_call):
            tasks.append(getattr(entity, method)())

        if tasks:
            await asyncio.wait(tasks)

    async def toggle_service_handler(service_call):
        """Handle automation toggle service calls."""
        tasks = []
        for entity in await component.async_extract_from_service(service_call):
            if entity.is_on:
                tasks.append(entity.async_turn_off())
            else:
                tasks.append(entity.async_turn_on())

        if tasks:
            await asyncio.wait(tasks)

    async def reload_service_handler(service_call):
        """Remove all automations and load new ones from config."""
        conf = await component.async_prepare_reload()
        if conf is None:
            return
        await _async_process_config(hass, conf, component)

    hass.services.async_register(
        DOMAIN, SERVICE_TRIGGER, trigger_service_handler,
        schema=TRIGGER_SERVICE_SCHEMA)

    hass.services.async_register(
        DOMAIN, SERVICE_RELOAD, reload_service_handler,
        schema=RELOAD_SERVICE_SCHEMA)

    hass.services.async_register(
        DOMAIN, SERVICE_TOGGLE, toggle_service_handler,
        schema=SERVICE_SCHEMA)

    for service in (SERVICE_TURN_ON, SERVICE_TURN_OFF):
        hass.services.async_register(
            DOMAIN, service, turn_onoff_service_handler,
            schema=SERVICE_SCHEMA)

    return True


class AutomationEntity(ToggleEntity, RestoreEntity):
    """Entity to show status of entity."""

    def __init__(self, automation_id, name, async_attach_triggers, cond_func,
                 async_action, hidden, initial_state):
        """Initialize an automation entity."""
        self._id = automation_id
        self._name = name
        self._async_attach_triggers = async_attach_triggers
        self._async_detach_triggers = None
        self._cond_func = cond_func
        self._async_action = async_action
        self._last_triggered = None
        self._hidden = hidden
        self._initial_state = initial_state
        self._is_enabled = False

    @property
    def name(self):
        """Name of the automation."""
        return self._name

    @property
    def should_poll(self):
        """No polling needed for automation entities."""
        return False

    @property
    def state_attributes(self):
        """Return the entity state attributes."""
        return {
            ATTR_LAST_TRIGGERED: self._last_triggered
        }

    @property
    def hidden(self) -> bool:
        """Return True if the automation entity should be hidden from UIs."""
        return self._hidden

    @property
    def is_on(self) -> bool:
        """Return True if entity is on."""
        return (self._async_detach_triggers is not None or
                self._is_enabled)

    async def async_added_to_hass(self) -> None:
        """Startup with initial state or previous state."""
        await super().async_added_to_hass()

        state = await self.async_get_last_state()
        if state:
            enable_automation = state.state == STATE_ON
            last_triggered = state.attributes.get('last_triggered')
            if last_triggered is not None:
                self._last_triggered = parse_datetime(last_triggered)
            _LOGGER.debug("Loaded automation %s with state %s from state "
                          " storage last state %s", self.entity_id,
                          enable_automation, state)
        else:
            enable_automation = DEFAULT_INITIAL_STATE
            _LOGGER.debug("Automation %s not in state storage, state %s from "
                          "default is used.", self.entity_id,
                          enable_automation)

        if self._initial_state is not None:
            enable_automation = self._initial_state
            _LOGGER.debug("Automation %s initial state %s overridden from "
                          "config initial_state", self.entity_id,
                          enable_automation)

        if enable_automation:
            await self.async_enable()

    async def async_turn_on(self, **kwargs) -> None:
        """Turn the entity on and update the state."""
        await self.async_enable()

    async def async_turn_off(self, **kwargs) -> None:
        """Turn the entity off."""
        await self.async_disable()

    async def async_trigger(self, variables, skip_condition=False,
                            context=None):
        """Trigger automation.

        This method is a coroutine.
        """
        if not skip_condition and not self._cond_func(variables):
            return

        # Create a new context referring to the old context.
        parent_id = None if context is None else context.id
        trigger_context = Context(parent_id=parent_id)

        self.async_set_context(trigger_context)
        self.hass.bus.async_fire(EVENT_AUTOMATION_TRIGGERED, {
            ATTR_NAME: self._name,
            ATTR_ENTITY_ID: self.entity_id,
        }, context=trigger_context)
        await self._async_action(self.entity_id, variables, trigger_context)
        self._last_triggered = utcnow()
        await self.async_update_ha_state()

    async def async_will_remove_from_hass(self):
        """Remove listeners when removing automation from HASS."""
        await super().async_will_remove_from_hass()
        await self.async_disable()

    async def async_enable(self):
        """Enable this automation entity.

        This method is a coroutine.
        """
        if self._is_enabled:
            return

        self._is_enabled = True

        # HomeAssistant is starting up
        if self.hass.state != CoreState.not_running:
            self._async_detach_triggers = await self._async_attach_triggers(
                self.async_trigger)
            self.async_write_ha_state()
            return

        async def async_enable_automation(event):
            """Start automation on startup."""
            # Don't do anything if no longer enabled or already attached
            if (not self._is_enabled or
                    self._async_detach_triggers is not None):
                return

            self._async_detach_triggers = await self._async_attach_triggers(
                self.async_trigger)

        self.hass.bus.async_listen_once(
            EVENT_HOMEASSISTANT_START, async_enable_automation)
        self.async_write_ha_state()

    async def async_disable(self):
        """Disable the automation entity."""
        if not self._is_enabled:
            return

        self._is_enabled = False

        if self._async_detach_triggers is not None:
            self._async_detach_triggers()
            self._async_detach_triggers = None

        self.async_write_ha_state()

    @property
    def device_state_attributes(self):
        """Return automation attributes."""
        if self._id is None:
            return None

        return {
            CONF_ID: self._id
        }


async def _async_process_config(hass, config, component):
    """Process config and add automations.

    This method is a coroutine.
    """
    entities = []

    for config_key in extract_domain_configs(config, DOMAIN):
        conf = config[config_key]

        for list_no, config_block in enumerate(conf):
            automation_id = config_block.get(CONF_ID)
            name = config_block.get(CONF_ALIAS) or "{} {}".format(config_key,
                                                                  list_no)

            hidden = config_block[CONF_HIDE_ENTITY]
            initial_state = config_block.get(CONF_INITIAL_STATE)

            action = _async_get_action(hass, config_block.get(CONF_ACTION, {}),
                                       name)

            if CONF_CONDITION in config_block:
                cond_func = _async_process_if(hass, config, config_block)

                if cond_func is None:
                    continue
            else:
                def cond_func(variables):
                    """Condition will always pass."""
                    return True

            async_attach_triggers = partial(
                _async_process_trigger, hass, config,
                config_block.get(CONF_TRIGGER, []), name
            )
            entity = AutomationEntity(
                automation_id, name, async_attach_triggers, cond_func, action,
                hidden, initial_state)

            entities.append(entity)

    if entities:
        await component.async_add_entities(entities)


def _async_get_action(hass, config, name):
    """Return an action based on a configuration."""
    script_obj = script.Script(hass, config, name)

    async def action(entity_id, variables, context):
        """Execute an action."""
        _LOGGER.info('Executing %s', name)

        try:
            await script_obj.async_run(variables, context)
        except Exception as err:  # pylint: disable=broad-except
            script_obj.async_log_exception(
                _LOGGER,
                'Error while executing automation {}'.format(entity_id), err)

    return action


def _async_process_if(hass, config, p_config):
    """Process if checks."""
    if_configs = p_config.get(CONF_CONDITION)

    checks = []
    for if_config in if_configs:
        try:
            checks.append(condition.async_from_config(if_config, False))
        except HomeAssistantError as ex:
            _LOGGER.warning('Invalid condition: %s', ex)
            return None

    def if_action(variables=None):
        """AND all conditions."""
        return all(check(hass, variables) for check in checks)

    return if_action


async def _async_process_trigger(hass, config, trigger_configs, name, action):
    """Set up the triggers.

    This method is a coroutine.
    """
    removes = []
    info = {
        'name': name
    }

    for conf in trigger_configs:
        platform = importlib.import_module('.{}'.format(conf[CONF_PLATFORM]),
                                           __name__)

        remove = await platform.async_trigger(hass, conf, action, info)

        if not remove:
            _LOGGER.error("Error setting up trigger %s", name)
            continue

        _LOGGER.info("Initialized trigger %s", name)
        removes.append(remove)

    if not removes:
        return None

    def remove_triggers():
        """Remove attached triggers."""
        for remove in removes:
            remove()

    return remove_triggers
