"""Support for August devices."""
import logging
from datetime import timedelta

import voluptuous as vol
from requests import RequestException

import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
    CONF_PASSWORD, CONF_USERNAME, CONF_TIMEOUT, EVENT_HOMEASSISTANT_STOP)
from homeassistant.helpers import discovery
from homeassistant.util import Throttle

_LOGGER = logging.getLogger(__name__)

_CONFIGURING = {}

DEFAULT_TIMEOUT = 10
ACTIVITY_FETCH_LIMIT = 10
ACTIVITY_INITIAL_FETCH_LIMIT = 20

CONF_LOGIN_METHOD = 'login_method'
CONF_INSTALL_ID = 'install_id'

NOTIFICATION_ID = 'august_notification'
NOTIFICATION_TITLE = "August Setup"

AUGUST_CONFIG_FILE = '.august.conf'

DATA_AUGUST = 'august'
DOMAIN = 'august'
DEFAULT_ENTITY_NAMESPACE = 'august'
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5)
DEFAULT_SCAN_INTERVAL = timedelta(seconds=5)
LOGIN_METHODS = ['phone', 'email']

CONFIG_SCHEMA = vol.Schema({
    DOMAIN: vol.Schema({
        vol.Required(CONF_LOGIN_METHOD): vol.In(LOGIN_METHODS),
        vol.Required(CONF_USERNAME): cv.string,
        vol.Required(CONF_PASSWORD): cv.string,
        vol.Optional(CONF_INSTALL_ID): cv.string,
        vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
    })
}, extra=vol.ALLOW_EXTRA)

AUGUST_COMPONENTS = [
    'camera', 'binary_sensor', 'lock'
]


def request_configuration(hass, config, api, authenticator):
    """Request configuration steps from the user."""
    configurator = hass.components.configurator

    def august_configuration_callback(data):
        """Run when the configuration callback is called."""
        from august.authenticator import ValidationResult

        result = authenticator.validate_verification_code(
            data.get('verification_code'))

        if result == ValidationResult.INVALID_VERIFICATION_CODE:
            configurator.notify_errors(_CONFIGURING[DOMAIN],
                                       "Invalid verification code")
        elif result == ValidationResult.VALIDATED:
            setup_august(hass, config, api, authenticator)

    if DOMAIN not in _CONFIGURING:
        authenticator.send_verification_code()

    conf = config[DOMAIN]
    username = conf.get(CONF_USERNAME)
    login_method = conf.get(CONF_LOGIN_METHOD)

    _CONFIGURING[DOMAIN] = configurator.request_config(
        NOTIFICATION_TITLE,
        august_configuration_callback,
        description="Please check your {} ({}) and enter the verification "
                    "code below".format(login_method, username),
        submit_caption='Verify',
        fields=[{
            'id': 'verification_code',
            'name': "Verification code",
            'type': 'string'}]
    )


def setup_august(hass, config, api, authenticator):
    """Set up the August component."""
    from august.authenticator import AuthenticationState

    authentication = None
    try:
        authentication = authenticator.authenticate()
    except RequestException as ex:
        _LOGGER.error("Unable to connect to August service: %s", str(ex))

        hass.components.persistent_notification.create(
            "Error: {}<br />"
            "You will need to restart hass after fixing."
            "".format(ex),
            title=NOTIFICATION_TITLE,
            notification_id=NOTIFICATION_ID)

    state = authentication.state

    if state == AuthenticationState.AUTHENTICATED:
        if DOMAIN in _CONFIGURING:
            hass.components.configurator.request_done(_CONFIGURING.pop(DOMAIN))

        hass.data[DATA_AUGUST] = AugustData(
            hass, api, authentication.access_token)

        for component in AUGUST_COMPONENTS:
            discovery.load_platform(hass, component, DOMAIN, {}, config)

        return True
    if state == AuthenticationState.BAD_PASSWORD:
        _LOGGER.error("Invalid password provided")
        return False
    if state == AuthenticationState.REQUIRES_VALIDATION:
        request_configuration(hass, config, api, authenticator)
        return True

    return False


def setup(hass, config):
    """Set up the August component."""
    from august.api import Api
    from august.authenticator import Authenticator
    from requests import Session

    conf = config[DOMAIN]
    api_http_session = None
    try:
        api_http_session = Session()
    except RequestException as ex:
        _LOGGER.warning("Creating HTTP session failed with: %s", str(ex))

    api = Api(timeout=conf.get(CONF_TIMEOUT), http_session=api_http_session)

    authenticator = Authenticator(
        api,
        conf.get(CONF_LOGIN_METHOD),
        conf.get(CONF_USERNAME),
        conf.get(CONF_PASSWORD),
        install_id=conf.get(CONF_INSTALL_ID),
        access_token_cache_file=hass.config.path(AUGUST_CONFIG_FILE))

    def close_http_session(event):
        """Close API sessions used to connect to August."""
        _LOGGER.debug("Closing August HTTP sessions")
        if api_http_session:
            try:
                api_http_session.close()
            except RequestException:
                pass

        _LOGGER.debug("August HTTP session closed.")

    hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, close_http_session)
    _LOGGER.debug("Registered for HASS stop event")

    return setup_august(hass, config, api, authenticator)


