from django.contrib.contenttypes.fields import GenericRelation
from django.db import models
from django.utils.datastructures import SortedDict
from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_lazy, ugettext_noop

from openslides.agenda.models import Item, Speaker
from openslides.core.config import config
from openslides.core.models import Tag
from openslides.poll.models import (
    BaseOption,
    BasePoll,
    BaseVote,
    CollectDefaultVotesMixin,
    PublishPollMixin,
)
from openslides.users.models import User
from openslides.utils.exceptions import OpenSlidesError
from openslides.utils.models import RESTModelMixin


class AssignmentRelatedUser(RESTModelMixin, models.Model):
    """
    Many to Many table between an assignment and user.
    """
    STATUS_CANDIDATE = 1
    STATUS_ELECTED = 2
    STATUS_BLOCKED = 3
    STATUSES = (
        (STATUS_CANDIDATE, ugettext_lazy('candidate')),
        (STATUS_ELECTED, ugettext_lazy('elected')),
        (STATUS_BLOCKED, ugettext_lazy('blocked')),
    )

    assignment = models.ForeignKey(
        'Assignment',
        db_index=True,
        related_name='assignment_related_users')
    user = models.ForeignKey(User, db_index=True)
    status = models.IntegerField(
        choices=STATUSES,
        default=STATUS_CANDIDATE)

    class Meta:
        unique_together = ('assignment', 'user')

    def __str__(self):
        return "%s <-> %s" % (self.assignment, self.user)

    def get_root_rest_element(self):
        """
        Returns the assignment to this instance which is the root REST element.
        """
        return self.assignment


class Assignment(RESTModelMixin, models.Model):
    slide_callback_name = 'assignment'

    PHASE_SEARCH = 0
    PHASE_VOTING = 1
    PHASE_FINISHED = 2

    PHASES = (
        (PHASE_SEARCH, ugettext_lazy('Searching for candidates')),
        (PHASE_VOTING, ugettext_lazy('Voting')),
        (PHASE_FINISHED, ugettext_lazy('Finished')),
    )

    title = models.CharField(
        max_length=100,
        verbose_name=ugettext_lazy("Title"))
    """
    Title of the assignment.
    """

    description = models.TextField(
        blank=True,
        verbose_name=ugettext_lazy("Description"))
    """
    Text to describe the assignment.
    """

    open_posts = models.PositiveSmallIntegerField(
        verbose_name=ugettext_lazy("Number of members to be elected"))
    """
    The number of members to be elected.
    """

    poll_description_default = models.CharField(
        max_length=79,
        blank=True,
        verbose_name=ugettext_lazy("Default comment on the ballot paper"))
    """
    Default text for the poll description.
    """

    phase = models.IntegerField(
        choices=PHASES,
        default=PHASE_SEARCH)
    """
    Phase in which the assignment is.
    """

    related_users = models.ManyToManyField(
        User,
        through='AssignmentRelatedUser')
    """
    Users that a candidates, elected or blocked as candidate.

    See AssignmentRelatedUser for more infos.
    """

    tags = models.ManyToManyField(Tag, blank=True)
    """
    Tags for the assignment.
    """

    items = GenericRelation(Item)
    """
    Agenda items for this assignment.
    """

    class Meta:
        permissions = (
            ('can_see', ugettext_noop('Can see elections')),
            ('can_nominate_other', ugettext_noop('Can nominate another participant')),
            ('can_nominate_self', ugettext_noop('Can nominate oneself')),
            ('can_manage', ugettext_noop('Can manage elections')),
        )
        ordering = ('title', )
        verbose_name = ugettext_noop('Election')

    def __str__(self):
        return self.title

    def get_slide_context(self, **context):
        """
        Retuns the context to generate the assignment slide.
        """
        return super().get_slide_context(
            polls=self.polls.filter(published=True),
            vote_results=self.vote_results(only_published=True),
            **context)

    @property
    def candidates(self):
        """
        Queryset that represents the candidates for the assignment.
        """
        return self.related_users.filter(
            assignmentrelateduser__status=AssignmentRelatedUser.STATUS_CANDIDATE)

    @property
    def elected(self):
        """
        Queryset that represents all elected users for the assignment.
        """
        return self.related_users.filter(
            assignmentrelateduser__status=AssignmentRelatedUser.STATUS_ELECTED)

    @property
    def blocked(self):
        """
        Queryset that represents all blocked users for the assignment.
        """
        return self.related_users.filter(
            assignmentrelateduser__status=AssignmentRelatedUser.STATUS_BLOCKED)

    def is_candidate(self, user):
        """
        Returns True if user is a candidate.

        Costs one database query.
        """
        return self.candidates.filter(pk=user.pk).exists()

    def is_elected(self, user):
        """
        Returns True if the user is elected for this assignment.

        Costs one database query.
        """
        return self.elected.filter(pk=user.pk).exists()

    def is_blocked(self, user):
        """
        Returns True if the user is blockt for candidature.

        Costs one database query.
        """
        return self.blocked.filter(pk=user.pk).exists()

    def set_candidate(self, user):
        """
        Adds the user as candidate.
        """
        related_user, __ = self.assignment_related_users.update_or_create(
            user=user,
            defaults={'status': AssignmentRelatedUser.STATUS_CANDIDATE})

    def set_elected(self, user):
        """
        Makes user an elected user for this assignment.
        """
        related_user, __ = self.assignment_related_users.update_or_create(
            user=user,
            defaults={'status': AssignmentRelatedUser.STATUS_ELECTED})

    def set_blocked(self, user):
        """
        Block user from this assignment, so he can not get an candidate.
        """
        related_user, __ = self.assignment_related_users.update_or_create(
            user=user,
            defaults={'status': AssignmentRelatedUser.STATUS_BLOCKED})

    def delete_related_user(self, user):
        """
        Delete the connection from the assignment to the user.
        """
        self.assignment_related_users.filter(user=user).delete()

    def set_phase(self, phase):
        """
        Sets the phase attribute of the assignment.

        Raises a ValueError if the phase is not valide.
        """
        if phase not in dict(self.PHASES):
            raise ValueError("Invalid phase %s" % phase)

        self.phase = phase

    def create_poll(self):
        """
        Creates a new poll for the assignment and adds all candidates to all
        lists of speakers of related agenda items.
        """
        candidates = self.candidates.all()

        # Find out the method of the election
        if config['assignments_poll_vote_values'] == 'votes':
            yesnoabstain = False
        elif config['assignments_poll_vote_values'] == 'yesnoabstain':
            yesnoabstain = True
        else:
            # config['assignments_poll_vote_values'] == 'auto'
            # candidates <= available posts -> yes/no/abstain
            if len(candidates) <= (self.open_posts - self.elected.count()):
                yesnoabstain = True
            else:
                yesnoabstain = False

        # Create the poll with the candidates.
        poll = self.polls.create(
            description=self.poll_description_default,
            yesnoabstain=yesnoabstain)
        poll.set_options({'candidate': user} for user in candidates)

        # Add all candidates to all agenda items for this assignment
        # TODO: Try to do this in a bulk create
        for item in self.items.all():
            for candidate in self.candidates:
                try:
                    Speaker.objects.add(candidate, item)
                except OpenSlidesError:
                    # The Speaker is already on the list. Do nothing.
                    # TODO: Find a smart way not to catch the error concerning AnonymousUser.
                    pass

        return poll

    def vote_results(self, only_published):
        """
        Returns a table represented as a list with all candidates from all
        related polls and their vote results.
        """
        vote_results_dict = SortedDict()

        polls = self.polls.all()
        if only_published:
            polls = polls.filter(published=True)

        # All PollOption-Objects related to this assignment
        options = []
        for poll in polls:
            options += poll.get_options()

        for option in options:
            candidate = option.candidate
            if candidate in vote_results_dict:
                continue
            vote_results_dict[candidate] = []
            for poll in polls:
                votes = {}
                try:
                    # candidate related to this poll
                    poll_option = poll.get_options().get(candidate=candidate)
                    for vote in poll_option.get_votes():
                        votes[vote.value] = vote.print_weight()
                except AssignmentOption.DoesNotExist:
                    # candidate not in related to this poll
                    votes = None
                vote_results_dict[candidate].append(votes)
        return vote_results_dict

    def get_agenda_title(self):
        return str(self)

    def get_agenda_title_supplement(self):
        return '(%s)' % _('Assignment')


