"""Building blocks for the heater switch keep-alive feature.

The heater switch keep-alive feature consists of regularly refreshing the state
of directly controlled switches at a configurable interval (regularly turning the
switch 'on' or 'off' again even if it is already turned 'on' or 'off'), just like
the keep_alive setting of Home Assistant's Generic Thermostat integration:
 https://www.home-assistant.io/integrations/generic_thermostat/
"""

import logging
from vtherm_api.log_collector import get_vtherm_logger
from collections.abc import Awaitable, Callable
from datetime import timedelta, datetime
from time import monotonic

from homeassistant.core import HomeAssistant, CALLBACK_TYPE
from homeassistant.helpers.event import async_track_time_interval


_LOGGER = get_vtherm_logger(__name__)


class BackoffTimer:
    """Exponential backoff timer with a non-blocking polling-style implementation.

    Usage example:
        timer = BackoffTimer(multiplier=1.5, upper_limit_sec=600)
        while some_condition:
            if timer.is_ready():
                do_something()
    """

    def __init__(
        self,
        *,
        multiplier=2.0,
        lower_limit_sec=30,
        upper_limit_sec=86400,
        initially_ready=True,
    ):
        """Initialize a BackoffTimer instance.

        Args:
            multiplier (int, optional): Period multiplier applied when is_ready() is True.
            lower_limit_sec (int, optional): Initial backoff period in seconds.
            upper_limit_sec (int, optional): Maximum backoff period in seconds.
            initially_ready (bool, optional): Whether is_ready() should return True the
            first time it is called, or after a call to reset().
        """
        self._multiplier = multiplier
        self._lower_limit_sec = lower_limit_sec
        self._upper_limit_sec = upper_limit_sec
        self._initially_ready = initially_ready

        self._timestamp = 0
        self._period_sec = self._lower_limit_sec

    @property
    def in_progress(self) -> bool:
        """Whether the backoff timer is in progress (True after a call to is_ready())."""
        return bool(self._timestamp)

    def reset(self):
        """Reset a BackoffTimer instance."""
        self._timestamp = 0
        self._period_sec = self._lower_limit_sec

    def is_ready(self) -> bool:
        """Check whether an exponentially increasing period of time has passed.

        Whenever is_ready() returns True, the timer period is multiplied so that
        it takes longer until is_ready() returns True again.
        Returns:
            bool: True if enough time has passed since one of the following events,
            in relation to an instance of this class:
            - The last time when this method returned True, if it ever did.
            - Or else, when this method was first called after a call to reset().
            - Or else, when this method was first called.
            False otherwise.
        """
        now = monotonic()
        if self._timestamp == 0:
            self._timestamp = now
            return self._initially_ready
        elif now - self._timestamp >= self._period_sec:
            self._timestamp = now
            self._period_sec = max(
                self._lower_limit_sec,
                min(self._upper_limit_sec, self._period_sec * self._multiplier),
            )
            return True

        return False


class IntervalCaller:
    """Repeatedly call a given async action function at a given regular interval.

    Convenience wrapper around Home Assistant's `async_track_time_interval` function.
    """

    def __init__(self, hass: HomeAssistant, interval_sec: float) -> None:
        self._hass = hass
        self._interval_sec = interval_sec
        self._remove_handle: CALLBACK_TYPE | None = None
        self.backoff_timer = BackoffTimer()

    @property
    def interval_sec(self) -> float:
        """Return the calling interval in seconds."""
        return self._interval_sec

    def cancel(self):
        """Cancel the regular calls to the action function."""
        if self._remove_handle:
            self._remove_handle()
            self._remove_handle = None

    def set_async_action(self, action: Callable[[], Awaitable[None]]):
        """Set the async action function to be called at regular intervals."""
        if not self._interval_sec:
            return
        self.cancel()

        async def callback(_time: datetime):
            try:
                _LOGGER.debug(
                    "IntervalCaller - Calling keep-alive action '%s' (%ss interval)",
                    action.__name__,
                    self._interval_sec,
                )
                await action()
            except Exception as e:  # pylint: disable=broad-exception-caught
                _LOGGER.error(e)
                self.cancel()

        self._remove_handle = async_track_time_interval(
            self._hass, callback, timedelta(seconds=self._interval_sec)
        )
