"""Support for the Netatmo Weather Service."""
from datetime import timedelta
import logging

import pyatmo

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
    ATTR_LATITUDE,
    ATTR_LONGITUDE,
    CONCENTRATION_PARTS_PER_MILLION,
    CONF_SHOW_ON_MAP,
    DEVICE_CLASS_BATTERY,
    DEVICE_CLASS_HUMIDITY,
    DEVICE_CLASS_TEMPERATURE,
    SPEED_KILOMETERS_PER_HOUR,
    TEMP_CELSIUS,
    UNIT_PERCENTAGE,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import async_entries_for_config_entry
from homeassistant.helpers.dispatcher import (
    async_dispatcher_connect,
    async_dispatcher_send,
)
from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle

from .const import (
    AUTH,
    CONF_AREA_NAME,
    CONF_LAT_NE,
    CONF_LAT_SW,
    CONF_LON_NE,
    CONF_LON_SW,
    CONF_PUBLIC_MODE,
    CONF_WEATHER_AREAS,
    DOMAIN,
    MANUFACTURER,
    MODELS,
)

_LOGGER = logging.getLogger(__name__)

# This is the Netatmo data upload interval in seconds
NETATMO_UPDATE_INTERVAL = 600

# NetAtmo Public Data is uploaded to server every 10 minutes
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=NETATMO_UPDATE_INTERVAL)

SUPPORTED_PUBLIC_SENSOR_TYPES = [
    "temperature",
    "pressure",
    "humidity",
    "rain",
    "windstrength",
    "guststrength",
    "sum_rain_1",
    "sum_rain_24",
]

SENSOR_TYPES = {
    "temperature": [
        "Temperature",
        TEMP_CELSIUS,
        "mdi:thermometer",
        DEVICE_CLASS_TEMPERATURE,
    ],
    "co2": ["CO2", CONCENTRATION_PARTS_PER_MILLION, "mdi:molecule-co2", None],
    "pressure": ["Pressure", "mbar", "mdi:gauge", None],
    "noise": ["Noise", "dB", "mdi:volume-high", None],
    "humidity": [
        "Humidity",
        UNIT_PERCENTAGE,
        "mdi:water-percent",
        DEVICE_CLASS_HUMIDITY,
    ],
    "rain": ["Rain", "mm", "mdi:weather-rainy", None],
    "sum_rain_1": ["sum_rain_1", "mm", "mdi:weather-rainy", None],
    "sum_rain_24": ["sum_rain_24", "mm", "mdi:weather-rainy", None],
    "battery_vp": ["Battery", "", "mdi:battery", None],
    "battery_lvl": ["Battery_lvl", "", "mdi:battery", None],
    "battery_percent": ["battery_percent", UNIT_PERCENTAGE, None, DEVICE_CLASS_BATTERY],
    "min_temp": ["Min Temp.", TEMP_CELSIUS, "mdi:thermometer", None],
    "max_temp": ["Max Temp.", TEMP_CELSIUS, "mdi:thermometer", None],
    "windangle": ["Angle", "", "mdi:compass", None],
    "windangle_value": ["Angle Value", "º", "mdi:compass", None],
    "windstrength": [
        "Wind Strength",
        SPEED_KILOMETERS_PER_HOUR,
        "mdi:weather-windy",
        None,
    ],
    "gustangle": ["Gust Angle", "", "mdi:compass", None],
    "gustangle_value": ["Gust Angle Value", "º", "mdi:compass", None],
    "guststrength": [
        "Gust Strength",
        SPEED_KILOMETERS_PER_HOUR,
        "mdi:weather-windy",
        None,
    ],
    "reachable": ["Reachability", "", "mdi:signal", None],
    "rf_status": ["Radio", "", "mdi:signal", None],
    "rf_status_lvl": ["Radio_lvl", "", "mdi:signal", None],
    "wifi_status": ["Wifi", "", "mdi:wifi", None],
    "wifi_status_lvl": ["Wifi_lvl", "dBm", "mdi:wifi", None],
    "health_idx": ["Health", "", "mdi:cloud", None],
}

MODULE_TYPE_OUTDOOR = "NAModule1"
MODULE_TYPE_WIND = "NAModule2"
MODULE_TYPE_RAIN = "NAModule3"
MODULE_TYPE_INDOOR = "NAModule4"


