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

from homeassistant.components import unifi
from homeassistant.components.switch import SwitchDevice
from homeassistant.const import CONF_HOST
from homeassistant.core import callback
from homeassistant.helpers import entity_registry
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.restore_state import RestoreEntity

from .const import CONF_CONTROLLER, CONF_SITE_ID, CONTROLLER_ID

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]

    if controller.site_role != "admin":
        return

    switches = {}
    switches_off = []

    registry = await entity_registry.async_get_registry(hass)

    # Restore clients that is not a part of active clients list.
    for entity in registry.entities.values():

        if (
            entity.config_entry_id == config_entry.entry_id
            and entity.unique_id.startswith("poe-")
        ):

            _, mac = entity.unique_id.split("-", 1)

            if mac in controller.api.clients or mac not in controller.api.clients_all:
                continue

            client = controller.api.clients_all[mac]
            controller.api.clients.process_raw([client.raw])
            switches_off.append(entity.unique_id)

    @callback
    def update_controller():
        """Update the values of the controller."""
        update_items(controller, async_add_entities, switches, switches_off)

    async_dispatcher_connect(hass, controller.event_update, update_controller)

    update_controller()
    switches_off.clear()


@callback
def update_items(controller, async_add_entities, switches, switches_off):
    """Update POE port state from the controller."""
    new_switches = []
    devices = controller.api.devices

    # block client
    for client_id in controller.block_clients:

        block_client_id = "block-{}".format(client_id)

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

        if client_id not in controller.api.clients_all:
            continue

        client = controller.api.clients_all[client_id]
        switches[block_client_id] = UniFiBlockClientSwitch(client, controller)
        new_switches.append(switches[block_client_id])
        LOGGER.debug("New UniFi Block switch %s (%s)", client.hostname, client.mac)

    # control poe
    for client_id in controller.api.clients:

        poe_client_id = "poe-{}".format(client_id)

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

        client = controller.api.clients[client_id]

        if poe_client_id in switches_off:
            pass
        # Network device with active POE
        elif (
            not client.is_wired
            or client.sw_mac not in devices
            or not devices[client.sw_mac].ports[client.sw_port].port_poe
            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 poe_client_id in switches_off:
                break

            if (
                client2.is_wired
                and 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[poe_client_id] = UniFiPOEClientSwitch(client, controller)
        new_switches.append(switches[poe_client_id])
        LOGGER.debug("New UniFi POE switch %s (%s)", client.hostname, client.mac)

    if new_switches:
        async_add_entities(new_switches)


class UniFiClient:
    """Base class for UniFi switches."""

    def __init__(self, client, controller):
        """Set up switch."""
        self.client = client
        self.controller = controller

    async def async_update(self):
        """Synchronize state with controller."""
        await self.controller.request_update()

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

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


class UniFiPOEClientSwitch(UniFiClient, SwitchDevice, RestoreEntity):
    """Representation of a client that uses POE."""

    def __init__(self, client, controller):
        """Set up POE switch."""
        super().__init__(client, controller)
        self.poe_mode = None
        if self.client.sw_port and self.port.poe_mode != "off":
            self.poe_mode = self.port.poe_mode

    async def async_added_to_hass(self):
        """Call when entity about to be added to Home Assistant."""
        state = await self.async_get_last_state()

        if state is None:
            return

        if self.poe_mode is None:
            self.poe_mode = state.attributes["poe_mode"]

        if not self.client.sw_mac:
            self.client.raw["sw_mac"] = state.attributes["switch"]

        if not self.client.sw_port:
            self.client.raw["sw_port"] = state.attributes["port"]

    @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.

        Poe_mode None means its poe state is unknown.
        Sw_mac unavailable means restored client.
        """
        return (
            self.poe_mode is None
            or self.client.sw_mac
            and (
                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(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]


class UniFiBlockClientSwitch(UniFiClient, SwitchDevice):
    """Representation of a blockable client."""

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

    @property
    def is_on(self):
        """Return true if client is allowed to connect."""
        return not self.client.blocked

    @property
    def available(self):
        """Return if controller is available."""
        return self.controller.available

    async def async_turn_on(self, **kwargs):
        """Turn on connectivity for client."""
        await self.controller.api.clients.async_unblock(self.client.mac)

    async def async_turn_off(self, **kwargs):
        """Turn off connectivity for client."""
        await self.controller.api.clients.async_block(self.client.mac)
