"""Platform for eQ-3 climate entities."""

import logging
from typing import Any

from eq3btsmart import Thermostat
from eq3btsmart.const import EQ3BT_MAX_TEMP, EQ3BT_OFF_TEMP, Eq3Preset, OperationMode
from eq3btsmart.exceptions import Eq3Exception

from homeassistant.components.climate import (
    ATTR_HVAC_MODE,
    PRESET_NONE,
    ClimateEntity,
    ClimateEntityFeature,
    HVACAction,
    HVACMode,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_TEMPERATURE, PRECISION_HALVES, UnitOfTemperature
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH, DeviceInfo
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import slugify

from .const import (
    DEVICE_MODEL,
    DOMAIN,
    EQ_TO_HA_HVAC,
    HA_TO_EQ_HVAC,
    MANUFACTURER,
    SIGNAL_THERMOSTAT_CONNECTED,
    SIGNAL_THERMOSTAT_DISCONNECTED,
    CurrentTemperatureSelector,
    Preset,
    TargetTemperatureSelector,
)
from .entity import Eq3Entity
from .models import Eq3Config, Eq3ConfigEntryData

_LOGGER = logging.getLogger(__name__)


async def async_setup_entry(
    hass: HomeAssistant,
    config_entry: ConfigEntry,
    async_add_entities: AddEntitiesCallback,
) -> None:
    """Handle config entry setup."""

    eq3_config_entry: Eq3ConfigEntryData = hass.data[DOMAIN][config_entry.entry_id]

    async_add_entities(
        [Eq3Climate(eq3_config_entry.eq3_config, eq3_config_entry.thermostat)],
    )


class Eq3Climate(Eq3Entity, ClimateEntity):
    """Climate entity to represent a eQ-3 thermostat."""

    _attr_name = None
    _attr_supported_features = (
        ClimateEntityFeature.TARGET_TEMPERATURE
        | ClimateEntityFeature.PRESET_MODE
        | ClimateEntityFeature.TURN_OFF
        | ClimateEntityFeature.TURN_ON
    )
    _attr_temperature_unit = UnitOfTemperature.CELSIUS
    _attr_min_temp = EQ3BT_OFF_TEMP
    _attr_max_temp = EQ3BT_MAX_TEMP
    _attr_precision = PRECISION_HALVES
    _attr_hvac_modes = list(HA_TO_EQ_HVAC.keys())
    _attr_preset_modes = list(Preset)
    _attr_should_poll = False
    _attr_available = False
    _attr_hvac_mode: HVACMode | None = None
    _attr_hvac_action: HVACAction | None = None
    _attr_preset_mode: str | None = None
    _target_temperature: float | None = None

    def __init__(self, eq3_config: Eq3Config, thermostat: Thermostat) -> None:
        """Initialize the climate entity."""

        super().__init__(eq3_config, thermostat)
        self._attr_unique_id = dr.format_mac(eq3_config.mac_address)
        self._attr_device_info = DeviceInfo(
            name=slugify(self._eq3_config.mac_address),
            manufacturer=MANUFACTURER,
            model=DEVICE_MODEL,
            connections={(CONNECTION_BLUETOOTH, self._eq3_config.mac_address)},
        )

    async def async_added_to_hass(self) -> None:
        """Run when entity about to be added to hass."""

        self._thermostat.register_update_callback(self._async_on_updated)

        self.async_on_remove(
            async_dispatcher_connect(
                self.hass,
                f"{SIGNAL_THERMOSTAT_DISCONNECTED}_{self._eq3_config.mac_address}",
                self._async_on_disconnected,
            )
        )
        self.async_on_remove(
            async_dispatcher_connect(
                self.hass,
                f"{SIGNAL_THERMOSTAT_CONNECTED}_{self._eq3_config.mac_address}",
                self._async_on_connected,
            )
        )

    async def async_will_remove_from_hass(self) -> None:
        """Run when entity will be removed from hass."""

        self._thermostat.unregister_update_callback(self._async_on_updated)

    @callback
    def _async_on_disconnected(self) -> None:
        self._attr_available = False
        self.async_write_ha_state()

    @callback
    def _async_on_connected(self) -> None:
        self._attr_available = True
        self.async_write_ha_state()

    @callback
    def _async_on_updated(self) -> None:
        """Handle updated data from the thermostat."""

        if self._thermostat.status is not None:
            self._async_on_status_updated()

        if self._thermostat.device_data is not None:
            self._async_on_device_updated()

        self.async_write_ha_state()

    @callback
    def _async_on_status_updated(self) -> None:
        """Handle updated status from the thermostat."""

        self._target_temperature = self._thermostat.status.target_temperature.value
        self._attr_hvac_mode = EQ_TO_HA_HVAC[self._thermostat.status.operation_mode]
        self._attr_current_temperature = self._get_current_temperature()
        self._attr_target_temperature = self._get_target_temperature()
        self._attr_preset_mode = self._get_current_preset_mode()
        self._attr_hvac_action = self._get_current_hvac_action()

    @callback
    def _async_on_device_updated(self) -> None:
        """Handle updated device data from the thermostat."""

        device_registry = dr.async_get(self.hass)
        if device := device_registry.async_get_device(
            connections={(CONNECTION_BLUETOOTH, self._eq3_config.mac_address)},
        ):
            device_registry.async_update_device(
                device.id,
                sw_version=self._thermostat.device_data.firmware_version,
                serial_number=self._thermostat.device_data.device_serial.value,
            )

    def _get_current_temperature(self) -> float | None:
        """Return the current temperature."""

        match self._eq3_config.current_temp_selector:
            case CurrentTemperatureSelector.NOTHING:
                return None
            case CurrentTemperatureSelector.VALVE:
                if self._thermostat.status is None:
                    return None

                return float(self._thermostat.status.valve_temperature)
            case CurrentTemperatureSelector.UI:
                return self._target_temperature
            case CurrentTemperatureSelector.DEVICE:
                if self._thermostat.status is None:
                    return None

                return float(self._thermostat.status.target_temperature.value)
            case CurrentTemperatureSelector.ENTITY:
                state = self.hass.states.get(self._eq3_config.external_temp_sensor)
                if state is not None:
                    try:
                        return float(state.state)
                    except ValueError:
                        pass

        return None

    def _get_target_temperature(self) -> float | None:
        """Return the target temperature."""

        match self._eq3_config.target_temp_selector:
            case TargetTemperatureSelector.TARGET:
                return self._target_temperature
            case TargetTemperatureSelector.LAST_REPORTED:
                if self._thermostat.status is None:
                    return None

                return float(self._thermostat.status.target_temperature.value)

    def _get_current_preset_mode(self) -> str:
        """Return the current preset mode."""

        if (status := self._thermostat.status) is None:
            return PRESET_NONE
        if status.is_window_open:
            return Preset.WINDOW_OPEN
        if status.is_boost:
            return Preset.BOOST
        if status.is_low_battery:
            return Preset.LOW_BATTERY
        if status.is_away:
            return Preset.AWAY
        if status.operation_mode is OperationMode.ON:
            return Preset.OPEN
        if status.presets is None:
            return PRESET_NONE
        if status.target_temperature == status.presets.eco_temperature:
            return Preset.ECO
        if status.target_temperature == status.presets.comfort_temperature:
            return Preset.COMFORT

        return PRESET_NONE

    def _get_current_hvac_action(self) -> HVACAction:
        """Return the current hvac action."""

        if (
            self._thermostat.status is None
            or self._thermostat.status.operation_mode is OperationMode.OFF
        ):
            return HVACAction.OFF
        if self._thermostat.status.valve == 0:
            return HVACAction.IDLE
        return HVACAction.HEATING

    async def async_set_temperature(self, **kwargs: Any) -> None:
        """Set new target temperature."""

        if ATTR_HVAC_MODE in kwargs:
            mode: HVACMode | None
            if (mode := kwargs.get(ATTR_HVAC_MODE)) is None:
                return

            if mode is not HVACMode.OFF:
                await self.async_set_hvac_mode(mode)
            else:
                raise ServiceValidationError(
                    f"[{self._eq3_config.mac_address}] Can't change HVAC mode to off while changing temperature",
                )

        temperature: float | None
        if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None:
            return

        previous_temperature = self._target_temperature
        self._target_temperature = temperature

        self.async_write_ha_state()

        try:
            await self._thermostat.async_set_temperature(self._target_temperature)
        except Eq3Exception:
            _LOGGER.error(
                "[%s] Failed setting temperature", self._eq3_config.mac_address
            )
            self._target_temperature = previous_temperature
            self.async_write_ha_state()
        except ValueError as ex:
            raise ServiceValidationError("Invalid temperature") from ex

    async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
        """Set new target hvac mode."""

        if hvac_mode is HVACMode.OFF:
            await self.async_set_temperature(temperature=EQ3BT_OFF_TEMP)

        try:
            await self._thermostat.async_set_mode(HA_TO_EQ_HVAC[hvac_mode])
        except Eq3Exception:
            _LOGGER.error("[%s] Failed setting HVAC mode", self._eq3_config.mac_address)

    async def async_set_preset_mode(self, preset_mode: str) -> None:
        """Set new preset mode."""

        match preset_mode:
            case Preset.BOOST:
                await self._thermostat.async_set_boost(True)
            case Preset.AWAY:
                await self._thermostat.async_set_away(True)
            case Preset.ECO:
                await self._thermostat.async_set_preset(Eq3Preset.ECO)
            case Preset.COMFORT:
                await self._thermostat.async_set_preset(Eq3Preset.COMFORT)
            case Preset.OPEN:
                await self._thermostat.async_set_mode(OperationMode.ON)
