import asyncio
import time
from dataclasses import dataclass
from random import normalvariate


@dataclass(frozen=True)
class BackoffConfig:
    min_delay: float
    max_delay: float
    factor: float
    jitter: float

    def __post_init__(self) -> None:
        if self.max_delay <= self.min_delay:
            raise ValueError("`max_delay` should be greater than `min_delay`")
        if self.factor <= 1:
            raise ValueError("`factor` should be greater than 1")


class Backoff:
    def __init__(self, config: BackoffConfig) -> None:
        self.config = config
        self._next_delay = config.min_delay
        self._current_delay = 0.0
        self._counter = 0

    def __iter__(self) -> "Backoff":
        return self

    @property
    def min_delay(self) -> float:
        return self.config.min_delay

    @property
    def max_delay(self) -> float:
        return self.config.max_delay

    @property
    def factor(self) -> float:
        return self.config.factor

    @property
    def jitter(self) -> float:
        return self.config.jitter

    @property
    def next_delay(self) -> float:
        return self._next_delay

    @property
    def current_delay(self) -> float:
        return self._current_delay

    @property
    def counter(self) -> int:
        return self._counter

    def sleep(self) -> None:
        time.sleep(next(self))

    async def asleep(self) -> None:
        await asyncio.sleep(next(self))

    def _calculate_next(self, value: float) -> float:
        return normalvariate(min(value * self.factor, self.max_delay), self.jitter)

    def __next__(self) -> float:
        self._current_delay = self._next_delay
        self._next_delay = self._calculate_next(self._next_delay)
        self._counter += 1
        return self._current_delay

    def reset(self) -> None:
        self._current_delay = 0.0
        self._counter = 0
        self._next_delay = self.min_delay

    def __str__(self) -> str:
        return (
            f"Backoff(tryings={self._counter}, current_delay={self._current_delay}, "
            f"next_delay={self._next_delay})"
        )
