"""Support for Wireless Sensor Tags."""
import logging

from requests.exceptions import ConnectTimeout, HTTPError
import voluptuous as vol
from wirelesstagpy import NotificationConfig as NC

from homeassistant import util
from homeassistant.const import (
    ATTR_BATTERY_LEVEL,
    ATTR_VOLTAGE,
    CONF_PASSWORD,
    CONF_USERNAME,
)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import dispatcher_send
from homeassistant.helpers.entity import Entity

_LOGGER = logging.getLogger(__name__)


# Strength of signal in dBm
ATTR_TAG_SIGNAL_STRENGTH = "signal_strength"
# Indicates if tag is out of range or not
ATTR_TAG_OUT_OF_RANGE = "out_of_range"
# Number in percents from max power of tag receiver
ATTR_TAG_POWER_CONSUMPTION = "power_consumption"


NOTIFICATION_ID = "wirelesstag_notification"
NOTIFICATION_TITLE = "Wireless Sensor Tag Setup"

DOMAIN = "wirelesstag"
DEFAULT_ENTITY_NAMESPACE = "wirelesstag"

# Template for signal - first parameter is tag_id,
# second, tag manager mac address
SIGNAL_TAG_UPDATE = "wirelesstag.tag_info_updated_{}_{}"

# Template for signal - tag_id, sensor type and
# tag manager mac address
SIGNAL_BINARY_EVENT_UPDATE = "wirelesstag.binary_event_updated_{}_{}_{}"

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


class WirelessTagPlatform:
    """Principal object to manage all registered in HA tags."""

    def __init__(self, hass, api):
        """Designated initializer for wirelesstags platform."""
        self.hass = hass
        self.api = api
        self.tags = {}
        self._local_base_url = None

    @property
    def tag_manager_macs(self):
        """Return list of tag managers mac addresses in user account."""
        return self.api.mac_addresses

    def load_tags(self):
        """Load tags from remote server."""
        self.tags = self.api.load_tags()
        return self.tags

    def arm(self, switch):
        """Arm entity sensor monitoring."""
        func_name = f"arm_{switch.sensor_type}"
        arm_func = getattr(self.api, func_name)
        if arm_func is not None:
            arm_func(switch.tag_id, switch.tag_manager_mac)

    def disarm(self, switch):
        """Disarm entity sensor monitoring."""
        func_name = f"disarm_{switch.sensor_type}"
        disarm_func = getattr(self.api, func_name)
        if disarm_func is not None:
            disarm_func(switch.tag_id, switch.tag_manager_mac)

    def make_notifications(self, binary_sensors, mac):
        """Create configurations for push notifications."""
        _LOGGER.info("Creating configurations for push notifications.")
        configs = []

        bi_url = self.binary_event_callback_url
        for bi_sensor in binary_sensors:
            configs.extend(bi_sensor.event.build_notifications(bi_url, mac))

        update_url = self.update_callback_url

        update_config = NC.make_config_for_update_event(update_url, mac)

        configs.append(update_config)
        return configs

    def install_push_notifications(self, binary_sensors):
        """Register local push notification from tag manager."""
        _LOGGER.info("Registering local push notifications.")
        for mac in self.tag_manager_macs:
            configs = self.make_notifications(binary_sensors, mac)
            # install notifications for all tags in tag manager
            # specified by mac
            result = self.api.install_push_notification(0, configs, True, mac)
            if not result:
                self.hass.components.persistent_notification.create(
                    "Error: failed to install local push notifications <br />",
                    title="Wireless Sensor Tag Setup Local Push Notifications",
                    notification_id="wirelesstag_failed_push_notification",
                )
            else:
                _LOGGER.info(
                    "Installed push notifications for all\
                             tags in %s.",
                    mac,
                )

    @property
    def local_base_url(self):
        """Define base url of hass in local network."""
        if self._local_base_url is None:
            self._local_base_url = "http://{}".format(util.get_local_ip())

            port = self.hass.config.api.port
            if port is not None:
                self._local_base_url += f":{port}"
        return self._local_base_url

    @property
    def update_callback_url(self):
        """Return url for local push notifications(update event)."""
        return f"{self.local_base_url}/api/events/wirelesstag_update_tags"

    @property
    def binary_event_callback_url(self):
        """Return url for local push notifications(binary event)."""
        return f"{self.local_base_url}/api/events/wirelesstag_binary_event"

    def handle_update_tags_event(self, event):
        """Handle push event from wireless tag manager."""
        _LOGGER.info("push notification for update arrived: %s", event)
        try:
            tag_id = event.data.get("id")
            mac = event.data.get("mac")
            dispatcher_send(self.hass, SIGNAL_TAG_UPDATE.format(tag_id, mac), event)
        except Exception as ex:  # pylint: disable=broad-except
            _LOGGER.error(
                "Unable to handle tag update event:\
                          %s error: %s",
                str(event),
                str(ex),
            )

    def handle_binary_event(self, event):
        """Handle push notifications for binary (on/off) events."""
        _LOGGER.info("Push notification for binary event arrived: %s", event)
        try:
            tag_id = event.data.get("id")
            event_type = event.data.get("type")
            mac = event.data.get("mac")
            dispatcher_send(
                self.hass,
                SIGNAL_BINARY_EVENT_UPDATE.format(tag_id, event_type, mac),
                event,
            )
        except Exception as ex:  # pylint: disable=broad-except
            _LOGGER.error(
                "Unable to handle tag binary event:\
                          %s error: %s",
                str(event),
                str(ex),
            )


