import asyncio
import datetime
import logging
from typing import Any

__all__ = ['AsyncCycler', 'SyncCycler']


class AsyncCycler:
    IDLE_LOG_EVERY = 10


    def __init__(self, name, async_cycled_func=None, DEFAULT_SLEEP=1, WAS_WORK_SLEEP=1, ERROR_SLEEP=1 * 60):
        self.name = name
        if async_cycled_func is not None:
            self.async_cycled_func = async_cycled_func
        self.DEFAULT_SLEEP = DEFAULT_SLEEP
        self.WAS_WORK_SLEEP = WAS_WORK_SLEEP
        self.ERROR_SLEEP = ERROR_SLEEP
        self.cycle_count: int = 0
        self.idle_count: int = 0
        self.last_cycle_at: datetime.datetime | None = None
        self.last_error_at: datetime.datetime | None = None


    def get_sleep_time(self, was_work, was_error):
        if was_error:
            return self.ERROR_SLEEP
        if was_work:
            return self.WAS_WORK_SLEEP
        return self.DEFAULT_SLEEP


    def health(self) -> dict:
        return {
            "name"         : self.name,
            "cycle_count"  : self.cycle_count,
            "idle_count"   : self.idle_count,
            "last_cycle_at": self.last_cycle_at.isoformat() if self.last_cycle_at else None,
            "last_error_at": self.last_error_at.isoformat() if self.last_error_at else None,
        }


    async def run(self):
        while True:
            was_work = False
            was_error = False
            try:
                was_work = await self.async_cycled_func()
            except Exception:
                was_error = True
                self.last_error_at = datetime.datetime.now()
                logging.exception(f"{self.name} | cycle error | sleep={self.ERROR_SLEEP}s")
            finally:
                sleep_time = self.get_sleep_time(was_work, was_error)
            self._update_stats(was_work)
            if was_work:
                logging.info(f"{self.name} | cycle={self.cycle_count} completed | sleep={sleep_time}s")
            elif not was_error and self.idle_count % self.IDLE_LOG_EVERY == 0:
                logging.debug(f"{self.name} | heartbeat | idle={self.idle_count}")
            await asyncio.sleep(sleep_time)


    def _update_stats(self, was_work: bool | Any):
        if was_work:
            self.cycle_count += 1
            self.idle_count = 0
            self.last_cycle_at = datetime.datetime.now()
        else:
            self.idle_count += 1


    async def async_cycled_func(self):
        return False


class SyncCycler(AsyncCycler):
    def cycled_func(self):
        return False


    async def async_cycled_func(self):
        return self.cycled_func()


    def run(self):
        asyncio.run(super().run())
