import time
from contextlib import contextmanager

from django.core.cache import cache
from django.db import transaction

from passerelle.utils.jsonresponse import APIError

DEFAULT_DURATION = 5 * 60  # 5 minutes

# keep data in cache for 1 day, i.e. we can answer a request from cache for 1 day
# day
CACHE_DURATION = 86400


@contextmanager
def row_lock(row):
    if row:
        with transaction.atomic():
            list(row.__class__.objects.filter(pk=row.pk).select_for_update())
            yield
    else:
        yield


class RowLockedCache:
    """Cache return value of a function, always return the cached value for
    performance but if the cache is stale update it asynchronously using
    a thread, prevent multiple update using row locks on database models and
    an update cache key.
    """

    def __init__(self, function, logger=None, row=None, duration=DEFAULT_DURATION, key_prefix=None):
        self.function = function
        self.row = row
        self.duration = duration
        self.key_prefix = key_prefix or function.__name__
        self.logger = logger

    def _key(self, *args, **kwargs):
        keys = []
        if self.row:
            keys.append(str(self.row.pk))
        for arg in args:
            if isinstance(arg, str):
                keys.append(arg)
            else:
                keys.append(hash(arg))
        return self.key_prefix + '-' + '-'.join(keys)

    def __call__(self, *args, **kwargs):
        now = time.time()
        key = self._key(*args, **kwargs)
        # Fast path
        cacheline = cache.get(key)
        if cacheline:
            timestamp = cacheline['timestamp']
            if now - timestamp >= self.duration:
                with row_lock(self.row):
                    # Slow path, check cacheline again
                    cacheline = cache.get(key)
                    if now - timestamp < self.duration:
                        return cacheline['value']
                    try:
                        value = self.function(*args, **kwargs)
                        cache.set(key, {'value': value, 'timestamp': now}, CACHE_DURATION)
                    except APIError as e:
                        if self.logger:
                            self.logger.error('failure to update cache (%s)', e)
            return cacheline['value']
        else:
            value = self.function(*args, **kwargs)
            cache.set(key, {'value': value, 'timestamp': now}, CACHE_DURATION)
            return value
