from translated_fields import TranslatedFieldWithFallback

from django.conf import settings
from django.contrib.auth.models import AbstractUser
from django.db import models, transaction
from django.template.loader import render_to_string
from django.utils.translation import gettext_lazy as _

from shared.auth import CONNECTION_METHODS
from shared.utils import choices_length

from .staticdefs import (
    BALLOT_TYPE,
    CAST_FUNCTIONS,
    QUESTION_TYPES,
    TALLY_FUNCTIONS,
    VALIDATE_FUNCTIONS,
)
from .utils import (
    BallotsData,
    CastFunctions,
    ResultsData,
    TallyFunctions,
    ValidateFunctions,
)

# #############################################################################
#  Models regarding an election
# #############################################################################


class Election(models.Model):
    name = TranslatedFieldWithFallback(models.CharField(_("nom"), max_length=255))
    short_name = models.SlugField(_("nom bref"), unique=True)
    description = TranslatedFieldWithFallback(
        models.TextField(_("description"), blank=True)
    )

    start_date = models.DateTimeField(_("date et heure de début"))
    end_date = models.DateTimeField(_("date et heure de fin"))

    visible = models.BooleanField(_("visible au public"), default=False)

    vote_restrictions = TranslatedFieldWithFallback(
        models.TextField(_("conditions de vote"), blank=True)
    )

    restricted = models.BooleanField(
        _("restreint le vote à une liste de personnes"), default=True
    )

    sent_mail = models.BooleanField(
        _("mail avec les identifiants envoyé"), null=True, default=False
    )

    created_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        related_name="elections_created",
        on_delete=models.SET_NULL,
        blank=True,
        null=True,
    )

    voters = models.ManyToManyField(
        settings.AUTH_USER_MODEL,
        related_name="cast_elections",
        blank=True,
    )

    results_public = models.BooleanField(_("résultats publics"), default=False)
    tallied = models.BooleanField(_("dépouillée"), default=False)

    archived = models.BooleanField(_("archivée"), default=False)

    time_tallied = models.DateTimeField(
        _("date du dépouillement"), null=True, default=None
    )
    time_published = models.DateTimeField(
        _("date de publication"), null=True, default=None
    )

    class Meta:
        permissions = [
            ("election_admin", _("Peut administrer des élections")),
        ]
        ordering = ["-start_date", "-end_date"]


class Question(models.Model):
    election = models.ForeignKey(
        Election, related_name="questions", on_delete=models.CASCADE
    )
    text = TranslatedFieldWithFallback(
        models.TextField(_("question"), blank=True, default="")
    )
    type = models.CharField(
        _("type de question"),
        choices=QUESTION_TYPES,
        default="assentiment",
        max_length=choices_length(QUESTION_TYPES),
    )
    # We cache the maximum number of votes for an option
    max_votes = models.PositiveSmallIntegerField(
        _("nombre maximal de votes reçus"), default=0
    )

    voters = models.ManyToManyField(
        settings.AUTH_USER_MODEL,
        related_name="cast_questions",
        blank=True,
    )

    def is_form_valid(self, vote_form):
        validate_function = getattr(ValidateFunctions, VALIDATE_FUNCTIONS[self.type])
        return vote_form.is_valid() and validate_function(vote_form)

    @transaction.atomic
    def cast_ballot(self, user, vote_form):
        cast_function = getattr(CastFunctions, CAST_FUNCTIONS[self.type])
        cast_function(user, vote_form)

    @transaction.atomic
    def tally(self):
        tally_function = getattr(TallyFunctions, TALLY_FUNCTIONS[self.type])
        tally_function(self)

    @property
    def results(self):
        return render_to_string(
            f"elections/results/{self.vote_type}_export.txt", {"question": self}
        )

    def get_formset(self):
        from .forms import BallotFormset  # Avoid circular imports

        return getattr(BallotFormset, BALLOT_TYPE[self.type])

    def get_results_data(self):
        results_function = getattr(ResultsData, BALLOT_TYPE[self.type])
        return results_function(self)

    def display_ballots(self):
        display_function = getattr(BallotsData, BALLOT_TYPE[self.type])
        return display_function(self)

    @property
    def vote_type(self):
        return BALLOT_TYPE[self.type]

    def __str__(self):
        return self.text

    class Meta:
        ordering = ["id"]


class Option(models.Model):
    question = models.ForeignKey(
        Question, related_name="options", on_delete=models.CASCADE
    )
    text = TranslatedFieldWithFallback(models.TextField(_("texte"), blank=False))
    abbreviation = models.CharField(_("abréviation"), max_length=3, blank=True)

    winner = models.BooleanField(_("option gagnante"), default=False)
    voters = models.ManyToManyField(
        settings.AUTH_USER_MODEL,
        related_name="votes",
        through="Vote",
        blank=True,
    )
    # For now, we store the amount of votes received after the election is tallied
    nb_votes = models.PositiveSmallIntegerField(_("nombre de votes reçus"), default=0)

    def save(self, *args, **kwargs):
        # On enlève les espaces et on passe tout en majuscules
        self.abbreviation = "".join(self.abbreviation.upper().split())

        super().save(*args, **kwargs)

    def get_abbr(self, default):
        return self.abbreviation or default

    def __str__(self):
        if self.abbreviation:
            return self.abbreviation + " - " + self.text
        return self.text

    class Meta:
        ordering = ["id"]


class Vote(models.Model):
    option = models.ForeignKey(Option, on_delete=models.CASCADE)
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)

    class Meta:
        ordering = ["option"]


class Rank(models.Model):
    vote = models.OneToOneField(Vote, on_delete=models.CASCADE)
    rank = models.PositiveSmallIntegerField(_("rang de l'option"))

    class Meta:
        ordering = ["vote"]


class Duel(models.Model):
    question = models.ForeignKey(
        Question, related_name="duels", on_delete=models.CASCADE
    )
    winner = models.ForeignKey(
        Option, related_name="contests_won", on_delete=models.CASCADE
    )
    loser = models.ForeignKey(
        Option, related_name="contests_lost", on_delete=models.CASCADE
    )
    amount = models.PositiveSmallIntegerField(_("votes supplémentaires"))


# #############################################################################
#  Modification of the base User Model
# #############################################################################


class User(AbstractUser):
    election = models.ForeignKey(
        Election,
        related_name="registered_voters",
        null=True,
        blank=True,
        on_delete=models.CASCADE,
    )
    full_name = models.CharField(_("Nom et Prénom"), max_length=150, blank=True)
    has_valid_email = models.BooleanField(_("email valide"), null=True, default=None)

    @property
    def base_username(self):
        return "__".join(self.username.split("__")[1:])

    def can_vote(self, request, election):
        # Si c'est un·e utilisateur·ice CAS, iel peut voter dans les élections
        # ouvertes à tou·te·s
        if self.election is None:
            # If the user is connected via CAS, request.session["CASCONNECTED"] is set
            # to True by authens
            return not election.restricted and request.session.get("CASCONNECTED")

        # Pour les élections restreintes, il faut y être associé
        return election.restricted and (self.election == election)

    def get_prefix(self):
        return self.username.split("__")[0]

    @property
    def connection_method(self):
        method = self.username.split("__")[0]
        return CONNECTION_METHODS.get(method, _("identifiants spécifiques"))

    class Meta:
        ordering = ["username"]