class AugustData:
    """August data object."""

    def __init__(self, hass, api, access_token):
        """Init August data object."""
        self._hass = hass
        self._api = api
        self._access_token = access_token
        self._doorbells = self._api.get_doorbells(self._access_token) or []
        self._locks = self._api.get_operable_locks(self._access_token) or []
        self._house_ids = [d.house_id for d in self._doorbells + self._locks]

        self._doorbell_detail_by_id = {}
        self._lock_status_by_id = {}
        self._lock_detail_by_id = {}
        self._door_state_by_id = {}
        self._activities_by_id = {}

    @property
    def house_ids(self):
        """Return a list of house_ids."""
        return self._house_ids

    @property
    def doorbells(self):
        """Return a list of doorbells."""
        return self._doorbells

    @property
    def locks(self):
        """Return a list of locks."""
        return self._locks

    def get_device_activities(self, device_id, *activity_types):
        """Return a list of activities."""
        _LOGGER.debug("Getting device activities")
        self._update_device_activities()

        activities = self._activities_by_id.get(device_id, [])
        if activity_types:
            return [a for a in activities if a.activity_type in activity_types]
        return activities

    def get_latest_device_activity(self, device_id, *activity_types):
        """Return latest activity."""
        activities = self.get_device_activities(device_id, *activity_types)
        return next(iter(activities or []), None)

    @Throttle(MIN_TIME_BETWEEN_UPDATES)
    def _update_device_activities(self, limit=ACTIVITY_FETCH_LIMIT):
        """Update data object with latest from August API."""
        _LOGGER.debug("Start retrieving device activities")
        for house_id in self.house_ids:
            _LOGGER.debug("Updating device activity for house id %s",
                          house_id)

            activities = self._api.get_house_activities(self._access_token,
                                                        house_id,
                                                        limit=limit)

            device_ids = {a.device_id for a in activities}
            for device_id in device_ids:
                self._activities_by_id[device_id] = [a for a in activities if
                                                     a.device_id == device_id]
        _LOGGER.debug("Completed retrieving device activities")

    def get_doorbell_detail(self, doorbell_id):
        """Return doorbell detail."""
        self._update_doorbells()
        return self._doorbell_detail_by_id.get(doorbell_id)

    @Throttle(MIN_TIME_BETWEEN_UPDATES)
    def _update_doorbells(self):
        detail_by_id = {}

        _LOGGER.debug("Start retrieving doorbell details")
        for doorbell in self._doorbells:
            _LOGGER.debug("Updating doorbell status for %s",
                          doorbell.device_name)
            try:
                detail_by_id[doorbell.device_id] =\
                    self._api.get_doorbell_detail(
                        self._access_token, doorbell.device_id)
            except RequestException as ex:
                _LOGGER.error("Request error trying to retrieve doorbell"
                              " status for %s. %s", doorbell.device_name, ex)
                detail_by_id[doorbell.device_id] = None
            except Exception:
                detail_by_id[doorbell.device_id] = None
                raise

        _LOGGER.debug("Completed retrieving doorbell details")
        self._doorbell_detail_by_id = detail_by_id

    def get_lock_status(self, lock_id):
        """Return status if the door is locked or unlocked.

        This is status for the lock itself.
        """
        self._update_locks()
        return self._lock_status_by_id.get(lock_id)

    def get_lock_detail(self, lock_id):
        """Return lock detail."""
        self._update_locks()
        return self._lock_detail_by_id.get(lock_id)

    def get_door_state(self, lock_id):
        """Return status if the door is open or closed.

        This is the status from the door sensor.
        """
        self._update_doors()
        return self._door_state_by_id.get(lock_id)

    @Throttle(MIN_TIME_BETWEEN_UPDATES)
    def _update_doors(self):
        state_by_id = {}

        _LOGGER.debug("Start retrieving door status")
        for lock in self._locks:
            _LOGGER.debug("Updating door status for %s",
                          lock.device_name)

            try:
                state_by_id[lock.device_id] = self._api.get_lock_door_status(
                    self._access_token, lock.device_id)
            except RequestException as ex:
                _LOGGER.error("Request error trying to retrieve door"
                              " status for %s. %s", lock.device_name, ex)
                state_by_id[lock.device_id] = None
            except Exception:
                state_by_id[lock.device_id] = None
                raise

        _LOGGER.debug("Completed retrieving door status")
        self._door_state_by_id = state_by_id

    @Throttle(MIN_TIME_BETWEEN_UPDATES)
    def _update_locks(self):
        status_by_id = {}
        detail_by_id = {}

        _LOGGER.debug("Start retrieving locks status")
        for lock in self._locks:
            _LOGGER.debug("Updating lock status for %s",
                          lock.device_name)
            try:
                status_by_id[lock.device_id] = self._api.get_lock_status(
                    self._access_token, lock.device_id)
            except RequestException as ex:
                _LOGGER.error("Request error trying to retrieve door"
                              " status for %s. %s", lock.device_name, ex)
                status_by_id[lock.device_id] = None
            except Exception:
                status_by_id[lock.device_id] = None
                raise

            try:
                detail_by_id[lock.device_id] = self._api.get_lock_detail(
                    self._access_token, lock.device_id)
            except RequestException as ex:
                _LOGGER.error("Request error trying to retrieve door"
                              " details for %s. %s", lock.device_name, ex)
                detail_by_id[lock.device_id] = None
            except Exception:
                detail_by_id[lock.device_id] = None
                raise

        _LOGGER.debug("Completed retrieving locks status")
        self._lock_status_by_id = status_by_id
        self._lock_detail_by_id = detail_by_id

    def lock(self, device_id):
        """Lock the device."""
        return self._api.lock(self._access_token, device_id)

    def unlock(self, device_id):
        """Unlock the device."""
        return self._api.unlock(self._access_token, device_id)
