import hashlib

from django.contrib.auth import get_user_model
from django.core.cache import cache
from django.template.defaultfilters import slugify
from django.utils.encoding import force_bytes

from avatar.conf import settings

cached_funcs = set()


def get_username(user):
    """Return username of a User instance"""
    if hasattr(user, "get_username"):
        return user.get_username()
    else:
        return user.username


def get_user(userdescriptor):
    """Return user from a username/ID/ish identifier"""
    User = get_user_model()
    if isinstance(userdescriptor, int):
        user = User.objects.filter(id=userdescriptor).first()
        if user:
            return user
    elif userdescriptor.isdigit():
        user = User.objects.filter(id=int(userdescriptor)).first()
        if user:
            return user
    return User.objects.get_by_natural_key(userdescriptor)


def get_cache_key(user_or_username, prefix, width=None, height=None):
    """
    Returns a cache key consisten of a username and image size.
    """
    if isinstance(user_or_username, get_user_model()):
        user_or_username = get_username(user_or_username)
    key = f"{prefix}_{user_or_username}"
    if width:
        key += f"_{width}"
    if height or width:
        key += f"x{height or width}"
    return "%s_%s" % (
        slugify(key)[:100],
        hashlib.md5(force_bytes(key)).hexdigest(),
    )


def cache_set(key, value):
    cache.set(key, value, settings.AVATAR_CACHE_TIMEOUT)
    return value


def cache_result(default_size=settings.AVATAR_DEFAULT_SIZE):
    """
    Decorator to cache the result of functions that take a ``user``, a
    ``width`` and a ``height`` value.
    """
    if not settings.AVATAR_CACHE_ENABLED:

        def decorator(func):
            return func

        return decorator

    def decorator(func):
        def cached_func(user, width=None, height=None, **kwargs):
            prefix = func.__name__
            cached_funcs.add(prefix)
            key = get_cache_key(user, prefix, width or default_size, height)
            result = cache.get(key)
            if result is None:
                result = func(user, width or default_size, height, **kwargs)
                cache_set(key, result)
                # add image size to set of cached sizes so we can invalidate them later
                sizes_key = get_cache_key(user, "cached_sizes")
                sizes = cache.get(sizes_key, set())
                sizes.add((width or default_size, height or width or default_size))
                cache_set(sizes_key, sizes)
            return result

        return cached_func

    return decorator


def invalidate_cache(user, width=None, height=None):
    """
    Function to be called when saving or changing a user's avatars.
    """
    sizes_key = get_cache_key(user, "cached_sizes")
    sizes = cache.get(sizes_key, set())
    if width is not None:
        sizes.add((width, height or width))
    for prefix in cached_funcs:
        for size in sizes:
            if isinstance(size, int):
                cache.delete(get_cache_key(user, prefix, size))
            else:
                # Size is specified with height and width.
                cache.delete(get_cache_key(user, prefix, size[0], size[1]))
    cache.set(sizes_key, set())


def get_default_avatar_url():
    base_url = getattr(settings, "STATIC_URL", None)
    if not base_url:
        base_url = getattr(settings, "MEDIA_URL", "")

    # Don't use base_url if the default url starts with http:// of https://
    if settings.AVATAR_DEFAULT_URL.startswith(("http://", "https://")):
        return settings.AVATAR_DEFAULT_URL
    # We'll be nice and make sure there are no duplicated forward slashes
    ends = base_url.endswith("/")

    begins = settings.AVATAR_DEFAULT_URL.startswith("/")
    if ends and begins:
        base_url = base_url[:-1]
    elif not ends and not begins:
        return "%s/%s" % (base_url, settings.AVATAR_DEFAULT_URL)

    return "%s%s" % (base_url, settings.AVATAR_DEFAULT_URL)


def get_primary_avatar(user, width=settings.AVATAR_DEFAULT_SIZE, height=None):
    User = get_user_model()
    if not isinstance(user, User):
        try:
            user = get_user(user)
        except User.DoesNotExist:
            return None
    try:
        # Order by -primary first; this means if a primary=True avatar exists
        # it will be first, and then ordered by date uploaded, otherwise a
        # primary=False avatar will be first.  Exactly the fallback behavior we
        # want.
        avatar = user.avatar_set.order_by("-primary", "-date_uploaded")[0]
    except IndexError:
        avatar = None
    if avatar:
        if not avatar.thumbnail_exists(width, height):
            avatar.create_thumbnail(width, height)
    return avatar
