# Copyright (C) 2023 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later

import os

from asgiref.sync import sync_to_async
from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.db.models import QuerySet
from django.urls import reverse_lazy
from django.utils.text import format_lazy, slugify
from django.utils.translation import gettext_lazy as _
from participation.models import Participation, Passage, Pool as PPool, Tournament


class Draw(models.Model):
    """
    A draw instance is linked to a :model:`participation.Tournament` and contains all information
    about a draw.
    """

    tournament = models.OneToOneField(
        Tournament,
        on_delete=models.CASCADE,
        verbose_name=_('tournament'),
        help_text=_("The associated tournament.")
    )

    current_round = models.ForeignKey(
        'Round',
        on_delete=models.CASCADE,
        null=True,
        default=None,
        related_name='+',
        verbose_name=_('current round'),
        help_text=_("The current round where teams select their problems."),
    )

    last_message = models.TextField(
        blank=True,
        default="",
        verbose_name=_("last message"),
        help_text=_("The last message that is displayed on the drawing interface.")
    )

    def get_absolute_url(self):
        return reverse_lazy('draw:index') + f'#{slugify(self.tournament.name)}'

    @property
    def exportable(self) -> bool:
        """
        True if any pool of the draw is exportable, ie. can be exported to the tournament interface.
        This operation is synchronous.
        """
        return any(pool.exportable for r in self.round_set.all() for pool in r.pool_set.all())

    async def is_exportable(self) -> bool:
        """
        True if any pool of the draw is exportable, ie. can be exported to the tournament interface.
        This operation is asynchronous.
        """
        return any([await pool.is_exportable() async for r in self.round_set.all() async for pool in r.pool_set.all()])

    def get_state(self) -> str:
        """
        The current state of the draw.
        Can be:

        * **DICE_SELECT_POULES** if we are waiting for teams to launch their dice to determine pools and passage order ;
        * **DICE_ORDER_POULE** if we are waiting for teams to launch their dice to determine the problem draw order ;
        * **WAITING_DRAW_PROBLEM** if we are waiting for a team to draw a problem ;
        * **WAITING_CHOOSE_PROBLEM** if we are waiting for a team to accept or reject a problem ;
        * **WAITING_FINAL** if this is the final tournament and we are between the two rounds ;
        * **DRAW_ENDED** if the draw is ended.

        Warning: the current round and the current team must be prefetched in an async context.
        """
        if self.current_round.current_pool is None:
            return 'DICE_SELECT_POULES'
        elif self.current_round.current_pool.current_team is None:
            return 'DICE_ORDER_POULE'
        elif self.current_round.current_pool.current_team.accepted is not None:
            if self.current_round.number < settings.NB_ROUNDS:
                # The last step can be the last problem acceptation after the first round
                # only for the final between the two rounds
                return 'WAITING_FINAL'
            else:
                return 'DRAW_ENDED'
        elif self.current_round.current_pool.current_team.purposed is None:
            return 'WAITING_DRAW_PROBLEM'
        else:
            return 'WAITING_CHOOSE_PROBLEM'
    get_state.short_description = _('State')

    @property
    def information(self):
        """
        The information header on the draw interface, which is defined according to the
        current state.

        Warning: this property is synchronous.
        """
        s = ""
        if self.last_message:
            s += self.last_message + "<br><br>"

        match self.get_state():
            case 'DICE_SELECT_POULES':
                # Waiting for dices to determine pools and passage order
                if self.current_round.number == 1:
                    # Specific information for the first round
                    s += _("We are going to start the problem draw.<br>"
                           "You can ask any question if something is not clear or wrong.<br><br>"
                           "We are going to first draw the pools and the passage order for the first round "
                           "with all the teams, then for each pool, we will draw the draw order and the problems.")
                    s += "<br><br>"
                s += _("The captains, you can now all throw a 100-sided dice, by clicking on the big dice button. "
                       "The pools and the passage order during the first round will be the increasing order "
                       "of the dices, ie. the smallest dice will be the first to pass in pool A.")
            case 'DICE_ORDER_POULE':
                # Waiting for dices to determine the choice order
                s += _("We are going to start the problem draw for the pool <strong>{pool}</strong>, "
                       "between the teams <strong>{teams}</strong>. "
                       "The captains can throw a 100-sided dice by clicking on the big dice button "
                       "to determine the order of draw. The team with the highest score will draw first.") \
                    .format(pool=self.current_round.current_pool,
                            teams=', '.join(td.participation.team.trigram
                                            for td in self.current_round.current_pool.teamdraw_set.all()))
            case 'WAITING_DRAW_PROBLEM':
                # Waiting for a problem draw
                td = self.current_round.current_pool.current_team
                s += _("The team <strong>{trigram}</strong> is going to draw a problem. "
                       "Click on the urn in the middle to draw a problem.") \
                    .format(trigram=td.participation.team.trigram)
            case 'WAITING_CHOOSE_PROBLEM':
                # Waiting for the team that can accept or reject the problem
                td = self.current_round.current_pool.current_team
                s += _("The team <strong>{trigram}</strong> drew the problem <strong>{problem}: "
                       "{problem_name}</strong>.") \
                    .format(trigram=td.participation.team.trigram,
                            problem=td.purposed, problem_name=settings.PROBLEMS[td.purposed - 1]) + " "
                if td.purposed in td.rejected:
                    # The problem was previously rejected
                    s += _("It already refused this problem before, so it can refuse it without penalty and "
                           "draw a new problem immediately, or change its mind.")
                else:
                    # The problem can be rejected
                    s += _("It can decide to accept or refuse this problem.") + " "
                    if len(td.rejected) >= len(settings.PROBLEMS) - settings.RECOMMENDED_SOLUTIONS_COUNT:
                        s += _("Refusing this problem will add a new 25% penalty "
                               "on the coefficient of the oral defense.")
                    else:
                        s += _("There are still {remaining} refusals without penalty.").format(
                            remaining=len(settings.PROBLEMS) - settings.RECOMMENDED_SOLUTIONS_COUNT - len(td.rejected))
            case 'WAITING_FINAL':
                # We are between the two rounds of the final tournament
                s += _("The draw for the second round will take place at the end of the first round. Good luck!")
            case 'DRAW_ENDED':
                # The draw is ended
                s += _("The draw is ended. The solutions of the other teams can be found in the tab "
                       "\"My participation\".")

        s += "<br><br>" if s else ""
        rules_link = settings.RULES_LINK
        s += _("For more details on the draw, the rules are available on "
               "<a class=\"alert-link\" href=\"{link}\">{link}</a>.").format(link=rules_link)
        return s

    async def ainformation(self) -> str:
        """
        Asynchronous version to get the information header content.
        """
        return await sync_to_async(lambda: self.information)()

    def __str__(self):
        return str(format_lazy(_("Draw of tournament {tournament}"), tournament=self.tournament.name))

    class Meta:
        verbose_name = _('draw')
        verbose_name_plural = _('draws')


