"""Support for Vallox ventilation units."""
from __future__ import annotations

from datetime import datetime
import ipaddress
import logging
from typing import Any

from vallox_websocket_api import PROFILE as VALLOX_PROFILE, Vallox
from vallox_websocket_api.constants import vlxDevConstants
from vallox_websocket_api.exceptions import ValloxApiException
import voluptuous as vol

from homeassistant.const import CONF_HOST, CONF_NAME
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.discovery import async_load_platform
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.typing import ConfigType, StateType

from .const import (
    DEFAULT_FAN_SPEED_AWAY,
    DEFAULT_FAN_SPEED_BOOST,
    DEFAULT_FAN_SPEED_HOME,
    DEFAULT_NAME,
    DOMAIN,
    METRIC_KEY_PROFILE_FAN_SPEED_AWAY,
    METRIC_KEY_PROFILE_FAN_SPEED_BOOST,
    METRIC_KEY_PROFILE_FAN_SPEED_HOME,
    SIGNAL_VALLOX_STATE_UPDATE,
    STATE_PROXY_SCAN_INTERVAL,
)

_LOGGER = logging.getLogger(__name__)

CONFIG_SCHEMA = vol.Schema(
    {
        DOMAIN: vol.Schema(
            {
                vol.Required(CONF_HOST): vol.All(ipaddress.ip_address, cv.string),
                vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
            }
        )
    },
    extra=vol.ALLOW_EXTRA,
)

PROFILE_TO_STR_SETTABLE = {
    VALLOX_PROFILE.HOME: "Home",
    VALLOX_PROFILE.AWAY: "Away",
    VALLOX_PROFILE.BOOST: "Boost",
    VALLOX_PROFILE.FIREPLACE: "Fireplace",
}

STR_TO_PROFILE = {v: k for (k, v) in PROFILE_TO_STR_SETTABLE.items()}

PROFILE_TO_STR_REPORTABLE = {
    **{VALLOX_PROFILE.NONE: "None", VALLOX_PROFILE.EXTRA: "Extra"},
    **PROFILE_TO_STR_SETTABLE,
}

ATTR_PROFILE = "profile"
ATTR_PROFILE_FAN_SPEED = "fan_speed"

SERVICE_SCHEMA_SET_PROFILE = vol.Schema(
    {vol.Required(ATTR_PROFILE): vol.All(cv.string, vol.In(STR_TO_PROFILE))}
)

SERVICE_SCHEMA_SET_PROFILE_FAN_SPEED = vol.Schema(
    {
        vol.Required(ATTR_PROFILE_FAN_SPEED): vol.All(
            vol.Coerce(int), vol.Clamp(min=0, max=100)
        )
    }
)

SERVICE_SET_PROFILE = "set_profile"
SERVICE_SET_PROFILE_FAN_SPEED_HOME = "set_profile_fan_speed_home"
SERVICE_SET_PROFILE_FAN_SPEED_AWAY = "set_profile_fan_speed_away"
SERVICE_SET_PROFILE_FAN_SPEED_BOOST = "set_profile_fan_speed_boost"

SERVICE_TO_METHOD = {
    SERVICE_SET_PROFILE: {
        "method": "async_set_profile",
        "schema": SERVICE_SCHEMA_SET_PROFILE,
    },
    SERVICE_SET_PROFILE_FAN_SPEED_HOME: {
        "method": "async_set_profile_fan_speed_home",
        "schema": SERVICE_SCHEMA_SET_PROFILE_FAN_SPEED,
    },
    SERVICE_SET_PROFILE_FAN_SPEED_AWAY: {
        "method": "async_set_profile_fan_speed_away",
        "schema": SERVICE_SCHEMA_SET_PROFILE_FAN_SPEED,
    },
    SERVICE_SET_PROFILE_FAN_SPEED_BOOST: {
        "method": "async_set_profile_fan_speed_boost",
        "schema": SERVICE_SCHEMA_SET_PROFILE_FAN_SPEED,
    },
}


async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
    """Set up the client and boot the platforms."""
    conf = config[DOMAIN]
    host = conf.get(CONF_HOST)
    name = conf.get(CONF_NAME)

    client = Vallox(host)
    state_proxy = ValloxStateProxy(hass, client)
    service_handler = ValloxServiceHandler(client, state_proxy)

    hass.data[DOMAIN] = {"client": client, "state_proxy": state_proxy, "name": name}

    for vallox_service, method in SERVICE_TO_METHOD.items():
        schema = method["schema"]
        hass.services.async_register(
            DOMAIN, vallox_service, service_handler.async_handle, schema=schema
        )

    # The vallox hardware expects quite strict timings for websocket requests. Timings that machines
    # with less processing power, like Raspberries, cannot live up to during the busy start phase of
    # Home Asssistant. Hence, async_add_entities() for fan and sensor in respective code will be
    # called with update_before_add=False to intentionally delay the first request, increasing
    # chance that it is issued only when the machine is less busy again.
    hass.async_create_task(async_load_platform(hass, "sensor", DOMAIN, {}, config))
    hass.async_create_task(async_load_platform(hass, "fan", DOMAIN, {}, config))

    async_track_time_interval(hass, state_proxy.async_update, STATE_PROXY_SCAN_INTERVAL)

    return True


