import datetime
import json
import logging
import random
import uuid
from dataclasses import dataclass
from importlib.resources import files as importlib_files
from typing import Dict, List, Optional, Tuple

from asgiref.sync import sync_to_async
from django.conf import settings
from django.contrib.auth import get_user
from django.contrib.auth.models import User
from django.core.exceptions import PermissionDenied
from django.db import IntegrityError, transaction
from django.http import HttpRequest
from django.utils import timezone

from promptgame.gameui.llm_wrappers import LLMChoice
from promptgame.gameui.models import Account, Defense
from promptgame.gameui.restrictions import get_random_restriction_id

log = logging.getLogger(__name__)


_default_defenses = None


def _get_initial_defenses() -> List[Dict[str, str]]:
    """Read default_defenses.json from promptgame.data with importlib."""
    global _default_defenses
    if _default_defenses is None:
        with importlib_files("promptgame.data").joinpath(
            "default_defenses.json"
        ).open() as fp:
            _default_defenses = json.load(fp)
    assert _default_defenses is not None
    return _default_defenses


@dataclass
class AccountDefaults:
    """Dataclass containing just account_id and balance for a user."""

    account_id: int
    account_name: str
    balance: int
    pre_prompt: str
    post_prompt: str
    access_code: str
    access_code_output: str
    restriction_id: str
    llm_choice: LLMChoice

    @staticmethod
    def generate() -> "AccountDefaults":
        defense_dict = dict(random.choice(_get_initial_defenses()))
        defense_dict["llm_choice"] = LLMChoice(defense_dict["llm_choice"])
        restriction_id = get_random_restriction_id()
        return AccountDefaults(
            account_id=random.randint(10**11, 10**12),
            account_name=get_account_name(),
            balance=1000,
            restriction_id=restriction_id,
            **defense_dict,
        )


class AccountCreationException(Exception):
    """Raised when we cannot generate a unique new account or account creation is locked."""


def create_user_and_account(
    user: Optional[User] = None,
) -> Tuple[User, Account]:
    # If user already exists and their account already exists,
    # we don't need to call this function. Throw an error.
    if settings.ACCOUNT_CREATION_LOCKED:
        raise AccountCreationException(
            "Failed to create an account because account creation is locked."
        )
    elif user is not None:
        # User must be authenticated
        if not user.is_authenticated:
            raise PermissionDenied("User is not authenticated")

        should_create_user = False
        try:
            account = user.account
            raise ValueError("User already has an account")
        except Account.DoesNotExist:
            pass
    else:
        should_create_user = True

    log.info(f"Creating new account, {should_create_user=}")

    # create an account
    for _ in range(10):  # Loop until we get a unique ID
        try:
            account_defaults = AccountDefaults.generate()
            with transaction.atomic():
                if should_create_user:
                    user = User.objects.create(username="auto-" + uuid.uuid1().hex)
                account = Account.objects.create(
                    owner=user,
                    account_id=account_defaults.account_id,
                    account_name=account_defaults.account_name,
                    balance=account_defaults.balance,
                    restriction_id=account_defaults.restriction_id,
                )
                defense = Defense.objects.create(
                    creator_account=account,
                    pre_prompt=account_defaults.pre_prompt,
                    post_prompt=account_defaults.post_prompt,
                    access_code=account_defaults.access_code,
                    access_code_output=account_defaults.access_code_output,
                    llm_choice=account_defaults.llm_choice.name,
                )
                account.active_defense = defense
                account.save()
                assert user is not None  # for type checker
            log.info(
                f"Created user {user.username} with account "
                f"{account.account_id=} ({account.account_name=})"
            )
            return user, account
        except IntegrityError as ex:
            # IntegrityError if the account ID is not unique
            log.warning(
                f"IntegrityError, likely due to account metadata collision: {ex}"
            )
    raise AccountCreationException(
        "Failed to create a unique account ID or account name after 10 tries. This is super weird."
    )


# This really has to be sync because Django doesn't support async transactions
# (and if we call sync code from an async view then we get an error).
async_create_user_and_account = sync_to_async(create_user_and_account)


def get_account_name() -> str:
    noun = random.choice(settings.ACCOUNT_NOUNS)
    adjective = random.choice(settings.ACCOUNT_ADJECTIVES)
    number = random.randint(settings.MIN_ACCOUNT_NUMBER, settings.MAX_ACCOUNT_NUMBER)
    return f"{adjective}{noun['name']}{number}{noun['emoji']}"


def get_unlock_time() -> datetime.datetime:
    return timezone.now() + settings.AUTO_UNLOCK_DELTA


def get_account(user: User) -> Account:
    try:
        return user.account
    except Account.DoesNotExist:
        _, account = create_user_and_account(user)
        return account


async def async_get_account(user: User) -> Account:
    try:
        return await Account.objects.aget(owner=user)
    except Account.DoesNotExist:
        _, account = await async_create_user_and_account(user)
        return account


def maybe_get_account(user: Optional[User]) -> Optional[Account]:
    if user is not None and user.is_authenticated:
        return get_account(user)
    return None


@sync_to_async
def async_get_authenticated_user(request: HttpRequest) -> Optional[User]:
    user = get_user(request)
    if user.is_authenticated:
        assert isinstance(user, User), f"Expected User, got {type(user)}"
        return user
    # explicit return None if we can't get an authenticated user
    return None
