"""Support for Minut Point."""
import asyncio
import logging

import voluptuous as vol

from homeassistant import config_entries
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_TOKEN, CONF_WEBHOOK_ID
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.dispatcher import (
    async_dispatcher_connect, async_dispatcher_send)
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.util.dt import as_local, parse_datetime, utc_from_timestamp

from . import config_flow  # noqa  pylint_disable=unused-import
from .const import (
    CONF_WEBHOOK_URL, DOMAIN, EVENT_RECEIVED, POINT_DISCOVERY_NEW,
    SCAN_INTERVAL, SIGNAL_UPDATE_ENTITY, SIGNAL_WEBHOOK)

_LOGGER = logging.getLogger(__name__)

CONF_CLIENT_ID = 'client_id'
CONF_CLIENT_SECRET = 'client_secret'

DATA_CONFIG_ENTRY_LOCK = 'point_config_entry_lock'
CONFIG_ENTRY_IS_SETUP = 'point_config_entry_is_setup'

CONFIG_SCHEMA = vol.Schema(
    {
        DOMAIN:
        vol.Schema({
            vol.Required(CONF_CLIENT_ID): cv.string,
            vol.Required(CONF_CLIENT_SECRET): cv.string,
        })
    },
    extra=vol.ALLOW_EXTRA,
)


async def async_setup(hass, config):
    """Set up the Minut Point component."""
    if DOMAIN not in config:
        return True

    conf = config[DOMAIN]

    config_flow.register_flow_implementation(
        hass, DOMAIN, conf[CONF_CLIENT_ID],
        conf[CONF_CLIENT_SECRET])

    hass.async_create_task(
        hass.config_entries.flow.async_init(
            DOMAIN,
            context={'source': config_entries.SOURCE_IMPORT},
        ))

    return True


async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry):
    """Set up Point from a config entry."""
    from pypoint import PointSession

    def token_saver(token):
        _LOGGER.debug('Saving updated token')
        entry.data[CONF_TOKEN] = token
        hass.config_entries.async_update_entry(entry, data={**entry.data})

    # Force token update.
    entry.data[CONF_TOKEN]['expires_in'] = -1
    session = PointSession(
        entry.data['refresh_args']['client_id'],
        token=entry.data[CONF_TOKEN],
        auto_refresh_kwargs=entry.data['refresh_args'],
        token_saver=token_saver,
    )

    if not session.is_authorized:
        _LOGGER.error('Authentication Error')
        return False

    hass.data[DATA_CONFIG_ENTRY_LOCK] = asyncio.Lock()
    hass.data[CONFIG_ENTRY_IS_SETUP] = set()

    await async_setup_webhook(hass, entry, session)
    client = MinutPointClient(hass, entry, session)
    hass.data.setdefault(DOMAIN, {}).update({entry.entry_id: client})
    hass.async_create_task(client.update())

    return True


async def async_setup_webhook(hass: HomeAssistantType, entry: ConfigEntry,
                              session):
    """Set up a webhook to handle binary sensor events."""
    if CONF_WEBHOOK_ID not in entry.data:
        entry.data[CONF_WEBHOOK_ID] = \
            hass.components.webhook.async_generate_id()
        entry.data[CONF_WEBHOOK_URL] = \
            hass.components.webhook.async_generate_url(
                entry.data[CONF_WEBHOOK_ID])
        _LOGGER.info('Registering new webhook at: %s',
                     entry.data[CONF_WEBHOOK_URL])
        hass.config_entries.async_update_entry(
            entry, data={
                **entry.data,
            })
    await hass.async_add_executor_job(
        session.update_webhook,
        entry.data[CONF_WEBHOOK_URL],
        entry.data[CONF_WEBHOOK_ID],
        ['*'])

    hass.components.webhook.async_register(
        DOMAIN, 'Point', entry.data[CONF_WEBHOOK_ID], handle_webhook)


async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry):
    """Unload a config entry."""
    hass.components.webhook.async_unregister(entry.data[CONF_WEBHOOK_ID])
    session = hass.data[DOMAIN].pop(entry.entry_id)
    await hass.async_add_executor_job(session.remove_webhook)

    if not hass.data[DOMAIN]:
        hass.data.pop(DOMAIN)

    for component in ('binary_sensor', 'sensor'):
        await hass.config_entries.async_forward_entry_unload(
            entry, component)

    return True


async def handle_webhook(hass, webhook_id, request):
    """Handle webhook callback."""
    try:
        data = await request.json()
        _LOGGER.debug("Webhook %s: %s", webhook_id, data)
    except ValueError:
        return None

    if isinstance(data, dict):
        data['webhook_id'] = webhook_id
        async_dispatcher_send(hass, SIGNAL_WEBHOOK, data, data.get('hook_id'))
    hass.bus.async_fire(EVENT_RECEIVED, data)