class AssignmentVote(RESTModelMixin, BaseVote):
    option = models.ForeignKey('AssignmentOption')

    def get_root_rest_element(self):
        """
        Returns the assignment to this instance which is the root REST element.
        """
        return self.option.poll.assignment


class AssignmentOption(RESTModelMixin, BaseOption):
    poll = models.ForeignKey('AssignmentPoll')
    candidate = models.ForeignKey(User)
    vote_class = AssignmentVote

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

    def get_root_rest_element(self):
        """
        Returns the assignment to this instance which is the root REST element.
        """
        return self.poll.assignment


class AssignmentPoll(RESTModelMixin, CollectDefaultVotesMixin,
                     PublishPollMixin, BasePoll):
    slide_callback_name = 'assignmentpoll'
    option_class = AssignmentOption

    assignment = models.ForeignKey(Assignment, related_name='polls')
    yesnoabstain = models.BooleanField(default=False)
    description = models.CharField(
        max_length=79,
        blank=True,
        verbose_name=ugettext_lazy("Comment on the ballot paper"))

    def __str__(self):
        return _("Ballot %d") % self.get_ballot()

    def get_assignment(self):
        return self.assignment

    def get_vote_values(self):
        if self.yesnoabstain:
            return [ugettext_noop('Yes'), ugettext_noop('No'), ugettext_noop('Abstain')]
        else:
            return [ugettext_noop('Votes')]

    def get_ballot(self):
        return self.assignment.polls.filter(id__lte=self.pk).count()

    def get_percent_base_choice(self):
        return config['assignments_poll_100_percent_base']

    def append_pollform_fields(self, fields):
        fields.append('description')
        super().append_pollform_fields(fields)

    def get_slide_context(self, **context):
        return super().get_slide_context(poll=self)

    def get_root_rest_element(self):
        """
        Returns the assignment to this instance which is the root REST element.
        """
        return self.assignment