class Round(models.Model):
    """
    This model is attached to a :model:`draw.Draw` and represents the draw
    for one round of the :model:`participation.Tournament`.
    """
    draw = models.ForeignKey(
        Draw,
        on_delete=models.CASCADE,
        verbose_name=_('draw'),
    )

    number = models.PositiveSmallIntegerField(
        choices=[
            (1, _('Round 1')),
            (2, _('Round 2')),
            (3, _('Round 3'))],
        verbose_name=_('number'),
        help_text=_("The number of the round, 1 or 2 (or 3 for ETEAM)"),
        validators=[MinValueValidator(1), MaxValueValidator(3)],
    )

    current_pool = models.ForeignKey(
        'Pool',
        on_delete=models.SET_NULL,
        null=True,
        default=None,
        related_name='+',
        verbose_name=_('current pool'),
        help_text=_("The current pool where teams select their problems."),
    )

    def get_absolute_url(self):
        return reverse_lazy('draw:index') + f'#{slugify(self.draw.tournament.name)}'

    @property
    def team_draws(self) -> QuerySet["TeamDraw"]:
        """
        Returns a query set ordered by pool and by passage index of all team draws.
        """
        return self.teamdraw_set.order_by('pool__letter', 'passage_index').all()

    async def next_pool(self):
        """
        Returns the next pool of the round.
        For example, after the pool A, we have the pool B.
        """
        pool = self.current_pool
        return await self.pool_set.aget(letter=pool.letter + 1)

    def __str__(self):
        return self.get_number_display()

    def clean(self):
        if self.number is not None and self.number > settings.NB_ROUNDS:
            raise ValidationError({'number': _("The number of the round must be between 1 and {nb}.")
                                  .format(nb=settings.NB_ROUNDS)})

        return super().clean()

    class Meta:
        verbose_name = _('round')
        verbose_name_plural = _('rounds')
        ordering = ('draw__tournament__name', 'number',)