class MinutPointClient():
    """Get the latest data and update the states."""

    def __init__(self, hass: HomeAssistantType, config_entry: ConfigEntry,
                 session):
        """Initialize the Minut data object."""
        self._known_devices = set()
        self._known_homes = set()
        self._hass = hass
        self._config_entry = config_entry
        self._is_available = True
        self._client = session

        async_track_time_interval(self._hass, self.update, SCAN_INTERVAL)

    async def update(self, *args):
        """Periodically poll the cloud for current state."""
        await self._sync()

    async def _sync(self):
        """Update local list of devices."""
        if not await self._hass.async_add_executor_job(
                self._client.update) and self._is_available:
            self._is_available = False
            _LOGGER.warning("Device is unavailable")
            return

        async def new_device(device_id, component):
            """Load new device."""
            config_entries_key = '{}.{}'.format(component, DOMAIN)
            async with self._hass.data[DATA_CONFIG_ENTRY_LOCK]:
                if config_entries_key not in self._hass.data[
                        CONFIG_ENTRY_IS_SETUP]:
                    await self._hass.config_entries.async_forward_entry_setup(
                        self._config_entry, component)
                    self._hass.data[CONFIG_ENTRY_IS_SETUP].add(
                        config_entries_key)

            async_dispatcher_send(
                self._hass, POINT_DISCOVERY_NEW.format(component, DOMAIN),
                device_id)

        self._is_available = True
        for home_id in self._client.homes:
            if home_id not in self._known_homes:
                await new_device(home_id, 'alarm_control_panel')
                self._known_homes.add(home_id)
        for device in self._client.devices:
            if device.device_id not in self._known_devices:
                for component in ('sensor', 'binary_sensor'):
                    await new_device(device.device_id, component)
                self._known_devices.add(device.device_id)
        async_dispatcher_send(self._hass, SIGNAL_UPDATE_ENTITY)

    def device(self, device_id):
        """Return device representation."""
        return self._client.device(device_id)

    def is_available(self, device_id):
        """Return device availability."""
        return device_id in self._client.device_ids

    def remove_webhook(self):
        """Remove the session webhook."""
        return self._client.remove_webhook()

    @property
    def homes(self):
        """Return known homes."""
        return self._client.homes

    def alarm_disarm(self, home_id):
        """Send alarm disarm command."""
        return self._client.alarm_disarm(home_id)

    def alarm_arm(self, home_id):
        """Send alarm arm command."""
        return self._client.alarm_arm(home_id)


class MinutPointEntity(Entity):
    """Base Entity used by the sensors."""

    def __init__(self, point_client, device_id, device_class):
        """Initialize the entity."""
        self._async_unsub_dispatcher_connect = None
        self._client = point_client
        self._id = device_id
        self._name = self.device.name
        self._device_class = device_class
        self._updated = utc_from_timestamp(0)
        self._value = None

    def __str__(self):
        """Return string representation of device."""
        return "MinutPoint {}".format(self.name)

    async def async_added_to_hass(self):
        """Call when entity is added to hass."""
        _LOGGER.debug('Created device %s', self)
        self._async_unsub_dispatcher_connect = async_dispatcher_connect(
            self.hass, SIGNAL_UPDATE_ENTITY, self._update_callback)
        await self._update_callback()

    async def async_will_remove_from_hass(self):
        """Disconnect dispatcher listener when removed."""
        if self._async_unsub_dispatcher_connect:
            self._async_unsub_dispatcher_connect()

    async def _update_callback(self):
        """Update the value of the sensor."""
        pass

    @property
    def available(self):
        """Return true if device is not offline."""
        return self._client.is_available(self.device_id)

    @property
    def device(self):
        """Return the representation of the device."""
        return self._client.device(self.device_id)

    @property
    def device_class(self):
        """Return the device class."""
        return self._device_class

    @property
    def device_id(self):
        """Return the id of the device."""
        return self._id

    @property
    def device_state_attributes(self):
        """Return status of device."""
        attrs = self.device.device_status
        attrs['last_heard_from'] = \
            as_local(self.last_update).strftime("%Y-%m-%d %H:%M:%S")
        return attrs

    @property
    def device_info(self):
        """Return a device description for device registry."""
        device = self.device.device
        return {
            'connections': {('mac', device['device_mac'])},
            'identifieres': device['device_id'],
            'manufacturer': 'Minut',
            'model': 'Point v{}'.format(device['hardware_version']),
            'name': device['description'],
            'sw_version': device['firmware']['installed'],
            'via_hub': (DOMAIN, device['home']),
        }

    @property
    def name(self):
        """Return the display name of this device."""
        return "{} {}".format(self._name, self.device_class.capitalize())

    @property
    def is_updated(self):
        """Return true if sensor have been updated."""
        return self.last_update > self._updated

    @property
    def last_update(self):
        """Return the last_update time for the device."""
        last_update = parse_datetime(self.device.last_update)
        return last_update

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

    @property
    def unique_id(self):
        """Return the unique id of the sensor."""
        return 'point.{}-{}'.format(self._id, self.device_class)

    @property
    def value(self):
        """Return the sensor value."""
        return self._value
