"""Support for devices connected to UniFi POE."""
import asyncio
import logging

from datetime import timedelta

import async_timeout

from homeassistant.components import unifi
from homeassistant.components.switch import SwitchDevice
from homeassistant.components.unifi.const import (
    CONF_CONTROLLER, CONF_SITE_ID, CONTROLLER_ID, DOMAIN)
from homeassistant.const import CONF_HOST
from homeassistant.core import callback
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC

DEPENDENCIES = [DOMAIN]
SCAN_INTERVAL = timedelta(seconds=15)

LOGGER = logging.getLogger(__name__)


async def async_setup_platform(
        hass, config, async_add_entities, discovery_info=None):
    """Component doesn't support configuration through configuration.yaml."""
    pass


async def async_setup_entry(hass, config_entry, async_add_entities):
    """Set up switches for UniFi component.

    Switches are controlling network switch ports with Poe.
    """
    controller_id = CONTROLLER_ID.format(
        host=config_entry.data[CONF_CONTROLLER][CONF_HOST],
        site=config_entry.data[CONF_CONTROLLER][CONF_SITE_ID],
    )
    controller = hass.data[unifi.DOMAIN][controller_id]
    switches = {}

    progress = None
    update_progress = set()

    async def request_update(object_id):
        """Request an update."""
        nonlocal progress
        update_progress.add(object_id)

        if progress is not None:
            return await progress

        progress = asyncio.ensure_future(update_controller())
        result = await progress
        progress = None
        update_progress.clear()
        return result

    async def update_controller():
        """Update the values of the controller."""
        tasks = [async_update_items(
            controller, async_add_entities, request_update,
            switches, update_progress
        )]
        await asyncio.wait(tasks)

    await update_controller()


async def async_update_items(controller, async_add_entities,
                             request_controller_update, switches,
                             progress_waiting):
    """Update POE port state from the controller."""
    import aiounifi

    @callback
    def update_switch_state():
        """Tell switches to reload state."""
        for client_id, client in switches.items():
            if client_id not in progress_waiting:
                client.async_schedule_update_ha_state()

    try:
        with async_timeout.timeout(4):
            await controller.api.clients.update()
            await controller.api.devices.update()

    except aiounifi.LoginRequired:
        try:
            with async_timeout.timeout(5):
                await controller.api.login()
        except (asyncio.TimeoutError, aiounifi.AiounifiException):
            if controller.available:
                controller.available = False
                update_switch_state()
            return

    except (asyncio.TimeoutError, aiounifi.AiounifiException):
        if controller.available:
            LOGGER.error('Unable to reach controller %s', controller.host)
            controller.available = False
            update_switch_state()
        return

    if not controller.available:
        LOGGER.info('Reconnected to controller %s', controller.host)
        controller.available = True

    new_switches = []
    devices = controller.api.devices
    for client_id in controller.api.clients:

        if client_id in progress_waiting:
            continue

        if client_id in switches:
            LOGGER.debug("Updating UniFi switch %s (%s)",
                         switches[client_id].entity_id,
                         switches[client_id].client.mac)
            switches[client_id].async_schedule_update_ha_state()
            continue

        client = controller.api.clients[client_id]
        # Network device with active POE
        if not client.is_wired or client.sw_mac not in devices or \
           not devices[client.sw_mac].ports[client.sw_port].port_poe or \
           not devices[client.sw_mac].ports[client.sw_port].poe_enable or \
           controller.mac == client.mac:
            continue

        # Multiple POE-devices on same port means non UniFi POE driven switch
        multi_clients_on_port = False
        for client2 in controller.api.clients.values():
            if client.mac != client2.mac and \
               client.sw_mac == client2.sw_mac and \
               client.sw_port == client2.sw_port:
                multi_clients_on_port = True
                break

        if multi_clients_on_port:
            continue

        switches[client_id] = UniFiSwitch(
            client, controller, request_controller_update)
        new_switches.append(switches[client_id])
        LOGGER.debug("New UniFi switch %s (%s)", client.hostname, client.mac)

    if new_switches:
        async_add_entities(new_switches)


class UniFiSwitch(SwitchDevice):
    """Representation of a client that uses POE."""

    def __init__(self, client, controller, request_controller_update):
        """Set up switch."""
        self.client = client
        self.controller = controller
        self.poe_mode = None
        if self.port.poe_mode != 'off':
            self.poe_mode = self.port.poe_mode
        self.async_request_controller_update = request_controller_update

    async def async_update(self):
        """Synchronize state with controller."""
        await self.async_request_controller_update(self.client.mac)

    @property
    def name(self):
        """Return the name of the switch."""
        return self.client.hostname

    @property
    def unique_id(self):
        """Return a unique identifier for this switch."""
        return 'poe-{}'.format(self.client.mac)

    @property
    def is_on(self):
        """Return true if POE is active."""
        return self.port.poe_mode != 'off'

    @property
    def available(self):
        """Return if switch is available."""
        return self.controller.available or \
            self.client.sw_mac in self.controller.api.devices

    async def async_turn_on(self, **kwargs):
        """Enable POE for client."""
        await self.device.async_set_port_poe_mode(
            self.client.sw_port, self.poe_mode)

    async def async_turn_off(self, **kwargs):
        """Disable POE for client."""
        await self.device.async_set_port_poe_mode(self.client.sw_port, 'off')

    @property
    def device_state_attributes(self):
        """Return the device state attributes."""
        attributes = {
            'power': self.port.poe_power,
            'received': self.client.wired_rx_bytes / 1000000,
            'sent': self.client.wired_tx_bytes / 1000000,
            'switch': self.client.sw_mac,
            'port': self.client.sw_port,
            'poe_mode': self.poe_mode
        }
        return attributes

    @property
    def device_info(self):
        """Return a device description for device registry."""
        return {
            'connections': {(CONNECTION_NETWORK_MAC, self.client.mac)}
        }

    @property
    def device(self):
        """Shortcut to the switch that client is connected to."""
        return self.controller.api.devices[self.client.sw_mac]

    @property
    def port(self):
        """Shortcut to the switch port that client is connected to."""
        return self.device.ports[self.client.sw_port]
