"""SmartThings Cloud integration for Home Assistant."""

import asyncio
import logging
from typing import Iterable

from aiohttp.client_exceptions import (
    ClientConnectionError, ClientResponseError)

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ACCESS_TOKEN
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.dispatcher import (
    async_dispatcher_connect, async_dispatcher_send)
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.typing import ConfigType, HomeAssistantType

from .config_flow import SmartThingsFlowHandler  # noqa
from .const import (
    CONF_APP_ID, CONF_INSTALLED_APP_ID, DATA_BROKERS, DATA_MANAGER, DOMAIN,
    EVENT_BUTTON, SIGNAL_SMARTTHINGS_UPDATE, SUPPORTED_PLATFORMS)
from .smartapp import (
    setup_smartapp, setup_smartapp_endpoint, validate_installed_app)

REQUIREMENTS = ['pysmartapp==0.3.0', 'pysmartthings==0.4.2']
DEPENDENCIES = ['webhook']

_LOGGER = logging.getLogger(__name__)


async def async_setup(hass: HomeAssistantType, config: ConfigType):
    """Initialize the SmartThings platform."""
    await setup_smartapp_endpoint(hass)
    return True


async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry):
    """Initialize config entry which represents an installed SmartApp."""
    from pysmartthings import SmartThings

    if not hass.config.api.base_url.lower().startswith('https://'):
        _LOGGER.warning("The 'base_url' of the 'http' component must be "
                        "configured and start with 'https://'")
        return False

    api = SmartThings(async_get_clientsession(hass),
                      entry.data[CONF_ACCESS_TOKEN])

    remove_entry = False
    try:
        # See if the app is already setup. This occurs when there are
        # installs in multiple SmartThings locations (valid use-case)
        manager = hass.data[DOMAIN][DATA_MANAGER]
        smart_app = manager.smartapps.get(entry.data[CONF_APP_ID])
        if not smart_app:
            # Validate and setup the app.
            app = await api.app(entry.data[CONF_APP_ID])
            smart_app = setup_smartapp(hass, app)

        # Validate and retrieve the installed app.
        installed_app = await validate_installed_app(
            api, entry.data[CONF_INSTALLED_APP_ID])

        # Get devices and their current status
        devices = await api.devices(
            location_ids=[installed_app.location_id])

        async def retrieve_device_status(device):
            try:
                await device.status.refresh()
            except ClientResponseError:
                _LOGGER.debug("Unable to update status for device: %s (%s), "
                              "the device will be ignored",
                              device.label, device.device_id, exc_info=True)
                devices.remove(device)

        await asyncio.gather(*[retrieve_device_status(d)
                               for d in devices.copy()])

        # Setup device broker
        broker = DeviceBroker(hass, devices,
                              installed_app.installed_app_id)
        broker.event_handler_disconnect = \
            smart_app.connect_event(broker.event_handler)
        hass.data[DOMAIN][DATA_BROKERS][entry.entry_id] = broker

    except ClientResponseError as ex:
        if ex.status in (401, 403):
            _LOGGER.exception("Unable to setup config entry '%s' - please "
                              "reconfigure the integration", entry.title)
            remove_entry = True
        else:
            _LOGGER.debug(ex, exc_info=True)
            raise ConfigEntryNotReady
    except (ClientConnectionError, RuntimeWarning) as ex:
        _LOGGER.debug(ex, exc_info=True)
        raise ConfigEntryNotReady

    if remove_entry:
        hass.async_create_task(
            hass.config_entries.async_remove(entry.entry_id))
        # only create new flow if there isn't a pending one for SmartThings.
        flows = hass.config_entries.flow.async_progress()
        if not [flow for flow in flows if flow['handler'] == DOMAIN]:
            hass.async_create_task(
                hass.config_entries.flow.async_init(
                    DOMAIN, context={'source': 'import'}))
        return False

    for component in SUPPORTED_PLATFORMS:
        hass.async_create_task(hass.config_entries.async_forward_entry_setup(
            entry, component))
    return True


async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry):
    """Unload a config entry."""
    broker = hass.data[DOMAIN][DATA_BROKERS].pop(entry.entry_id, None)
    if broker and broker.event_handler_disconnect:
        broker.event_handler_disconnect()

    tasks = [hass.config_entries.async_forward_entry_unload(entry, component)
             for component in SUPPORTED_PLATFORMS]
    return all(await asyncio.gather(*tasks))


class DeviceBroker:
    """Manages an individual SmartThings config entry."""

    def __init__(self, hass: HomeAssistantType, devices: Iterable,
                 installed_app_id: str):
        """Create a new instance of the DeviceBroker."""
        self._hass = hass
        self._installed_app_id = installed_app_id
        self.devices = {device.device_id: device for device in devices}
        self.event_handler_disconnect = None

    async def event_handler(self, req, resp, app):
        """Broker for incoming events."""
        from pysmartapp.event import EVENT_TYPE_DEVICE

        # Do not process events received from a different installed app
        # under the same parent SmartApp (valid use-scenario)
        if req.installed_app_id != self._installed_app_id:
            return

        updated_devices = set()
        for evt in req.events:
            if evt.event_type != EVENT_TYPE_DEVICE:
                continue
            device = self.devices.get(evt.device_id)
            if not device:
                continue
            device.status.apply_attribute_update(
                evt.component_id, evt.capability, evt.attribute, evt.value)

            # Fire events for buttons
            if evt.capability == 'button' and evt.attribute == 'button':
                data = {
                    'component_id': evt.component_id,
                    'device_id': evt.device_id,
                    'location_id': evt.location_id,
                    'value': evt.value,
                    'name': device.label
                }
                self._hass.bus.async_fire(EVENT_BUTTON, data)
                _LOGGER.debug("Fired button event: %s", data)

            updated_devices.add(device.device_id)
        _LOGGER.debug("Update received with %s events and updated %s devices",
                      len(req.events), len(updated_devices))

        async_dispatcher_send(self._hass, SIGNAL_SMARTTHINGS_UPDATE,
                              updated_devices)


class SmartThingsEntity(Entity):
    """Defines a SmartThings entity."""

    def __init__(self, device):
        """Initialize the instance."""
        self._device = device
        self._dispatcher_remove = None

    async def async_added_to_hass(self):
        """Device added to hass."""
        async def async_update_state(devices):
            """Update device state."""
            if self._device.device_id in devices:
                await self.async_update_ha_state(True)

        self._dispatcher_remove = async_dispatcher_connect(
            self.hass, SIGNAL_SMARTTHINGS_UPDATE, async_update_state)

    async def async_will_remove_from_hass(self) -> None:
        """Disconnect the device when removed."""
        if self._dispatcher_remove:
            self._dispatcher_remove()

    @property
    def device_info(self):
        """Get attributes about the device."""
        return {
            'identifiers': {
                (DOMAIN, self._device.device_id)
            },
            'name': self._device.label,
            'model': self._device.device_type_name,
            'manufacturer': 'Unavailable'
        }

    @property
    def name(self) -> str:
        """Return the name of the device."""
        return self._device.label

    @property
    def should_poll(self) -> bool:
        """No polling needed for this device."""
        return False

    @property
    def unique_id(self) -> str:
        """Return a unique ID."""
        return self._device.device_id