def setup(hass, config):
    """Set up the Wireless Sensor Tag component."""
    conf = config[DOMAIN]
    username = conf.get(CONF_USERNAME)
    password = conf.get(CONF_PASSWORD)

    try:
        from wirelesstagpy import WirelessTags, WirelessTagsException

        wirelesstags = WirelessTags(username=username, password=password)

        platform = WirelessTagPlatform(hass, wirelesstags)
        platform.load_tags()
        hass.data[DOMAIN] = platform
    except (ConnectTimeout, HTTPError, WirelessTagsException) as ex:
        _LOGGER.error("Unable to connect to wirelesstag.net service: %s", str(ex))
        hass.components.persistent_notification.create(
            "Error: {}<br />" "Please restart hass after fixing this." "".format(ex),
            title=NOTIFICATION_TITLE,
            notification_id=NOTIFICATION_ID,
        )
        return False

    # listen to custom events
    hass.bus.listen(
        "wirelesstag_update_tags", hass.data[DOMAIN].handle_update_tags_event
    )
    hass.bus.listen("wirelesstag_binary_event", hass.data[DOMAIN].handle_binary_event)

    return True


class WirelessTagBaseSensor(Entity):
    """Base class for HA implementation for Wireless Sensor Tag."""

    def __init__(self, api, tag):
        """Initialize a base sensor for Wireless Sensor Tag platform."""
        self._api = api
        self._tag = tag
        self._uuid = self._tag.uuid
        self.tag_id = self._tag.tag_id
        self.tag_manager_mac = self._tag.tag_manager_mac
        self._name = self._tag.name
        self._state = None

    @property
    def should_poll(self):
        """Return the polling state."""
        return True

    @property
    def name(self):
        """Return the name of the sensor."""
        return self._name

    @property
    def principal_value(self):
        """Return base value.

        Subclasses need override based on type of sensor.
        """
        return 0

    def updated_state_value(self):
        """Return formatted value.

        The default implementation formats principal value.
        """
        return self.decorate_value(self.principal_value)

    # pylint: disable=no-self-use
    def decorate_value(self, value):
        """Decorate input value to be well presented for end user."""
        return f"{value:.1f}"

    @property
    def available(self):
        """Return True if entity is available."""
        return self._tag.is_alive

    def update(self):
        """Update state."""
        if not self.should_poll:
            return

        updated_tags = self._api.load_tags()
        updated_tag = updated_tags[self._uuid]
        if updated_tag is None:
            _LOGGER.error('Unable to update tag: "%s"', self.name)
            return

        self._tag = updated_tag
        self._state = self.updated_state_value()

    @property
    def device_state_attributes(self):
        """Return the state attributes."""
        return {
            ATTR_BATTERY_LEVEL: int(self._tag.battery_remaining * 100),
            ATTR_VOLTAGE: f"{self._tag.battery_volts:.2f}V",
            ATTR_TAG_SIGNAL_STRENGTH: f"{self._tag.signal_strength}dBm",
            ATTR_TAG_OUT_OF_RANGE: not self._tag.is_in_range,
            ATTR_TAG_POWER_CONSUMPTION: f"{self._tag.power_consumption:.2f}%",
        }