NETATMO_DEVICE_TYPES = {
    "WeatherStationData": "weather station",
    "HomeCoachData": "home coach",
}

PUBLIC = "public"


async def async_setup_entry(
    hass: HomeAssistant, entry: ConfigEntry, async_add_entities
):
    """Set up the Netatmo weather and homecoach platform."""
    auth = hass.data[DOMAIN][entry.entry_id][AUTH]
    device_registry = await hass.helpers.device_registry.async_get_registry()

    def find_entities(data):
        """Find all entities."""
        all_module_infos = data.get_module_infos()
        entities = []
        for module in all_module_infos.values():
            _LOGGER.debug("Adding module %s %s", module["module_name"], module["id"])
            for condition in data.station_data.monitoredConditions(
                moduleId=module["id"]
            ):
                entities.append(NetatmoSensor(data, module, condition.lower()))
        return entities

    def get_entities():
        """Retrieve Netatmo entities."""
        entities = []

        for data_class in [pyatmo.WeatherStationData, pyatmo.HomeCoachData]:
            try:
                dc_data = data_class(auth)
                _LOGGER.debug("%s detected!", NETATMO_DEVICE_TYPES[data_class.__name__])
                data = NetatmoData(auth, dc_data)
            except pyatmo.NoDevice:
                _LOGGER.debug(
                    "No %s entities found", NETATMO_DEVICE_TYPES[data_class.__name__]
                )
                continue

            entities.extend(find_entities(data))

        return entities

    async_add_entities(await hass.async_add_executor_job(get_entities), True)

    @callback
    def add_public_entities():
        """Retrieve Netatmo public weather entities."""
        entities = []
        for area in entry.options.get(CONF_WEATHER_AREAS, {}).values():
            data = NetatmoPublicData(
                auth,
                lat_ne=area[CONF_LAT_NE],
                lon_ne=area[CONF_LON_NE],
                lat_sw=area[CONF_LAT_SW],
                lon_sw=area[CONF_LON_SW],
            )
            for sensor_type in SUPPORTED_PUBLIC_SENSOR_TYPES:
                entities.append(NetatmoPublicSensor(area, data, sensor_type,))

        for device in async_entries_for_config_entry(device_registry, entry.entry_id):
            if device.model == "Public Weather stations":
                device_registry.async_remove_device(device.id)

        if entities:
            async_add_entities(entities)

    async_dispatcher_connect(
        hass, f"signal-{DOMAIN}-public-update-{entry.entry_id}", add_public_entities
    )

    entry.add_update_listener(async_config_entry_updated)

    add_public_entities()


async def async_config_entry_updated(hass: HomeAssistant, entry: ConfigEntry) -> None:
    """Handle signals of config entry being updated."""
    async_dispatcher_send(hass, f"signal-{DOMAIN}-public-update-{entry.entry_id}")


async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
    """Set up the Netatmo weather and homecoach platform."""
    return


