"""
Support for Nest devices.

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

import voluptuous as vol

from homeassistant import config_entries
from homeassistant.const import (
    CONF_STRUCTURE, CONF_FILENAME, CONF_BINARY_SENSORS, CONF_SENSORS,
    CONF_MONITORED_CONDITIONS,
    EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.dispatcher import dispatcher_send, \
    async_dispatcher_connect
from homeassistant.helpers.entity import Entity

from .const import DOMAIN
from . import local_auth

REQUIREMENTS = ['python-nest==4.0.3']

_CONFIGURING = {}
_LOGGER = logging.getLogger(__name__)


DATA_NEST = 'nest'
DATA_NEST_CONFIG = 'nest_config'

SIGNAL_NEST_UPDATE = 'nest_update'

NEST_CONFIG_FILE = 'nest.conf'
CONF_CLIENT_ID = 'client_id'
CONF_CLIENT_SECRET = 'client_secret'

ATTR_HOME_MODE = 'home_mode'
ATTR_STRUCTURE = 'structure'
ATTR_TRIP_ID = 'trip_id'
ATTR_ETA = 'eta'
ATTR_ETA_WINDOW = 'eta_window'

HOME_MODE_AWAY = 'away'
HOME_MODE_HOME = 'home'

SENSOR_SCHEMA = vol.Schema({
    vol.Optional(CONF_MONITORED_CONDITIONS): vol.All(cv.ensure_list)
})

AWAY_SCHEMA = vol.Schema({
    vol.Required(ATTR_HOME_MODE): vol.In([HOME_MODE_AWAY, HOME_MODE_HOME]),
    vol.Optional(ATTR_STRUCTURE): vol.All(cv.ensure_list, cv.string),
    vol.Optional(ATTR_TRIP_ID): cv.string,
    vol.Optional(ATTR_ETA): cv.time_period,
    vol.Optional(ATTR_ETA_WINDOW): cv.time_period
})

CONFIG_SCHEMA = vol.Schema({
    DOMAIN: vol.Schema({
        vol.Required(CONF_CLIENT_ID): cv.string,
        vol.Required(CONF_CLIENT_SECRET): cv.string,
        vol.Optional(CONF_STRUCTURE): vol.All(cv.ensure_list, cv.string),
        vol.Optional(CONF_SENSORS): SENSOR_SCHEMA,
        vol.Optional(CONF_BINARY_SENSORS): SENSOR_SCHEMA
    })
}, extra=vol.ALLOW_EXTRA)


def nest_update_event_broker(hass, nest):
    """
    Dispatch SIGNAL_NEST_UPDATE to devices when nest stream API received data.

    Runs in its own thread.
    """
    _LOGGER.debug("listening nest.update_event")

    while hass.is_running:
        nest.update_event.wait()

        if not hass.is_running:
            break

        nest.update_event.clear()
        _LOGGER.debug("dispatching nest data update")
        dispatcher_send(hass, SIGNAL_NEST_UPDATE)

    _LOGGER.debug("stop listening nest.update_event")


async def async_setup(hass, config):
    """Set up Nest components."""
    if DOMAIN not in config:
        return True

    conf = config[DOMAIN]

    local_auth.initialize(hass, conf[CONF_CLIENT_ID], conf[CONF_CLIENT_SECRET])

    filename = config.get(CONF_FILENAME, NEST_CONFIG_FILE)
    access_token_cache_file = hass.config.path(filename)

    hass.async_add_job(hass.config_entries.flow.async_init(
        DOMAIN, context={'source': config_entries.SOURCE_IMPORT},
        data={
            'nest_conf_path': access_token_cache_file,
        }
    ))

    # Store config to be used during entry setup
    hass.data[DATA_NEST_CONFIG] = conf

    return True


async def async_setup_entry(hass, entry):
    """Setup Nest from a config entry."""
    from nest import Nest

    nest = Nest(access_token=entry.data['tokens']['access_token'])

    _LOGGER.debug("proceeding with setup")
    conf = hass.data.get(DATA_NEST_CONFIG, {})
    hass.data[DATA_NEST] = NestDevice(hass, conf, nest)
    if not await hass.async_add_job(hass.data[DATA_NEST].initialize):
        return False

    for component in 'climate', 'camera', 'sensor', 'binary_sensor':
        hass.async_create_task(hass.config_entries.async_forward_entry_setup(
            entry, component))

    def set_mode(service):
        """
        Set the home/away mode for a Nest structure.

        You can set optional eta information when set mode to away.
        """
        if ATTR_STRUCTURE in service.data:
            structures = service.data[ATTR_STRUCTURE]
        else:
            structures = hass.data[DATA_NEST].local_structure

        for structure in nest.structures:
            if structure.name in structures:
                _LOGGER.info("Setting mode for %s", structure.name)
                structure.away = service.data[ATTR_HOME_MODE]

                if service.data[ATTR_HOME_MODE] == HOME_MODE_AWAY \
                        and ATTR_ETA in service.data:
                    now = datetime.utcnow()
                    eta_begin = now + service.data[ATTR_ETA]
                    eta_window = service.data.get(ATTR_ETA_WINDOW,
                                                  timedelta(minutes=1))
                    eta_end = eta_begin + eta_window
                    trip_id = service.data.get(
                        ATTR_TRIP_ID, "trip_{}".format(int(now.timestamp())))
                    _LOGGER.info("Setting eta for %s, eta window starts at "
                                 "%s ends at %s", trip_id, eta_begin, eta_end)
                    structure.set_eta(trip_id, eta_begin, eta_end)
            else:
                _LOGGER.error("Invalid structure %s",
                              service.data[ATTR_STRUCTURE])

    hass.services.async_register(
        DOMAIN, 'set_mode', set_mode, schema=AWAY_SCHEMA)

    @callback
    def start_up(event):
        """Start Nest update event listener."""
        threading.Thread(
            name='Nest update listener',
            target=nest_update_event_broker,
            args=(hass, nest)
        ).start()

    hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_up)

    @callback
    def shut_down(event):
        """Stop Nest update event listener."""
        nest.update_event.set()

    hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shut_down)

    _LOGGER.debug("async_setup_nest is done")

    return True


class NestDevice:
    """Structure Nest functions for hass."""

    def __init__(self, hass, conf, nest):
        """Init Nest Devices."""
        self.hass = hass
        self.nest = nest
        self.local_structure = conf.get(CONF_STRUCTURE)

    def initialize(self):
        """Initialize Nest."""
        from nest.nest import AuthorizationError, APIError
        try:
            # Do not optimize next statement, it is here for initialize
            # persistence Nest API connection.
            structure_names = [s.name for s in self.nest.structures]
            if self.local_structure is None:
                self.local_structure = structure_names

        except (AuthorizationError, APIError, socket.error) as err:
            _LOGGER.error(
                "Connection error while access Nest web service: %s", err)
            return False
        return True

    def structures(self):
        """Generate a list of structures."""
        from nest.nest import AuthorizationError, APIError
        try:
            for structure in self.nest.structures:
                if structure.name not in self.local_structure:
                    _LOGGER.debug("Ignoring structure %s, not in %s",
                                  structure.name, self.local_structure)
                    continue
                yield structure

        except (AuthorizationError, APIError, socket.error) as err:
            _LOGGER.error(
                "Connection error while access Nest web service: %s", err)

    def thermostats(self):
        """Generate a list of thermostats."""
        return self._devices('thermostats')

    def smoke_co_alarms(self):
        """Generate a list of smoke co alarms."""
        return self._devices('smoke_co_alarms')

    def cameras(self):
        """Generate a list of cameras."""
        return self._devices('cameras')

    def _devices(self, device_type):
        """Generate a list of Nest devices."""
        from nest.nest import AuthorizationError, APIError
        try:
            for structure in self.nest.structures:
                if structure.name not in self.local_structure:
                    _LOGGER.debug("Ignoring structure %s, not in %s",
                                  structure.name, self.local_structure)
                    continue

                for device in getattr(structure, device_type, []):
                    try:
                        # Do not optimize next statement,
                        # it is here for verify Nest API permission.
                        device.name_long
                    except KeyError:
                        _LOGGER.warning("Cannot retrieve device name for [%s]"
                                        ", please check your Nest developer "
                                        "account permission settings.",
                                        device.serial)
                        continue
                    yield (structure, device)

        except (AuthorizationError, APIError, socket.error) as err:
            _LOGGER.error(
                "Connection error while access Nest web service: %s", err)


class NestSensorDevice(Entity):
    """Representation of a Nest sensor."""

    def __init__(self, structure, device, variable):
        """Initialize the sensor."""
        self.structure = structure
        self.variable = variable

        if device is not None:
            # device specific
            self.device = device
            self._name = "{} {}".format(self.device.name_long,
                                        self.variable.replace('_', ' '))
        else:
            # structure only
            self.device = structure
            self._name = "{} {}".format(self.structure.name,
                                        self.variable.replace('_', ' '))

        self._state = None
        self._unit = None

    @property
    def name(self):
        """Return the name of the nest, if any."""
        return self._name

    @property
    def unit_of_measurement(self):
        """Return the unit the value is expressed in."""
        return self._unit

    @property
    def should_poll(self):
        """Do not need poll thanks using Nest streaming API."""
        return False

    def update(self):
        """Do not use NestSensorDevice directly."""
        raise NotImplementedError

    async def async_added_to_hass(self):
        """Register update signal handler."""
        async def async_update_state():
            """Update sensor state."""
            await self.async_update_ha_state(True)

        async_dispatcher_connect(self.hass, SIGNAL_NEST_UPDATE,
                                 async_update_state)