class Pool(models.Model):
    """
    A Pool is a collection of teams in a :model:`draw.Round` of a `draw.Draw`.
    It has a letter (eg. A, B, C or D) and a size, between 3 and 5.
    After the draw, the pool can be exported in a `participation.Pool` instance.
    """
    round = models.ForeignKey(
        Round,
        on_delete=models.CASCADE,
    )

    letter = models.PositiveSmallIntegerField(
        choices=[
            (1, 'A'),
            (2, 'B'),
            (3, 'C'),
            (4, 'D'),
        ],
        verbose_name=_('letter'),
        help_text=_("The letter of the pool: A, B, C or D."),
    )

    size = models.PositiveSmallIntegerField(
        verbose_name=_('size'),
        validators=[MinValueValidator(3), MaxValueValidator(5)],
        help_text=_("The number of teams in this pool, between 3 and 5."),
    )

    current_team = models.ForeignKey(
        'TeamDraw',
        on_delete=models.CASCADE,
        null=True,
        default=None,
        related_name='+',
        verbose_name=_('current team'),
        help_text=_("The current team that is selecting its problem."),
    )

    associated_pool = models.OneToOneField(
        'participation.Pool',
        on_delete=models.SET_NULL,
        null=True,
        default=None,
        related_name='draw_pool',
        verbose_name=_("associated pool"),
        help_text=_("The full pool instance."),
    )

    def get_absolute_url(self):
        return reverse_lazy('draw:index') + f'#{slugify(self.round.draw.tournament.name)}'

    @property
    def team_draws(self) -> QuerySet["TeamDraw"]:
        """
        Returns a query set ordered by passage index of all team draws in this pool.
        """
        return self.teamdraw_set.all()

    @property
    def trigrams(self) -> list[str]:
        """
        Returns a list of trigrams of the teams in this pool ordered by passage index.
        This property is synchronous.
        """
        return [td.participation.team.trigram for td in self.teamdraw_set.order_by('passage_index')
                .prefetch_related('participation__team').all()]

    async def atrigrams(self) -> list[str]:
        """
        Returns a list of trigrams of the teams in this pool ordered by passage index.
        This property is asynchronous.
        """
        return [td.participation.team.trigram async for td in self.teamdraw_set.order_by('passage_index')
                .prefetch_related('participation__team').all()]

    async def next_td(self) -> "TeamDraw":
        """
        Returns the next team draw after the current one, to know who should draw a new problem.
        """
        td = self.current_team
        current_index = (td.choose_index + 1) % self.size
        td = await self.teamdraw_set.prefetch_related('participation__team').aget(choose_index=current_index)
        while td.accepted:
            # Ignore if the next team already accepted its problem
            current_index += 1
            current_index %= self.size
            td = await self.teamdraw_set.prefetch_related('participation__team').aget(choose_index=current_index)
        return td

    @property
    def exportable(self) -> bool:
        """
        True if this pool is exportable, ie. can be exported to the tournament interface. That means that
        each team selected its problem.
        This operation is synchronous.
        """
        return self.associated_pool_id is None and self.teamdraw_set.exists() \
            and all(td.accepted is not None for td in self.teamdraw_set.all())

    async def is_exportable(self) -> bool:
        """
        True if this pool is exportable, ie. can be exported to the tournament interface. That means that
        each team selected its problem.
        This operation is asynchronous.
        """
        return self.associated_pool_id is None and await self.teamdraw_set.aexists() \
            and all([td.accepted is not None async for td in self.teamdraw_set.all()])

    async def export(self) -> PPool:
        """
        Translates this Pool instance in a :model:`participation.Pool` instance, with the passage orders.
        """
        # Create the pool
        self.associated_pool, _created = await PPool.objects.aget_or_create(
            tournament=self.round.draw.tournament,
            round=self.round.number,
            letter=self.letter,
        )

        # Define the participations of the pool
        tds = [td async for td in self.team_draws.prefetch_related('participation')]
        await self.associated_pool.participations.aset([td.participation async for td in self.team_draws
                                                       .prefetch_related('participation')])
        await self.asave()

        pool2 = None
        if self.size == 5:
            pool2, _created = await PPool.objects.aget_or_create(
                tournament=self.round.draw.tournament,
                round=self.round.number,
                letter=self.letter,
                room=2,
            )
            await pool2.participations.aset([td.participation async for td in self.team_draws
                                             .prefetch_related('participation')])

        # Define the passage matrix according to the number of teams
        table = []
        if self.size == 3:
            table = [
                [0, 1, 2],
                [1, 2, 0],
                [2, 0, 1],
            ]
        elif self.size == 4:
            table = [
                [0, 1, 2, 3],
                [1, 2, 3, 0],
                [2, 3, 0, 1],
                [3, 0, 1, 2],
            ]
        elif self.size == 5:
            table = [
                [0, 2, 3, 4],
                [1, 3, 4, 0],
                [2, 4, 0, 1],
                [3, 0, 1, 2],
                [4, 1, 2, 3],
            ]

        for i, line in enumerate(table):
            passage_pool = self.associated_pool
            passage_position = i + 1
            if self.size == 5:
                # In 5-teams pools, we may create some passages in the second room
                if i % 2 == 1:
                    passage_pool = pool2
                passage_position = 1 + i // 2

            reporter = tds[line[0]].participation
            opponent = tds[line[1]].participation
            reviewer = tds[line[2]].participation
            observer = tds[line[3]].participation if self.size >= 4 and settings.HAS_OBSERVER else None

            # Create the passage
            await Passage.objects.acreate(
                pool=passage_pool,
                position=passage_position,
                solution_number=tds[line[0]].accepted,
                reporter=reporter,
                opponent=opponent,
                reviewer=reviewer,
                observer=observer,
                reporter_penalties=tds[line[0]].penalty_int,
            )

        # Update Google Sheets
        if os.getenv('GOOGLE_PRIVATE_KEY_ID', None):
            await sync_to_async(self.associated_pool.update_spreadsheet)()

        return self.associated_pool

    def __str__(self):
        return str(format_lazy(_("Pool {letter}{number}"), letter=self.get_letter_display(), number=self.round.number))

    class Meta:
        verbose_name = _('pool')
        verbose_name_plural = _('pools')
        ordering = ('round__draw__tournament__name', 'round__number', 'letter',)


