from django.db import models
from django.db.models import Count
from django.utils import timezone
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
from ordered_model.models import OrderedModel

from club.models import Club, ClubRole, Membership
from core.models import Group, User


class Election(models.Model):
    """This class allows to create a new election."""

    title = models.CharField(_("title"), max_length=255)
    description = models.TextField(_("description"), null=True, blank=True)
    clubs = models.ManyToManyField(
        Club,
        related_name="elections",
        verbose_name=_("clubs"),
        help_text=_("The club(s) this election is held for."),
    )
    start_candidature = models.DateTimeField(_("start candidature"), blank=False)
    end_candidature = models.DateTimeField(_("end candidature"), blank=False)
    start_date = models.DateTimeField(_("start date"), blank=False)
    end_date = models.DateTimeField(_("end date"), blank=False)

    edit_groups = models.ManyToManyField(
        Group,
        related_name="editable_elections",
        verbose_name=_("edit groups"),
        blank=True,
    )
    view_groups = models.ManyToManyField(
        Group,
        related_name="viewable_elections",
        verbose_name=_("view groups"),
        blank=True,
    )
    vote_groups = models.ManyToManyField(
        Group,
        related_name="votable_elections",
        verbose_name=_("vote groups"),
        blank=True,
    )
    candidature_groups = models.ManyToManyField(
        Group,
        related_name="candidate_elections",
        verbose_name=_("candidature groups"),
        blank=True,
    )

    voters = models.ManyToManyField(
        User, verbose_name=_("voters"), related_name="voted_elections"
    )
    archived = models.BooleanField(_("archived"), default=False)

    def __str__(self):
        return self.title

    @property
    def is_vote_active(self):
        now = timezone.now()
        return self.start_date <= now <= self.end_date

    @property
    def is_vote_finished(self):
        return timezone.now() > self.end_date

    @property
    def is_candidature_active(self):
        now = timezone.now()
        return self.start_candidature <= now <= self.end_candidature

    @property
    def is_vote_editable(self):
        return timezone.now() <= self.end_candidature

    def can_candidate(self, user):
        for group_id in self.candidature_groups.values_list("pk", flat=True):
            if user.is_in_group(pk=group_id):
                return True
        return False

    def can_vote(self, user):
        if not self.is_vote_active or self.has_voted(user):
            return False
        for group_id in self.vote_groups.values_list("pk", flat=True):
            if user.is_in_group(pk=group_id):
                return True
        return False

    def has_voted(self, user):
        return self.voters.filter(id=user.id).exists()

    @cached_property
    def results(self):
        results = {}
        total_vote = self.voters.count()
        for role in self.roles.all():
            results[role.title] = role.results(total_vote)
        return results

    @cached_property
    def results_applied(self) -> bool:
        """Returns True if one or more roles of this election have been applied."""
        return Membership.objects.filter(
            role__election_roles__election=self,
            end_date=None,
            start_date__gte=self.end_date,
        ).exists()


class Role(OrderedModel):
    """This class allows to create a new role available for a candidature."""

    election = models.ForeignKey(
        Election,
        related_name="roles",
        verbose_name=_("election"),
        on_delete=models.CASCADE,
    )
    title = models.CharField(_("title"), max_length=255)
    description = models.TextField(_("description"), default="", blank=True)
    max_choice = models.PositiveSmallIntegerField(_("max choice"), default=1)
    club_role = models.ForeignKey(
        ClubRole,
        related_name="election_roles",
        verbose_name=_("club role"),
        help_text=_(
            "A club role. Filling this will allow automatic "
            "completion of title and description, "
            "and automatic assignation after the elections."
        ),
        on_delete=models.CASCADE,
        null=True,
        blank=True,
    )

    order_with_respect_to = "election"

    class Meta(OrderedModel.Meta):
        constraints = [
            models.UniqueConstraint(
                fields=["title", "election"],
                name="title_election_unique_constraint",
                violation_error_message=_("This role already exists for this election"),
                violation_error_code="invalid",
            )
        ]

    def __str__(self):
        return f"{self.title} - {self.election.title}"

    def results(self, total_vote: int) -> dict[str, dict[str, int | float]]:
        if total_vote == 0:
            candidates = self.candidatures.values_list("user__username", flat=True)
            return {
                key: {"vote": 0, "percent": 0} for key in ["blank vote", *candidates]
            }
        total_vote *= self.max_choice
        results = {"total vote": total_vote}
        non_blank = 0
        candidatures = self.candidatures.annotate(nb_votes=Count("votes")).values(
            "nb_votes", "user__username"
        )
        for candidature in candidatures:
            non_blank += candidature["nb_votes"]
            results[candidature["user__username"]] = {
                "vote": candidature["nb_votes"],
                "percent": candidature["nb_votes"] * 100 / total_vote,
            }
        results["blank vote"] = {
            "vote": total_vote - non_blank,
            "percent": (total_vote - non_blank) * 100 / total_vote,
        }
        return results

    @property
    def edit_groups(self):
        return self.election.edit_groups


class ElectionList(models.Model):
    """To allow per list vote."""

    title = models.CharField(_("title"), max_length=255)
    election = models.ForeignKey(
        Election,
        related_name="election_lists",
        verbose_name=_("election"),
        on_delete=models.CASCADE,
    )

    def __str__(self):
        return self.title

    def can_be_edited_by(self, user):
        return user.can_edit(self.election)


class Candidature(models.Model):
    """This class is a component of responsability."""

    role = models.ForeignKey(
        Role,
        related_name="candidatures",
        verbose_name=_("role"),
        on_delete=models.CASCADE,
    )
    user = models.ForeignKey(
        User,
        verbose_name=_("user"),
        related_name="candidates",
        on_delete=models.CASCADE,
    )
    program = models.TextField(_("description"), default="", blank=True)
    election_list = models.ForeignKey(
        ElectionList,
        related_name="candidatures",
        verbose_name=_("election list"),
        on_delete=models.CASCADE,
    )

    def __str__(self):
        return f"{self.role.title} : {self.user.username}"

    def can_be_edited_by(self, user):
        return (
            (user == self.user) or user.can_edit(self.role.election)
        ) and self.role.election.is_vote_editable


class Vote(models.Model):
    """This class allows to vote for candidates."""

    role = models.ForeignKey(
        Role, related_name="votes", verbose_name=_("role"), on_delete=models.CASCADE
    )
    candidature = models.ManyToManyField(
        Candidature, related_name="votes", verbose_name=_("candidature")
    )

    def __str__(self):
        return "Vote"
