import asyncio
import logging
import time
from datetime import datetime, timedelta
from functools import wraps
from typing import Any, Callable, Dict, Optional, Tuple, Union

from ..backends.interface import Backend
from ..key import get_cache_key

__all__ = ("early",)
logger = logging.getLogger(__name__)


def _default_condition(result) -> bool:
    return result is not None


def early(
    backend: Backend,
    ttl: Optional[Union[int, timedelta]],
    func_args: Optional[Union[Dict, Tuple]] = None,
    key: Optional[str] = None,
    condition: Callable[[Any], bool] = _default_condition,
    prefix: str = "early",
):
    """
    Cache strategy that try to solve Cache stampede problem (https://en.wikipedia.org/wiki/Cache_stampede),
    With hot cache recalculate a result in background near expiration time
    Warning Not good at cold cache

    :param backend: cache backend
    :param ttl: seconds in int or as timedelta object to store a result
    :param func_args: arguments that will be used in key
    :param key: custom cache key, may contain alias to args or kwargs passed to a call
    :param condition: callable object that determines whether the result will be saved or not
    :param prefix: custom prefix for key, default 'early'
    """

    def _decor(func):
        @wraps(func)
        async def _wrap(*args, **kwargs):
            _cache_key = prefix + ":" + get_cache_key(func, args, kwargs, func_args, key)
            cached = await backend.get(_cache_key)
            execution = _get_result_for_early(backend, func, args, kwargs, _cache_key, ttl, condition)

            if cached:
                expire_at, delta, result = cached
                if expire_at <= datetime.utcnow() and await backend.set(
                    _cache_key + ":hit", "1", expire=delta.total_seconds(), exist=False
                ):
                    logger.info("Recalculate cache for %s (exp_at %s)", _cache_key, expire_at)
                    asyncio.create_task(execution)
                return result
            return await execution

        return _wrap

    return _decor


async def _get_result_for_early(backend: Backend, func, args, kwargs, key, ttl, condition: Callable[[Any], bool]):
    start = time.time()
    result = await func(*args, **kwargs)
    if condition(result):
        delta = timedelta(seconds=max([ttl - (time.time() - start) * 2, 0]))
        logging.info("Set result for key %s", key)
        asyncio.create_task(backend.set(key, [datetime.utcnow() + delta, delta, result], expire=ttl))
    return result