class ValloxStateProxy:
    """Helper class to reduce websocket API calls."""

    def __init__(self, hass: HomeAssistant, client: Vallox) -> None:
        """Initialize the proxy."""
        self._hass = hass
        self._client = client
        self._metric_cache: dict[str, Any] = {}
        self._profile = VALLOX_PROFILE.NONE
        self._valid = False

    def fetch_metric(self, metric_key: str) -> StateType:
        """Return cached state value."""
        _LOGGER.debug("Fetching metric key: %s", metric_key)

        if not self._valid:
            raise OSError("Device state out of sync.")

        if metric_key not in vlxDevConstants.__dict__:
            raise KeyError(f"Unknown metric key: {metric_key}")

        value = self._metric_cache[metric_key]
        if value is None:
            return None

        if not isinstance(value, (str, int, float)):
            raise TypeError(
                f"Return value of metric {metric_key} has unexpected type {type(value)}"
            )

        return value

    def get_profile(self) -> str:
        """Return cached profile value."""
        _LOGGER.debug("Returning profile")

        if not self._valid:
            raise OSError("Device state out of sync.")

        return PROFILE_TO_STR_REPORTABLE[self._profile]

    async def async_update(self, time: datetime | None = None) -> None:
        """Fetch state update."""
        _LOGGER.debug("Updating Vallox state cache")

        try:
            self._metric_cache = await self._client.fetch_metrics()
            self._profile = await self._client.get_profile()

        except (OSError, ValloxApiException) as err:
            self._valid = False
            _LOGGER.error("Error during state cache update: %s", err)
            return

        self._valid = True
        async_dispatcher_send(self._hass, SIGNAL_VALLOX_STATE_UPDATE)


class ValloxServiceHandler:
    """Services implementation."""

    def __init__(self, client: Vallox, state_proxy: ValloxStateProxy) -> None:
        """Initialize the proxy."""
        self._client = client
        self._state_proxy = state_proxy

    async def async_set_profile(self, profile: str = "Home") -> bool:
        """Set the ventilation profile."""
        _LOGGER.debug("Setting ventilation profile to: %s", profile)

        try:
            await self._client.set_profile(STR_TO_PROFILE[profile])
            return True

        except (OSError, ValloxApiException) as err:
            _LOGGER.error("Error setting ventilation profile: %s", err)
            return False

    async def async_set_profile_fan_speed_home(
        self, fan_speed: int = DEFAULT_FAN_SPEED_HOME
    ) -> bool:
        """Set the fan speed in percent for the Home profile."""
        _LOGGER.debug("Setting Home fan speed to: %d%%", fan_speed)

        try:
            await self._client.set_values(
                {METRIC_KEY_PROFILE_FAN_SPEED_HOME: fan_speed}
            )
            return True

        except (OSError, ValloxApiException) as err:
            _LOGGER.error("Error setting fan speed for Home profile: %s", err)
            return False

    async def async_set_profile_fan_speed_away(
        self, fan_speed: int = DEFAULT_FAN_SPEED_AWAY
    ) -> bool:
        """Set the fan speed in percent for the Away profile."""
        _LOGGER.debug("Setting Away fan speed to: %d%%", fan_speed)

        try:
            await self._client.set_values(
                {METRIC_KEY_PROFILE_FAN_SPEED_AWAY: fan_speed}
            )
            return True

        except (OSError, ValloxApiException) as err:
            _LOGGER.error("Error setting fan speed for Away profile: %s", err)
            return False

    async def async_set_profile_fan_speed_boost(
        self, fan_speed: int = DEFAULT_FAN_SPEED_BOOST
    ) -> bool:
        """Set the fan speed in percent for the Boost profile."""
        _LOGGER.debug("Setting Boost fan speed to: %d%%", fan_speed)

        try:
            await self._client.set_values(
                {METRIC_KEY_PROFILE_FAN_SPEED_BOOST: fan_speed}
            )
            return True

        except (OSError, ValloxApiException) as err:
            _LOGGER.error("Error setting fan speed for Boost profile: %s", err)
            return False

    async def async_handle(self, call: ServiceCall) -> None:
        """Dispatch a service call."""
        method = SERVICE_TO_METHOD.get(call.service)
        params = call.data.copy()

        if method is None:
            return

        if not hasattr(self, method["method"]):
            _LOGGER.error("Service not implemented: %s", method["method"])
            return

        result = await getattr(self, method["method"])(**params)

        # Force state_proxy to refresh device state, so that updates are propagated to platforms.
        if result:
            await self._state_proxy.async_update()
