"""Python Control of Nobø Hub - Nobø Energy Control."""

from typing import Any, override

from pynobo import PynoboError, nobo

from homeassistant.components.climate import (
    ATTR_TARGET_TEMP_HIGH,
    ATTR_TARGET_TEMP_LOW,
    PRESET_AWAY,
    PRESET_COMFORT,
    PRESET_ECO,
    PRESET_NONE,
    ClimateEntity,
    ClimateEntityFeature,
    HVACMode,
)
from homeassistant.const import (
    ATTR_NAME,
    PRECISION_TENTHS,
    PRECISION_WHOLE,
    UnitOfTemperature,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util import dt as dt_util

from . import NoboHubConfigEntry
from .const import (
    ATTR_TEMP_COMFORT_C,
    ATTR_TEMP_ECO_C,
    CONF_OVERRIDE_TYPE,
    DOMAIN,
    OVERRIDE_TYPE_NOW,
)
from .entity import NoboBaseEntity

PARALLEL_UPDATES = 0

SUPPORT_FLAGS = (
    ClimateEntityFeature.PRESET_MODE | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
)

PRESET_MODES = [PRESET_NONE, PRESET_COMFORT, PRESET_ECO, PRESET_AWAY]

MIN_TEMPERATURE = 7
MAX_TEMPERATURE = 30


async def async_setup_entry(
    hass: HomeAssistant,
    config_entry: NoboHubConfigEntry,
    async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
    """Set up the Nobø Ecohub platform from UI configuration."""
    hub = config_entry.runtime_data

    override_type = (
        nobo.API.OVERRIDE_TYPE_NOW
        if config_entry.options.get(CONF_OVERRIDE_TYPE) == OVERRIDE_TYPE_NOW
        else nobo.API.OVERRIDE_TYPE_CONSTANT
    )

    known_zones: set[str] = set()

    @callback
    def _add_zones(_hub: nobo) -> None:
        """Add climate entities for zones added to the hub."""
        if hub.connected:
            # Forget zones no longer on the hub so a removed-then-re-added zone
            # (the hub reuses zone ids) is detected as new again. Skip while
            # disconnected: a stale/empty snapshot would drop live zones and
            # cause duplicate re-adds on reconnect.
            known_zones.intersection_update(hub.zones)
        new_zones = [zone_id for zone_id in hub.zones if zone_id not in known_zones]
        known_zones.update(new_zones)
        async_add_entities(
            NoboZone(hass, zone_id, hub, override_type, config_entry.entry_id)
            for zone_id in new_zones
        )

    _add_zones(hub)
    hub.register_callback(_add_zones)
    config_entry.async_on_unload(lambda: hub.deregister_callback(_add_zones))


class NoboZone(NoboBaseEntity, ClimateEntity):
    """Representation of a Nobø zone.

    A Nobø zone consists of a group of physical devices that are
    controlled as a unity.
    """

    _attr_name = None
    _attr_max_temp = MAX_TEMPERATURE
    _attr_min_temp = MIN_TEMPERATURE
    _attr_precision = PRECISION_TENTHS
    _attr_hvac_modes = [HVACMode.HEAT, HVACMode.AUTO]
    _attr_hvac_mode = HVACMode.AUTO
    _attr_preset_modes = PRESET_MODES
    _attr_supported_features = SUPPORT_FLAGS
    _attr_temperature_unit = UnitOfTemperature.CELSIUS
    _attr_target_temperature_step = PRECISION_WHOLE
    # Need to poll to get preset change when in HVACMode.AUTO
    _attr_should_poll = True

    def __init__(
        self,
        hass: HomeAssistant,
        zone_id: str,
        hub: nobo,
        override_type: str,
        entry_id: str,
    ) -> None:
        """Initialize the climate device."""
        super().__init__(hass, hub, entry_id)
        self._id = zone_id
        self._attr_unique_id = f"{hub.hub_serial}:{zone_id}"
        self._override_type = override_type
        zone_name = hub.zones[zone_id][ATTR_NAME]
        self._attr_device_info = DeviceInfo(
            identifiers={(DOMAIN, self._attr_unique_id)},
            name=zone_name,
            via_device_id=self._hub_device_id,
            suggested_area=zone_name,
        )
        self._read_state()

    @override
    async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
        """Set new target HVAC mode."""
        preset = PRESET_COMFORT if hvac_mode == HVACMode.HEAT else PRESET_NONE
        await self._apply_preset(preset, "set_hvac_mode_failed")

    @override
    async def async_set_preset_mode(self, preset_mode: str) -> None:
        """Set new zone override."""
        await self._apply_preset(preset_mode, "set_preset_mode_failed")

    async def _apply_preset(
        self,
        preset_mode: str,
        translation_key: str,
    ) -> None:
        if preset_mode == PRESET_ECO:
            mode = nobo.API.OVERRIDE_MODE_ECO
        elif preset_mode == PRESET_AWAY:
            mode = nobo.API.OVERRIDE_MODE_AWAY
        elif preset_mode == PRESET_COMFORT:
            mode = nobo.API.OVERRIDE_MODE_COMFORT
        else:  # PRESET_NONE
            mode = nobo.API.OVERRIDE_MODE_NORMAL
        try:
            await self._nobo.async_create_override(
                mode,
                self._override_type,
                nobo.API.OVERRIDE_TARGET_ZONE,
                self._id,
            )
        except PynoboError as err:
            raise HomeAssistantError(
                translation_domain=DOMAIN,
                translation_key=translation_key,
            ) from err

    @override
    async def async_set_temperature(self, **kwargs: Any) -> None:
        """Set new target temperature."""
        if ATTR_TARGET_TEMP_LOW in kwargs:
            low = round(kwargs[ATTR_TARGET_TEMP_LOW])
            high = round(kwargs[ATTR_TARGET_TEMP_HIGH])
            try:
                await self._nobo.async_update_zone(
                    self._id, temp_comfort_c=high, temp_eco_c=low
                )
            except PynoboError as err:
                raise HomeAssistantError(
                    translation_domain=DOMAIN,
                    translation_key="set_temperature_failed",
                ) from err

    async def async_update(self) -> None:
        """Fetch new state data for this zone."""
        self._read_state()

    @property
    @override
    def available(self) -> bool:
        """Available when the hub is connected and the zone still exists."""
        return super().available and self._id in self._nobo.zones

    @callback
    @override
    def _read_state(self) -> None:
        """Read the current state from the hub. These are only local calls."""
        if not self.available:
            return
        state = self._nobo.get_current_zone_mode(self._id, dt_util.now())
        self._attr_hvac_mode = HVACMode.AUTO
        self._attr_preset_mode = PRESET_NONE

        if state == nobo.API.NAME_OFF:
            self._attr_hvac_mode = HVACMode.OFF
        elif state == nobo.API.NAME_AWAY:
            self._attr_preset_mode = PRESET_AWAY
        elif state == nobo.API.NAME_ECO:
            self._attr_preset_mode = PRESET_ECO
        elif state == nobo.API.NAME_COMFORT:
            self._attr_preset_mode = PRESET_COMFORT

        if self._nobo.get_zone_override_mode(self._id) != nobo.API.NAME_NORMAL:
            self._attr_hvac_mode = HVACMode.HEAT

        current_temperature = self._nobo.get_current_zone_temperature(self._id)
        self._attr_current_temperature = (
            None if current_temperature is None else float(current_temperature)
        )
        self._attr_target_temperature_high = int(
            self._nobo.zones[self._id][ATTR_TEMP_COMFORT_C]
        )
        self._attr_target_temperature_low = int(
            self._nobo.zones[self._id][ATTR_TEMP_ECO_C]
        )
