"""Allow users to set and activate scenes."""
from collections import namedtuple
import logging

import voluptuous as vol

from homeassistant import config as conf_util
from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN, STATES, Scene
from homeassistant.const import (
    ATTR_ENTITY_ID,
    ATTR_STATE,
    CONF_ENTITIES,
    CONF_ID,
    CONF_NAME,
    CONF_PLATFORM,
    SERVICE_RELOAD,
    STATE_OFF,
    STATE_ON,
)
from homeassistant.core import DOMAIN as HA_DOMAIN, State
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import (
    config_per_platform,
    config_validation as cv,
    entity_platform,
)
from homeassistant.helpers.state import async_reproduce_state
from homeassistant.loader import async_get_integration


def _convert_states(states):
    """Convert state definitions to State objects."""
    result = {}

    for entity_id in states:
        entity_id = cv.entity_id(entity_id)

        if isinstance(states[entity_id], dict):
            entity_attrs = states[entity_id].copy()
            state = entity_attrs.pop(ATTR_STATE, None)
            attributes = entity_attrs
        else:
            state = states[entity_id]
            attributes = {}

        # YAML translates 'on' to a boolean
        # http://yaml.org/type/bool.html
        if isinstance(state, bool):
            state = STATE_ON if state else STATE_OFF
        elif not isinstance(state, str):
            raise vol.Invalid(f"State for {entity_id} should be a string")

        result[entity_id] = State(entity_id, state, attributes)

    return result


def _ensure_no_intersection(value):
    """Validate that entities and snapshot_entities do not overlap."""
    if (
        CONF_SNAPSHOT not in value
        or CONF_ENTITIES not in value
        or not any(
            entity_id in value[CONF_SNAPSHOT] for entity_id in value[CONF_ENTITIES]
        )
    ):
        return value

    raise vol.Invalid("entities and snapshot_entities must not overlap")


CONF_SCENE_ID = "scene_id"
CONF_SNAPSHOT = "snapshot_entities"

STATES_SCHEMA = vol.All(dict, _convert_states)

PLATFORM_SCHEMA = vol.Schema(
    {
        vol.Required(CONF_PLATFORM): HA_DOMAIN,
        vol.Required(STATES): vol.All(
            cv.ensure_list,
            [
                {
                    vol.Required(CONF_NAME): cv.string,
                    vol.Required(CONF_ENTITIES): STATES_SCHEMA,
                }
            ],
        ),
    },
    extra=vol.ALLOW_EXTRA,
)

CREATE_SCENE_SCHEMA = vol.All(
    cv.has_at_least_one_key(CONF_ENTITIES, CONF_SNAPSHOT),
    _ensure_no_intersection,
    vol.Schema(
        {
            vol.Required(CONF_SCENE_ID): cv.slug,
            vol.Optional(CONF_ENTITIES, default={}): STATES_SCHEMA,
            vol.Optional(CONF_SNAPSHOT, default=[]): cv.entity_ids,
        }
    ),
)

SERVICE_APPLY = "apply"
SERVICE_CREATE = "create"
SCENECONFIG = namedtuple("SceneConfig", [CONF_NAME, STATES])
_LOGGER = logging.getLogger(__name__)


async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
    """Set up home assistant scene entries."""
    _process_scenes_config(hass, async_add_entities, config)

    # This platform can be loaded multiple times. Only first time register the service.
    if hass.services.has_service(SCENE_DOMAIN, SERVICE_RELOAD):
        return

    # Store platform for later.
    platform = entity_platform.current_platform.get()

    async def reload_config(call):
        """Reload the scene config."""
        try:
            conf = await conf_util.async_hass_config_yaml(hass)
        except HomeAssistantError as err:
            _LOGGER.error(err)
            return

        integration = await async_get_integration(hass, SCENE_DOMAIN)

        conf = await conf_util.async_process_component_config(hass, conf, integration)

        if not conf or not platform:
            return

        await platform.async_reset()

        # Extract only the config for the Home Assistant platform, ignore the rest.
        for p_type, p_config in config_per_platform(conf, SCENE_DOMAIN):
            if p_type != HA_DOMAIN:
                continue

            _process_scenes_config(hass, async_add_entities, p_config)

    hass.helpers.service.async_register_admin_service(
        SCENE_DOMAIN, SERVICE_RELOAD, reload_config
    )

    async def apply_service(call):
        """Apply a scene."""
        await async_reproduce_state(
            hass, call.data[CONF_ENTITIES].values(), blocking=True, context=call.context
        )

    hass.services.async_register(
        SCENE_DOMAIN,
        SERVICE_APPLY,
        apply_service,
        vol.Schema({vol.Required(CONF_ENTITIES): STATES_SCHEMA}),
    )

    async def create_service(call):
        """Create a scene."""
        snapshot = call.data[CONF_SNAPSHOT]
        entities = call.data[CONF_ENTITIES]

        for entity_id in snapshot:
            state = hass.states.get(entity_id)
            if state is None:
                _LOGGER.warning(
                    "Entity %s does not exist and therefore cannot be snapshotted",
                    entity_id,
                )
                continue
            entities[entity_id] = State(entity_id, state.state, state.attributes)

        if not entities:
            _LOGGER.warning("Empty scenes are not allowed")
            return

        scene_config = SCENECONFIG(call.data[CONF_SCENE_ID], entities)
        entity_id = f"{SCENE_DOMAIN}.{scene_config.name}"
        old = platform.entities.get(entity_id)
        if old is not None:
            if not old.from_service:
                _LOGGER.warning("The scene %s already exists", entity_id)
                return
            await platform.async_remove_entity(entity_id)
        async_add_entities([HomeAssistantScene(hass, scene_config, from_service=True)])

    hass.services.async_register(
        SCENE_DOMAIN, SERVICE_CREATE, create_service, CREATE_SCENE_SCHEMA
    )


def _process_scenes_config(hass, async_add_entities, config):
    """Process multiple scenes and add them."""
    scene_config = config[STATES]

    # Check empty list
    if not scene_config:
        return

    async_add_entities(
        HomeAssistantScene(
            hass,
            SCENECONFIG(scene[CONF_NAME], scene[CONF_ENTITIES]),
            scene.get(CONF_ID),
        )
        for scene in scene_config
    )


class HomeAssistantScene(Scene):
    """A scene is a group of entities and the states we want them to be."""

    def __init__(self, hass, scene_config, scene_id=None, from_service=False):
        """Initialize the scene."""
        self._id = scene_id
        self.hass = hass
        self.scene_config = scene_config
        self.from_service = from_service

    @property
    def name(self):
        """Return the name of the scene."""
        return self.scene_config.name

    @property
    def device_state_attributes(self):
        """Return the scene state attributes."""
        attributes = {ATTR_ENTITY_ID: list(self.scene_config.states)}
        if self._id is not None:
            attributes[CONF_ID] = self._id
        return attributes

    async def async_activate(self):
        """Activate scene. Try to get entities into requested state."""
        await async_reproduce_state(
            self.hass,
            self.scene_config.states.values(),
            blocking=True,
            context=self._context,
        )