class NetatmoSensor(Entity):
    """Implementation of a Netatmo sensor."""

    def __init__(self, netatmo_data, module_info, sensor_type):
        """Initialize the sensor."""
        self.netatmo_data = netatmo_data

        device = self.netatmo_data.station_data.moduleById(mid=module_info["id"])
        if not device:
            # Assume it's a station if module can't be found
            device = self.netatmo_data.station_data.stationById(sid=module_info["id"])

        if device["type"] == "NHC":
            self.module_name = module_info["station_name"]
        else:
            self.module_name = (
                f"{module_info['station_name']} {module_info['module_name']}"
            )

        self._name = f"{MANUFACTURER} {self.module_name} {SENSOR_TYPES[sensor_type][0]}"
        self.type = sensor_type
        self._state = None
        self._device_class = SENSOR_TYPES[self.type][3]
        self._icon = SENSOR_TYPES[self.type][2]
        self._unit_of_measurement = SENSOR_TYPES[self.type][1]
        self._module_type = device["type"]
        self._module_id = module_info["id"]
        self._unique_id = f"{self._module_id}-{self.type}"

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

    @property
    def icon(self):
        """Icon to use in the frontend, if any."""
        return self._icon

    @property
    def device_class(self):
        """Return the device class of the sensor."""
        return self._device_class

    @property
    def device_info(self):
        """Return the device info for the sensor."""
        return {
            "identifiers": {(DOMAIN, self._module_id)},
            "name": self.module_name,
            "manufacturer": MANUFACTURER,
            "model": MODELS[self._module_type],
        }

    @property
    def state(self):
        """Return the state of the device."""
        return self._state

    @property
    def unit_of_measurement(self):
        """Return the unit of measurement of this entity, if any."""
        return self._unit_of_measurement

    @property
    def unique_id(self):
        """Return the unique ID for this sensor."""
        return self._unique_id

    @property
    def available(self):
        """Return True if entity is available."""
        return self._state is not None

    def update(self):
        """Get the latest data from Netatmo API and updates the states."""
        self.netatmo_data.update()
        if self.netatmo_data.data is None:
            if self._state is None:
                return
            _LOGGER.warning("No data from update")
            self._state = None
            return

        data = self.netatmo_data.data.get(self._module_id)

        if data is None:
            if self._state:
                _LOGGER.debug(
                    "No data found for %s (%s)", self.module_name, self._module_id
                )
                _LOGGER.debug("data: %s", self.netatmo_data.data)
            self._state = None
            return

        try:
            if self.type == "temperature":
                self._state = round(data["Temperature"], 1)
            elif self.type == "humidity":
                self._state = data["Humidity"]
            elif self.type == "rain":
                self._state = data["Rain"]
            elif self.type == "sum_rain_1":
                self._state = round(data["sum_rain_1"], 1)
            elif self.type == "sum_rain_24":
                self._state = data["sum_rain_24"]
            elif self.type == "noise":
                self._state = data["Noise"]
            elif self.type == "co2":
                self._state = data["CO2"]
            elif self.type == "pressure":
                self._state = round(data["Pressure"], 1)
            elif self.type == "battery_percent":
                self._state = data["battery_percent"]
            elif self.type == "battery_lvl":
                self._state = data["battery_vp"]
            elif self.type == "battery_vp" and self._module_type == MODULE_TYPE_WIND:
                if data["battery_vp"] >= 5590:
                    self._state = "Full"
                elif data["battery_vp"] >= 5180:
                    self._state = "High"
                elif data["battery_vp"] >= 4770:
                    self._state = "Medium"
                elif data["battery_vp"] >= 4360:
                    self._state = "Low"
                elif data["battery_vp"] < 4360:
                    self._state = "Very Low"
            elif self.type == "battery_vp" and self._module_type == MODULE_TYPE_RAIN:
                if data["battery_vp"] >= 5500:
                    self._state = "Full"
                elif data["battery_vp"] >= 5000:
                    self._state = "High"
                elif data["battery_vp"] >= 4500:
                    self._state = "Medium"
                elif data["battery_vp"] >= 4000:
                    self._state = "Low"
                elif data["battery_vp"] < 4000:
                    self._state = "Very Low"
            elif self.type == "battery_vp" and self._module_type == MODULE_TYPE_INDOOR:
                if data["battery_vp"] >= 5640:
                    self._state = "Full"
                elif data["battery_vp"] >= 5280:
                    self._state = "High"
                elif data["battery_vp"] >= 4920:
                    self._state = "Medium"
                elif data["battery_vp"] >= 4560:
                    self._state = "Low"
                elif data["battery_vp"] < 4560:
                    self._state = "Very Low"
            elif self.type == "battery_vp" and self._module_type == MODULE_TYPE_OUTDOOR:
                if data["battery_vp"] >= 5500:
                    self._state = "Full"
                elif data["battery_vp"] >= 5000:
                    self._state = "High"
                elif data["battery_vp"] >= 4500:
                    self._state = "Medium"
                elif data["battery_vp"] >= 4000:
                    self._state = "Low"
                elif data["battery_vp"] < 4000:
                    self._state = "Very Low"
            elif self.type == "min_temp":
                self._state = data["min_temp"]
            elif self.type == "max_temp":
                self._state = data["max_temp"]
            elif self.type == "windangle_value":
                self._state = data["WindAngle"]
            elif self.type == "windangle":
                if data["WindAngle"] >= 330:
                    self._state = "N (%d\xb0)" % data["WindAngle"]
                elif data["WindAngle"] >= 300:
                    self._state = "NW (%d\xb0)" % data["WindAngle"]
                elif data["WindAngle"] >= 240:
                    self._state = "W (%d\xb0)" % data["WindAngle"]
                elif data["WindAngle"] >= 210:
                    self._state = "SW (%d\xb0)" % data["WindAngle"]
                elif data["WindAngle"] >= 150:
                    self._state = "S (%d\xb0)" % data["WindAngle"]
                elif data["WindAngle"] >= 120:
                    self._state = "SE (%d\xb0)" % data["WindAngle"]
                elif data["WindAngle"] >= 60:
                    self._state = "E (%d\xb0)" % data["WindAngle"]
                elif data["WindAngle"] >= 30:
                    self._state = "NE (%d\xb0)" % data["WindAngle"]
                elif data["WindAngle"] >= 0:
                    self._state = "N (%d\xb0)" % data["WindAngle"]
            elif self.type == "windstrength":
                self._state = data["WindStrength"]
            elif self.type == "gustangle_value":
                self._state = data["GustAngle"]
            elif self.type == "gustangle":
                if data["GustAngle"] >= 330:
                    self._state = "N (%d\xb0)" % data["GustAngle"]
                elif data["GustAngle"] >= 300:
                    self._state = "NW (%d\xb0)" % data["GustAngle"]
                elif data["GustAngle"] >= 240:
                    self._state = "W (%d\xb0)" % data["GustAngle"]
                elif data["GustAngle"] >= 210:
                    self._state = "SW (%d\xb0)" % data["GustAngle"]
                elif data["GustAngle"] >= 150:
                    self._state = "S (%d\xb0)" % data["GustAngle"]
                elif data["GustAngle"] >= 120:
                    self._state = "SE (%d\xb0)" % data["GustAngle"]
                elif data["GustAngle"] >= 60:
                    self._state = "E (%d\xb0)" % data["GustAngle"]
                elif data["GustAngle"] >= 30:
                    self._state = "NE (%d\xb0)" % data["GustAngle"]
                elif data["GustAngle"] >= 0:
                    self._state = "N (%d\xb0)" % data["GustAngle"]
            elif self.type == "guststrength":
                self._state = data["GustStrength"]
            elif self.type == "reachable":
                self._state = data["reachable"]
            elif self.type == "rf_status_lvl":
                self._state = data["rf_status"]
            elif self.type == "rf_status":
                if data["rf_status"] >= 90:
                    self._state = "Low"
                elif data["rf_status"] >= 76:
                    self._state = "Medium"
                elif data["rf_status"] >= 60:
                    self._state = "High"
                elif data["rf_status"] <= 59:
                    self._state = "Full"
            elif self.type == "wifi_status_lvl":
                self._state = data["wifi_status"]
            elif self.type == "wifi_status":
                if data["wifi_status"] >= 86:
                    self._state = "Low"
                elif data["wifi_status"] >= 71:
                    self._state = "Medium"
                elif data["wifi_status"] >= 56:
                    self._state = "High"
                elif data["wifi_status"] <= 55:
                    self._state = "Full"
            elif self.type == "health_idx":
                if data["health_idx"] == 0:
                    self._state = "Healthy"
                elif data["health_idx"] == 1:
                    self._state = "Fine"
                elif data["health_idx"] == 2:
                    self._state = "Fair"
                elif data["health_idx"] == 3:
                    self._state = "Poor"
                elif data["health_idx"] == 4:
                    self._state = "Unhealthy"
        except KeyError:
            if self._state:
                _LOGGER.info("No %s data found for %s", self.type, self.module_name)
            self._state = None
            return


