"""Utilities for levels."""

import hashlib
import random

from django.contrib.auth.models import User
from django.core.cache import cache

EMAIL_DOMAIN = "example.com"

ADMIN_USERNAME = "admin"
ADMIN_EMAIL = f"{ADMIN_USERNAME}@{EMAIL_DOMAIN}"

USER_USERNAME = "user"
USER_EMAIL = f"{USER_USERNAME}@{EMAIL_DOMAIN}"


# These are commonly used passwords, that are certain to be present
# in pretty much and rainbow table. We will randomly choose a password
# from this list, and assign it to the user.
_AVAILABLE_PASSWORDS = [
    "charlie",
    "1234567890",
    "qwerty",
    "senha",
    "abc123",
    "iloveyou",
    "qwertyuiop",
    "princess",
    "sunshine",
    "dragon",
    "pokemon",
    "football",
    "baseball",
    "superman",
    "michelle",
    "chocolate",
    "computer",
    "cookie",
    "batman",
    "madison",
]

AVAILABLE_HASHED_PASSWORDS = {}
for password in _AVAILABLE_PASSWORDS:
    AVAILABLE_HASHED_PASSWORDS[password] = hashlib.sha256(password.encode("utf-8")).hexdigest()

# Emails to be randomly picked from while populating the leak database
AVAILABLE_USER_NAMES = [
    "adam",
    "alice",
    "anika",
    "aragorn",
    "benedict",
    "bob",
    "boris",
    "damian",
    "darth_vader",
    "donna",
    "frodo",
    "gandalf",
    "geordi",
    "hilbert",
    "jack",
    "jake",
    "james",
    "jamie",
    "jane",
    "jennifer",
    "jessie",
    "joe" "john",
    "josh",
    "joshua",
    "kathryn",
    "laura",
    "legolas",
    "lucy",
    "luke_skywalker",
    "mario",
    "michelle",
    "mrhackerman",
    "robert",
    "rodger",
    "sarah",
    "saruman",
    "sauron",
    "spock",
    "susan",
    "tiberius",
    "tim",
    "tom",
    "travis",
    "victor",
    "vincent",
    "xavier",
]


def _get_random_names(amount: int, include_admin: bool = False) -> list[str]:
    """
    Get a list of `amount` random usernames.

    If a conflict occurs, the username will be suffixed with a random number.
    """
    if include_admin:
        amount -= 1

    usernames = set()
    for _ in range(amount):
        username = random.choice(AVAILABLE_USER_NAMES)
        max_suffix = 10
        while username in usernames:
            username = f"{username}{random.randint(1, max_suffix)}"
            max_suffix *= 10
        usernames.add(username)

    usernames_lst = list(usernames)

    if include_admin:
        usernames_lst.append(ADMIN_USERNAME)
        # Shuffle the list so the admin username isn't always last
        random.shuffle(usernames_lst)

    return usernames_lst


def generate_user_pw_entries(amount: int, store_hashes: bool = False) -> tuple[dict[str, str], str]:
    """
    Randomly generate (user, password) pairs.

    :param store_hashes:
        If True, store the password hashes instead of plain text passwords.
        The admin password will still be returned in plaintext
    :param amount: Number of entries to generate.
    :return: Tuple of (dictionary of username-pasword pairs, admin password)
    """
    entries: dict[str, str] = {}

    for username in _get_random_names(amount - 1):
        email = f"{username}@{EMAIL_DOMAIN}"
        password = random.choice(list(AVAILABLE_HASHED_PASSWORDS.keys()))
        if store_hashes:
            entries[email] = AVAILABLE_HASHED_PASSWORDS[password]
        else:
            entries[email] = password

    admin_password = random.choice(list(AVAILABLE_HASHED_PASSWORDS.keys()))
    if store_hashes:
        entries[ADMIN_EMAIL] = AVAILABLE_HASHED_PASSWORDS[admin_password]
    else:
        entries[ADMIN_EMAIL] = admin_password

    # Dictionaries in CPython are guaranteed to be sorted in the insertion order
    # this isn't good, since it would mean the admin email would always be last
    # which is too easy to find.
    items = list(entries.items())
    random.shuffle(items)
    entries = dict(items)

    return entries, admin_password


def generate_single_password() -> str:
    """Return a randomly chosen plaintext password."""
    return random.choice(list(AVAILABLE_HASHED_PASSWORDS.keys()))


def get_cached_password_entries(
    user: User,
    key_prefix: str,
    amount: int = 50,
    store_hashes: bool = False,
) -> tuple[dict[str, str], str]:
    """
    Obtain the cached password entries for given user, or create a new one.

    The password entries will be cached for 1 day, resetting any time it is accessed.

    :param user: User to generate password entries for.
    :param key_prefix: Prefix for the cache key.
    :param amount: Number of entries to generate.
    :param store_hashes: If True, store the entries will contain password hashes instead of plain text passwords.
    :return: Tuple of (dictionary of username-password pairs, admin password)
    """
    key = f"password_entries-{key_prefix}-{user.username}"
    admin_pw_key = f"{key}-admin-password"

    # Use existing (cached) database for this user
    if entries := cache.get(key):
        admin_password = cache.get(admin_pw_key)
        if admin_password is None:
            # This will probably never happen, it's only possible if the admin password entry
            # expires before we were able to get it, but just in case, reset the cache and
            # regenerate the entries.
            reset_password_entries_cache(user, key_prefix)
            return get_cached_password_entries(user, key_prefix, amount, store_hashes=store_hashes)

        # Reset the cache timeout (1 day)
        cache.touch(key, timeout=86400)
        cache.touch(admin_password, timeout=86400)

        return entries, admin_password

    # Generate new entries
    entries, admin_password = generate_user_pw_entries(amount, store_hashes=store_hashes)

    # cache the generated database (1 day)
    cache.set(key, entries, timeout=86400)
    cache.set(admin_pw_key, admin_password, timeout=86400)

    return entries, admin_password


def reset_password_entries_cache(user: User, key_prefix: str) -> None:
    """Reset the cached password entries from :func:`get_cached_password_entries`."""
    key = f"password_entries-{key_prefix}-{user.username}"
    admin_pw_key = f"{key}-admin-password"

    cache.delete(key)
    cache.delete(admin_pw_key)


def get_cached_single_password(user: User, key_prefix: str) -> str:
    """
    Obtain a single cached password entry for given user, or create a new one.

    The password will be cached for 1 day, resetting any time it is accessed.

    :param user: User to generate password entries for.
    :param key_prefix: Prefix for the cache key.
    :return: Plaintext password
    """
    key = f"password-{key_prefix}-{user.username}"
    if password := cache.get(key):
        cache.touch(key, timeout=86400)  # 1 day
    else:
        password = random.choice(_AVAILABLE_PASSWORDS)
        cache.set(key, password, timeout=86400)  # 1 day

    return password


def reset_single_password_cache(user: User, key_prefix: str) -> None:
    """Reset the cached single password from :func:`get_cached_single_password`."""
    key = f"password-{key_prefix}-{user.username}"
    cache.delete(key)
