"""Support for the Escea Fireplace."""
from __future__ import annotations

from collections.abc import Coroutine
import logging
from typing import Any

from pescea import Controller

from homeassistant.components.climate import ClimateEntity
from homeassistant.components.climate.const import (
    FAN_AUTO,
    FAN_HIGH,
    FAN_LOW,
    ClimateEntityFeature,
    HVACMode,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, TEMP_CELSIUS
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from .const import (
    DATA_DISCOVERY_SERVICE,
    DISPATCH_CONTROLLER_DISCONNECTED,
    DISPATCH_CONTROLLER_DISCOVERED,
    DISPATCH_CONTROLLER_RECONNECTED,
    DISPATCH_CONTROLLER_UPDATE,
    DOMAIN,
    ESCEA_FIREPLACE,
    ESCEA_MANUFACTURER,
    ICON,
)

_LOGGER = logging.getLogger(__name__)

_ESCEA_FAN_TO_HA = {
    Controller.Fan.FLAME_EFFECT: FAN_LOW,
    Controller.Fan.FAN_BOOST: FAN_HIGH,
    Controller.Fan.AUTO: FAN_AUTO,
}
_HA_FAN_TO_ESCEA = {v: k for k, v in _ESCEA_FAN_TO_HA.items()}


async def async_setup_entry(
    hass: HomeAssistant,
    config_entry: ConfigEntry,
    async_add_entities: AddEntitiesCallback,
) -> None:
    """Initialize an Escea Controller."""
    discovery_service = hass.data[DATA_DISCOVERY_SERVICE]

    @callback
    def init_controller(ctrl: Controller) -> None:
        """Register the controller device."""

        _LOGGER.debug("Controller UID=%s discovered", ctrl.device_uid)

        entity = ControllerEntity(ctrl)
        async_add_entities([entity])

    # create any components not yet created
    for controller in discovery_service.controllers.values():
        init_controller(controller)

    # connect to register any further components
    config_entry.async_on_unload(
        async_dispatcher_connect(hass, DISPATCH_CONTROLLER_DISCOVERED, init_controller)
    )


class ControllerEntity(ClimateEntity):
    """Representation of Escea Controller."""

    _attr_fan_modes = list(_HA_FAN_TO_ESCEA)
    _attr_has_entity_name = True
    _attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF]
    _attr_icon = ICON
    _attr_precision = PRECISION_WHOLE
    _attr_should_poll = False
    _attr_supported_features = (
        ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE
    )
    _attr_target_temperature_step = PRECISION_WHOLE
    _attr_temperature_unit = TEMP_CELSIUS

    def __init__(self, controller: Controller) -> None:
        """Initialise ControllerDevice."""
        self._controller = controller

        self._attr_min_temp = controller.min_temp
        self._attr_max_temp = controller.max_temp

        self._attr_unique_id = controller.device_uid

        # temporary assignment to get past mypy checker
        unique_id: str = controller.device_uid

        self._attr_device_info = DeviceInfo(
            identifiers={(DOMAIN, unique_id)},
            manufacturer=ESCEA_MANUFACTURER,
            name=ESCEA_FIREPLACE,
        )

        self._attr_available = True

    async def async_added_to_hass(self) -> None:
        """Call on adding to hass.

        Registers for connect/disconnect/update events
        """

        @callback
        def controller_disconnected(ctrl: Controller, ex: Exception) -> None:
            """Disconnected from controller."""
            if ctrl is not self._controller:
                return
            self.set_available(False, ex)

        self.async_on_remove(
            async_dispatcher_connect(
                self.hass, DISPATCH_CONTROLLER_DISCONNECTED, controller_disconnected
            )
        )

        @callback
        def controller_reconnected(ctrl: Controller) -> None:
            """Reconnected to controller."""
            if ctrl is not self._controller:
                return
            self.set_available(True)

        self.async_on_remove(
            async_dispatcher_connect(
                self.hass, DISPATCH_CONTROLLER_RECONNECTED, controller_reconnected
            )
        )

        @callback
        def controller_update(ctrl: Controller) -> None:
            """Handle controller data updates."""
            if ctrl is not self._controller:
                return
            self.async_write_ha_state()

        self.async_on_remove(
            async_dispatcher_connect(
                self.hass, DISPATCH_CONTROLLER_UPDATE, controller_update
            )
        )

    @callback
    def set_available(self, available: bool, ex: Exception | None = None) -> None:
        """Set availability for the controller."""
        if self._attr_available == available:
            return

        if available:
            _LOGGER.debug("Reconnected controller %s ", self._controller.device_uid)
        else:
            _LOGGER.debug(
                "Controller %s disconnected due to exception: %s",
                self._controller.device_uid,
                ex,
            )

        self._attr_available = available
        self.async_write_ha_state()

    @property
    def hvac_mode(self) -> HVACMode:
        """Return current operation ie. heat, cool, idle."""
        return HVACMode.HEAT if self._controller.is_on else HVACMode.OFF

    @property
    def current_temperature(self) -> float | None:
        """Return the current temperature."""
        return self._controller.current_temp

    @property
    def target_temperature(self) -> float | None:
        """Return the temperature we try to reach."""
        return self._controller.desired_temp

    @property
    def fan_mode(self) -> str | None:
        """Return the fan setting."""
        return _ESCEA_FAN_TO_HA[self._controller.fan]

    async def wrap_and_catch(self, coro: Coroutine) -> None:
        """Catch any connection errors and set unavailable."""
        try:
            await coro
        except ConnectionError as ex:
            self.set_available(False, ex)
        else:
            self.set_available(True)

    async def async_set_temperature(self, **kwargs: Any) -> None:
        """Set new target temperature."""
        temp = kwargs.get(ATTR_TEMPERATURE)
        if temp is not None:
            await self.wrap_and_catch(self._controller.set_desired_temp(temp))

    async def async_set_fan_mode(self, fan_mode: str) -> None:
        """Set new target fan mode."""
        await self.wrap_and_catch(self._controller.set_fan(_HA_FAN_TO_ESCEA[fan_mode]))

    async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
        """Set new target operation mode."""
        await self.wrap_and_catch(self._controller.set_on(hvac_mode == HVACMode.HEAT))

    async def async_turn_on(self) -> None:
        """Turn the entity on."""
        await self.wrap_and_catch(self._controller.set_on(True))

    async def async_turn_off(self) -> None:
        """Turn the entity off."""
        await self.wrap_and_catch(self._controller.set_on(False))