class NetatmoData:
    """Get the latest data from Netatmo."""

    def __init__(self, auth, station_data):
        """Initialize the data object."""
        self.data = {}
        self.station_data = station_data
        self.auth = auth

    def get_module_infos(self):
        """Return all modules available on the API as a dict."""
        return self.station_data.getModules()

    @Throttle(MIN_TIME_BETWEEN_UPDATES)
    def update(self):
        """Call the Netatmo API to update the data."""
        self.station_data = self.station_data.__class__(self.auth)

        data = self.station_data.lastData(exclude=3600, byId=True)
        if not data:
            _LOGGER.debug("No data received when updating station data")
            return
        self.data = data


class NetatmoPublicSensor(Entity):
    """Represent a single sensor in a Netatmo."""

    def __init__(self, area, data, sensor_type):
        """Initialize the sensor."""
        self.netatmo_data = data
        self.type = sensor_type
        self._mode = area[CONF_PUBLIC_MODE]
        self._area_name = area[CONF_AREA_NAME]
        self._name = f"{MANUFACTURER} {self._area_name} {SENSOR_TYPES[self.type][0]}"
        self._state = None
        self._device_class = SENSOR_TYPES[self.type][3]
        self._icon = SENSOR_TYPES[self.type][2]
        self._unit_of_measurement = SENSOR_TYPES[self.type][1]
        self._show_on_map = area[CONF_SHOW_ON_MAP]
        self._unique_id = f"{self._name.replace(' ', '-')}"
        self._module_type = PUBLIC

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

    @property
    def icon(self):
        """Icon to use in the frontend."""
        return self._icon

    @property
    def device_class(self):
        """Return the device class of the sensor."""
        return self._device_class

    @property
    def device_info(self):
        """Return the device info for the sensor."""
        return {
            "identifiers": {(DOMAIN, self._area_name)},
            "name": self._area_name,
            "manufacturer": MANUFACTURER,
            "model": MODELS[self._module_type],
        }

    @property
    def device_state_attributes(self):
        """Return the attributes of the device."""
        attrs = {}

        if self._show_on_map:
            attrs[ATTR_LATITUDE] = (
                self.netatmo_data.lat_ne + self.netatmo_data.lat_sw
            ) / 2
            attrs[ATTR_LONGITUDE] = (
                self.netatmo_data.lon_ne + self.netatmo_data.lon_sw
            ) / 2

        return attrs

    @property
    def state(self):
        """Return the state of the device."""
        return self._state

    @property
    def unit_of_measurement(self):
        """Return the unit of measurement of this entity."""
        return self._unit_of_measurement

    @property
    def unique_id(self):
        """Return the unique ID for this sensor."""
        return self._unique_id

    @property
    def available(self):
        """Return True if entity is available."""
        return self._state is not None

    def update(self):
        """Get the latest data from Netatmo API and updates the states."""
        self.netatmo_data.update()

        if self.netatmo_data.data is None:
            _LOGGER.info("No data found for %s", self._name)
            self._state = None
            return

        data = None

        if self.type == "temperature":
            data = self.netatmo_data.data.getLatestTemperatures()
        elif self.type == "pressure":
            data = self.netatmo_data.data.getLatestPressures()
        elif self.type == "humidity":
            data = self.netatmo_data.data.getLatestHumidities()
        elif self.type == "rain":
            data = self.netatmo_data.data.getLatestRain()
        elif self.type == "sum_rain_1":
            data = self.netatmo_data.data.get60minRain()
        elif self.type == "sum_rain_24":
            data = self.netatmo_data.data.get24hRain()
        elif self.type == "windstrength":
            data = self.netatmo_data.data.getLatestWindStrengths()
        elif self.type == "guststrength":
            data = self.netatmo_data.data.getLatestGustStrengths()

        if not data:
            _LOGGER.warning(
                "No station provides %s data in the area %s", self.type, self._area_name
            )
            self._state = None
            return

        values = [x for x in data.values() if x is not None]
        if self._mode == "avg":
            self._state = round(sum(values) / len(values), 1)
        elif self._mode == "max":
            self._state = max(values)


class NetatmoPublicData:
    """Get the latest data from Netatmo."""

    def __init__(self, auth, lat_ne, lon_ne, lat_sw, lon_sw):
        """Initialize the data object."""
        self.auth = auth
        self.data = None
        self.lat_ne = lat_ne
        self.lon_ne = lon_ne
        self.lat_sw = lat_sw
        self.lon_sw = lon_sw

    @Throttle(MIN_TIME_BETWEEN_UPDATES)
    def update(self):
        """Request an update from the Netatmo API."""
        try:
            data = pyatmo.PublicData(
                self.auth,
                LAT_NE=self.lat_ne,
                LON_NE=self.lon_ne,
                LAT_SW=self.lat_sw,
                LON_SW=self.lon_sw,
                filtering=True,
            )
        except pyatmo.NoDevice:
            data = None

        if not data:
            _LOGGER.debug("No data received when updating public station data")
            return

        if data.CountStationInArea() == 0:
            _LOGGER.warning("No Stations available in this area")
            return

        self.data = data
