"""Track devices using UniFi controllers."""
import logging
from pprint import pformat

from homeassistant.components.unifi.config_flow import get_controller_from_config_entry
from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN
from homeassistant.components.device_tracker.config_entry import ScannerEntity
from homeassistant.components.device_tracker.const import SOURCE_TYPE_ROUTER
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.entity_registry import DISABLED_CONFIG_ENTRY

import homeassistant.util.dt as dt_util

from .const import ATTR_MANUFACTURER

LOGGER = logging.getLogger(__name__)

DEVICE_ATTRIBUTES = [
    "_is_guest_by_uap",
    "ap_mac",
    "authorized",
    "essid",
    "hostname",
    "ip",
    "is_11r",
    "is_guest",
    "mac",
    "name",
    "noted",
    "oui",
    "qos_policy_applied",
    "radio",
    "radio_proto",
    "site_id",
    "vlan",
]


async def async_setup_entry(hass, config_entry, async_add_entities):
    """Set up device tracker for UniFi component."""
    controller = get_controller_from_config_entry(hass, config_entry)
    tracked = {}

    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.domain == DEVICE_TRACKER_DOMAIN
            and "-" in entity.unique_id
        ):

            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])

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

    controller.listeners.append(
        async_dispatcher_connect(hass, controller.signal_update, update_controller)
    )

    @callback
    def update_disable_on_entities():
        """Update the values of the controller."""
        for entity in tracked.values():

            if entity.entity_registry_enabled_default == entity.enabled:
                continue

            disabled_by = None
            if not entity.entity_registry_enabled_default and entity.enabled:
                disabled_by = DISABLED_CONFIG_ENTRY

            registry.async_update_entity(
                entity.registry_entry.entity_id, disabled_by=disabled_by
            )

    controller.listeners.append(
        async_dispatcher_connect(
            hass, controller.signal_options_update, update_disable_on_entities
        )
    )

    update_controller()


@callback
def update_items(controller, async_add_entities, tracked):
    """Update tracked device state from the controller."""
    new_tracked = []

    for items, tracker_class in (
        (controller.api.clients, UniFiClientTracker),
        (controller.api.devices, UniFiDeviceTracker),
    ):

        for item_id in items:

            if item_id in tracked:
                if tracked[item_id].enabled:
                    tracked[item_id].async_schedule_update_ha_state()
                continue

            tracked[item_id] = tracker_class(items[item_id], controller)
            new_tracked.append(tracked[item_id])

    if new_tracked:
        async_add_entities(new_tracked)


class UniFiClientTracker(ScannerEntity):
    """Representation of a network client."""

    def __init__(self, client, controller):
        """Set up tracked client."""
        self.client = client
        self.controller = controller
        self.is_wired = self.client.mac not in controller.wireless_clients
        self.wired_bug = None

    @property
    def entity_registry_enabled_default(self):
        """Return if the entity should be enabled when first added to the entity registry."""
        if not self.controller.option_track_clients:
            return False

        if (
            not self.is_wired
            and self.controller.option_ssid_filter
            and self.client.essid not in self.controller.option_ssid_filter
        ):
            return False

        if not self.controller.option_track_wired_clients and self.is_wired:
            return False

        return True

    async def async_added_to_hass(self):
        """Client entity created."""
        LOGGER.debug("New UniFi client tracker %s (%s)", self.name, self.client.mac)

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

        Make sure to update self.is_wired if client is wireless, there is an issue when clients go offline that they get marked as wired.
        """
        await self.controller.request_update()

        if self.is_wired and self.client.mac in self.controller.wireless_clients:
            self.is_wired = False

        LOGGER.debug(
            "Updating UniFi tracked client %s\n%s",
            self.entity_id,
            pformat(self.client.raw),
        )

    @property
    def is_connected(self):
        """Return true if the client is connected to the network.

        If is_wired and client.is_wired differ it means that the device is offline and UniFi bug shows device as wired.
        """
        if self.is_wired != self.client.is_wired:
            if not self.wired_bug:
                self.wired_bug = dt_util.utcnow()
            since_last_seen = dt_util.utcnow() - self.wired_bug

        else:
            self.wired_bug = None
            since_last_seen = dt_util.utcnow() - dt_util.utc_from_timestamp(
                float(self.client.last_seen)
            )

        if since_last_seen < self.controller.option_detection_time:
            return True

        return False

    @property
    def source_type(self):
        """Return the source type of the client."""
        return SOURCE_TYPE_ROUTER

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

    @property
    def unique_id(self) -> str:
        """Return a unique identifier for this client."""
        return f"{self.client.mac}-{self.controller.site}"

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

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

    @property
    def device_state_attributes(self):
        """Return the client state attributes."""
        attributes = {}

        for variable in DEVICE_ATTRIBUTES:
            if variable in self.client.raw:
                attributes[variable] = self.client.raw[variable]

        attributes["is_wired"] = self.is_wired

        return attributes


class UniFiDeviceTracker(ScannerEntity):
    """Representation of a network infrastructure device."""

    def __init__(self, device, controller):
        """Set up tracked device."""
        self.device = device
        self.controller = controller

    @property
    def entity_registry_enabled_default(self):
        """Return if the entity should be enabled when first added to the entity registry."""
        if self.controller.option_track_devices:
            return True
        return False

    async def async_added_to_hass(self):
        """Subscribe to device events."""
        LOGGER.debug("New UniFi device tracker %s (%s)", self.name, self.device.mac)

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

        LOGGER.debug(
            "Updating UniFi tracked device %s\n%s",
            self.entity_id,
            pformat(self.device.raw),
        )

    @property
    def is_connected(self):
        """Return true if the device is connected to the network."""
        if self.device.state == 1 and (
            dt_util.utcnow() - dt_util.utc_from_timestamp(float(self.device.last_seen))
            < self.controller.option_detection_time
        ):
            return True

        return False

    @property
    def source_type(self):
        """Return the source type of the device."""
        return SOURCE_TYPE_ROUTER

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

    @property
    def unique_id(self) -> str:
        """Return a unique identifier for this device."""
        return self.device.mac

    @property
    def available(self) -> bool:
        """Return if controller is available."""
        return not self.device.disabled and self.controller.available

    @property
    def device_info(self):
        """Return a device description for device registry."""
        info = {
            "connections": {(CONNECTION_NETWORK_MAC, self.device.mac)},
            "manufacturer": ATTR_MANUFACTURER,
            "model": self.device.model,
            "sw_version": self.device.version,
        }

        if self.device.name:
            info["name"] = self.device.name

        return info

    @property
    def device_state_attributes(self):
        """Return the device state attributes."""
        if self.device.state == 0:
            return {}

        attributes = {}

        if self.device.has_fan:
            attributes["fan_level"] = self.device.fan_level

        if self.device.overheating:
            attributes["overheating"] = self.device.overheating

        if self.device.upgradable:
            attributes["upgradable"] = self.device.upgradable

        return attributes
