import uuid
from typing import Dict, Optional

from django.conf import settings
from django.contrib.auth.models import User
from django.db import models
from django.utils import timezone

from promptgame.gameui.llm_wrappers import LLMChoice
from promptgame.gameui.restrictions import RESTRICTIONS_DICT


class Account(models.Model):
    owner = models.OneToOneField(User, on_delete=models.CASCADE)
    account_id = models.BigIntegerField(unique=True)
    account_name = models.CharField(unique=True, max_length=64)
    balance = models.BigIntegerField(db_index=True)
    active_defense = models.ForeignKey(
        "Defense", on_delete=models.CASCADE, null=True, blank=True
    )
    unlock_at = models.DateTimeField(db_index=True, null=True, blank=True)
    cached_target_accounts = models.JSONField(null=True)
    choices = [(k, k) for k in RESTRICTIONS_DICT.keys()]
    restriction_id = models.CharField(max_length=64, choices=choices)
    restriction_id_updated_at = models.DateTimeField(auto_now_add=True)

    shadow_balance = models.FloatField(default=settings.SHADOW_BALANCE_CAP)
    shadow_balance_updated_at = models.DateTimeField(auto_now_add=True)

    # what tutorial step the user is on
    tutorial_step = models.IntegerField(default=1)

    @property
    def has_been_attacked(self) -> bool:
        return (
            AuthAttempt.objects.filter(defending_account=self)
            .exclude(attacking_account=self)
            .exists()
        )

    @property
    def locked(self) -> bool:
        return self.unlock_at is None or timezone.now() < self.unlock_at

    @property
    def auto_unlock_time_left(self) -> str:
        return self._auto_unlock_time_left(self.unlock_at)

    @staticmethod
    def _auto_unlock_time_left(unlock_at: Optional[timezone.datetime]) -> str:
        if unlock_at is None:
            return "Never"

        time_difference = unlock_at - timezone.now()
        total_seconds = time_difference.days * 24 * 3600 + time_difference.seconds
        hours = total_seconds // 3600
        minutes = (total_seconds % 3600) // 60

        hour_str = f"{hours} hour" if hours == 1 else f"{hours} hours"
        minute_str = f"{minutes} minute" if minutes == 1 else f"{minutes} minutes"

        return f"{hour_str}, {minute_str}" if hours > 0 else f"{minute_str}"

    def __str__(self):
        return str(self.account_id)


LLM_CHOICES = (
    # database name and human-readable name for each model
    (LLMChoice.GPT_35_TURBO_0613.name, "GPT 3.5 Turbo"),
    (LLMChoice.PALM_CHAT_BISON_001.name, "PaLM Chat Bison"),
    (LLMChoice.CLAUDE_INSTANT_1_2.name, "Claude Instant 1.2"),
)


class AuthAttempt(models.Model):
    defending_account = models.ForeignKey(
        Account, related_name="defense_attempts", on_delete=models.CASCADE
    )
    attacking_account = models.ForeignKey(
        Account, related_name="attack_attempts", on_delete=models.CASCADE
    )
    timestamp = models.DateTimeField(auto_now_add=True)
    was_successful = models.BooleanField()
    attacker_input = models.TextField(blank=True, default="")
    # llm_output can be null when the attack text got flagged
    llm_output = models.TextField(blank=True, default="", null=True)
    # amount the defender won (now a misnomer, since the money for the defender
    # is magicked out of thin air)
    attacking_transfer_amount = models.BigIntegerField(blank=True, default=0)
    # amount the attacker won from the defender
    defending_transfer_amount = models.BigIntegerField(blank=True, default=0)
    defense = models.ForeignKey(
        "Defense", on_delete=models.CASCADE, null=True, blank=True
    )
    defense_pre_prompt = models.TextField(blank=True, default="")
    defense_post_prompt = models.TextField(blank=True, default="")
    access_code = models.TextField(blank=True, default="")
    auto_moderated = models.BooleanField(default=True)
    attacking_balance = models.BigIntegerField(blank=True, null=True)
    defending_balance = models.BigIntegerField(blank=True, null=True)
    # Unlike in Account, we don't set defaults here because we want to be able
    # to record the restriction id even if the restriction doesn't exist anymore.
    restriction_id = models.CharField(max_length=64, blank=True, null=True)
    llm_choice = models.CharField(max_length=64, choices=LLM_CHOICES)

    def __str__(self):
        success = "successful" if self.was_successful else "unsuccessful"
        formatted_time = self.timestamp.strftime("%Y-%m-%d %H:%M:%S")
        return (
            f"{formatted_time}: {self.attacking_account.account_id}'s {success} "
            f"attempt on {self.defending_account.account_id}'s account"
        )


class Defense(models.Model):
    creator_account = models.ForeignKey(
        Account, on_delete=models.CASCADE, related_name="defenses"
    )
    pre_prompt = models.TextField(blank=True)
    post_prompt = models.TextField(blank=True)
    access_code = models.TextField(blank=True)
    access_code_output = models.TextField(blank=True)
    timestamp = models.DateTimeField(auto_now_add=True)
    auto_moderated = models.BooleanField(default=True)
    llm_choice = models.CharField(max_length=64, choices=LLM_CHOICES)

    @property
    def llm_choice_as_enum(self) -> LLMChoice:
        return LLMChoice[self.llm_choice]

    @property
    def human_readable_llm_choice(self) -> str:
        current_choice = self.llm_choice
        default_name = "<unknown model>"
        name_dict: Dict[str, str] = dict(LLM_CHOICES)
        return name_dict.get(current_choice, default_name)

    @classmethod
    @property
    def llm_choice_to_name(cls) -> Dict[str, str]:
        # return choices for self.model field
        return dict(LLM_CHOICES)

    def __str__(self):
        return (
            f"Defense for {self.creator_account.account_id} created at {self.timestamp}"
        )


class LoginURL(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4)
    user = models.ForeignKey(User, on_delete=models.CASCADE)

    def __str__(self):
        return f"{self.id} (for user {self.user.account.account_id})"


class Flag(models.Model):
    REPORT_SOURCE_CHOICES = [
        ("admin", "Admin"),
        ("user", "User"),
        ("automatic", "Automatic"),
    ]

    resolution_text = models.TextField(
        null=True, blank=True
    )  # Text field for admins to say if it was resolved and how
    reviewed_by_admin = models.BooleanField(default=False)
    report_source = models.CharField(
        max_length=20,
        choices=REPORT_SOURCE_CHOICES,
        default="automatic",
    )
    defense = models.ForeignKey("Defense", on_delete=models.SET_NULL, null=True)
    authattempt = models.ForeignKey("AuthAttempt", on_delete=models.SET_NULL, null=True)
    metadata = models.JSONField(
        null=True, blank=True
    )  # Metadata json (includes output of the moderation endpoint)
    flagger = models.ForeignKey("Account", on_delete=models.SET_NULL, null=True)
    timestamp = models.DateTimeField(auto_now_add=True)  # Timestamp of report