class TeamDraw(models.Model):
    """
    This model represents the state of the draw for a given team, including
    its accepted problem or their rejected ones.
    """
    participation = models.ForeignKey(
        Participation,
        on_delete=models.CASCADE,
        verbose_name=_('participation'),
    )

    round = models.ForeignKey(
        Round,
        on_delete=models.CASCADE,
        verbose_name=_('round'),
    )

    pool = models.ForeignKey(
        Pool,
        on_delete=models.CASCADE,
        null=True,
        default=None,
        verbose_name=_('pool'),
    )

    passage_index = models.PositiveSmallIntegerField(
        choices=zip(range(0, 5), range(0, 5)),
        null=True,
        default=None,
        verbose_name=_('passage index'),
        help_text=_("The passage order in the pool, between 0 and the size of the pool minus 1."),
        validators=[MinValueValidator(0), MaxValueValidator(4)],
    )

    choose_index = models.PositiveSmallIntegerField(
        choices=zip(range(0, 5), range(0, 5)),
        null=True,
        default=None,
        verbose_name=_('choose index'),
        help_text=_("The choice order in the pool, between 0 and the size of the pool minus 1."),
        validators=[MinValueValidator(0), MaxValueValidator(4)],
    )

    accepted = models.PositiveSmallIntegerField(
        choices=[
            (i, format_lazy(_("Problem #{problem}"), problem=i)) for i in range(1, len(settings.PROBLEMS) + 1)
        ],
        null=True,
        default=None,
        verbose_name=_("accepted problem"),
    )

    passage_dice = models.PositiveSmallIntegerField(
        choices=zip(range(1, 101), range(1, 101)),
        null=True,
        default=None,
        verbose_name=_("passage dice"),
    )

    choice_dice = models.PositiveSmallIntegerField(
        choices=zip(range(1, 101), range(1, 101)),
        null=True,
        default=None,
        verbose_name=_("choice dice"),
    )

    purposed = models.PositiveSmallIntegerField(
        choices=[
            (i, format_lazy(_("Problem #{problem}"), problem=i)) for i in range(1, len(settings.PROBLEMS) + 1)
        ],
        null=True,
        default=None,
        verbose_name=_("purposed problem"),
    )

    rejected = models.JSONField(
        default=list,
        verbose_name=_('rejected problems'),
    )

    def get_absolute_url(self):
        return reverse_lazy('draw:index') + f'#{slugify(self.round.draw.tournament.name)}'

    @property
    def last_dice(self):
        """
        The last dice that was thrown.
        """
        return self.passage_dice if self.round.draw.get_state() == 'DICE_SELECT_POULES' else self.choice_dice

    @property
    def penalty_int(self):
        """
        The number of penalties, which is the number of rejected problems after the P - 5 free rejects
        (P - 6 for ETEAM), where P is the number of problems.
        """
        return max(0, len(self.rejected) - (len(settings.PROBLEMS) - settings.RECOMMENDED_SOLUTIONS_COUNT))

    @property
    def penalty(self):
        """
        The penalty multiplier on the reporter oral, in percentage, which is a malus of 25% for each penalty.
        """
        return 25 * self.penalty_int

    def __str__(self):
        return str(format_lazy(_("Draw of the team {trigram} for the pool {letter}{number}"),
                               trigram=self.participation.team.trigram,
                               letter=self.pool.get_letter_display() if self.pool else "",
                               number=self.round.number))

    class Meta:
        verbose_name = _('team draw')
        verbose_name_plural = _('team draws')
        ordering = ('round__draw__tournament__name', 'round__number', 'pool__letter', 'passage_index',
                    'choice_dice', 'passage_dice',)