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

from collections import OrderedDict
import json
import os
from random import randint, shuffle

from asgiref.sync import sync_to_async
from channels.generic.websocket import AsyncJsonWebsocketConsumer
from django.conf import settings
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.utils import translation
from django.utils.translation import gettext_lazy as _
from draw.models import Draw, Pool, Round, TeamDraw
from logs.models import Changelog
from participation.models import Participation, Tournament
from registration.models import Registration


def ensure_orga(f):
    """
    This decorator to an asynchronous receiver guarantees that the user is a volunteer.
    If it is not the case, we send an alert and don't run the function.
    """
    async def func(self, *args, **kwargs):
        reg = self.registration
        if reg.is_volunteer and not reg.is_admin and self.tournament not in reg.interesting_tournaments \
                or not reg.is_volunteer:
            return await self.alert(_("You are not an organizer."), 'danger')

        return await f(self, *args, **kwargs)

    return func


class DrawConsumer(AsyncJsonWebsocketConsumer):
    """
    This consumer manages the websocket of the draw interface.
    """
    async def connect(self) -> None:
        """
        This function is called when a new websocket is trying to connect to the server.
        We accept only if this is a user of a team of the associated tournament, or a volunteer
        of the tournament.
        """
        if '_fake_user_id' in self.scope['session']:
            self.scope['user'] = await User.objects.aget(pk=self.scope['session']['_fake_user_id'])

        # Fetch the registration of the current user
        user = self.scope['user']
        if user.is_anonymous:
            # User is not authenticated
            await self.close()
            return

        reg = await Registration.objects.aget(user_id=user.id)
        self.registration = reg

        # Accept the connection
        await self.accept()

        # Register to channel layers to get updates
        if self.registration.participates:
            await self.channel_layer.group_add(f"team-{self.registration.team.trigram}", self.channel_name)
            participation = reg.team.participation
            if participation.valid:
                await self.channel_layer.group_add(f"tournament-{participation.tournament.id}", self.channel_name)
        else:
            tids = [t.id async for t in Tournament.objects.all()] \
                if reg.is_admin else [t.id for t in reg.interesting_tournaments]
            for tid in tids:
                await self.channel_layer.group_add(f"tournament-{tid}", self.channel_name)
                await self.channel_layer.group_add(f"volunteer-{tid}", self.channel_name)

    async def disconnect(self, close_code) -> None:
        """
        Called when the websocket got disconnected, for any reason.
        :param close_code: The error code.
        """
        if self.scope['user'].is_anonymous:
            # User is not authenticated
            return

        # Unregister from channel layers
        if not self.registration.is_volunteer:
            await self.channel_layer.group_discard(f"team-{self.registration.team.trigram}", self.channel_name)
            participation = self.registration.team.participation
            await self.channel_layer.group_discard(f"tournament-{participation.tournament.id}", self.channel_name)
        else:
            async for tournament in Tournament.objects.all():
                await self.channel_layer.group_discard(f"tournament-{tournament.id}", self.channel_name)
                await self.channel_layer.group_discard(f"volunteer-{tournament.id}", self.channel_name)

    async def alert(self, message: str, alert_type: str = 'info', tid: int = -1, **kwargs):
        """
        Send an alert message to the current user.
        :param message: The body of the alert.
        :param alert_type: The type of the alert, which is a bootstrap color (success, warning, info, danger,…)
        :param tid: The tournament id. Default to -1, the current tournament.
        """
        tid = tid if tid > 0 else self.tournament_id
        return await self.send_json({'tid': tid, 'type': 'alert', 'alert_type': alert_type, 'message': str(message)})

    async def receive_json(self, content, **kwargs):
        """
        Called when the client sends us some data, parsed as JSON.
        :param content: The sent data, decoded from JSON text. Must content a `type` field.
        """
        # Get the tournament from the message
        self.tournament_id = content['tid']
        self.tournament = await Tournament.objects.filter(pk=self.tournament_id) \
            .prefetch_related('draw__current_round__current_pool__current_team__participation__team').aget()

        # Fetch participations from the tournament
        self.participations = []
        async for participation in self.tournament.participations.filter(valid=True).prefetch_related('team'):
            self.participations.append(participation)

        # Refresh tournament
        self.tournament = await Tournament.objects.filter(pk=self.tournament_id)\
            .prefetch_related('draw__current_round__current_pool__current_team__participation__team').aget()

        match content['type']:
            case 'set_language':
                # Update the translation language
                translation.activate(content['language'])
            case 'start_draw':
                # Start a new draw
                await self.start_draw(**content)
            case 'abort':
                # Abort the current draw
                await self.abort(**content)
            case 'cancel':
                # Cancel the last step
                await self.cancel_last_step(**content)
            case 'dice':
                # Launch a dice
                await self.process_dice(**content)
            case 'draw_problem':
                # Draw a new problem
                await self.select_problem(**content)
            case 'accept':
                # Accept the proposed problem
                await self.accept_problem(**content)
            case 'reject':
                # Reject the proposed problem
                await self.reject_problem(**content)
            case 'export':
                # Export the current state of the draw
                await self.export(**content)
            case 'continue_final':
                # Continue the draw for the final tournament
                await self.continue_final(**content)

    @ensure_orga
    async def start_draw(self, fmt: str, **kwargs) -> None:
        """
        Initialize a new draw, with a given format.
        :param fmt: The format of the tournament, which is the size of each pool.
                    Sizes must be between 3 and 5, and the sum must be the number of teams.
        """
        if await Draw.objects.filter(tournament=self.tournament).aexists():
            return await self.alert(_("The draw is already started."), 'danger')

        try:
            # Parse format from string
            fmt: list[int] = sorted(map(int, fmt.split('+')))
        except ValueError:
            return await self.alert(_("Invalid format"), 'danger')

        # Ensure that the number of teams is good
        if sum(fmt) != len(self.participations):
            return await self.alert(
                _("The sum must be equal to the number of teams: expected {len}, got {sum}")
                .format(len=len(self.participations), sum=sum(fmt)), 'danger')

        # The drawing system works with a maximum of 1 pool of 5 teams, which is already the case in the TFJM²
        if fmt.count(5) > 1:
            return await self.alert(_("There can be at most one pool with 5 teams."), 'danger')

        # Create the draw
        draw = await Draw.objects.acreate(tournament=self.tournament)
        r1 = None
        for i in [1, 2]:
            # Create the round
            r = await Round.objects.acreate(draw=draw, number=i)
            if i == 1:
                r1 = r

            for j, f in enumerate(fmt):
                # Create the pool, and correspond the size with the wanted format
                await Pool.objects.acreate(round=r, letter=j + 1, size=f)
            for participation in self.participations:
                # Create a team draw object per participation
                await TeamDraw.objects.acreate(participation=participation, round=r)
            # Send to clients the different pools
            await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
                                                {
                                                    'tid': self.tournament_id,
                                                    'type': 'draw.send_poules',
                                                    'round': r.number,
                                                    'poules': [
                                                        {
                                                            'letter': pool.get_letter_display(),
                                                            'teams': await pool.atrigrams(),
                                                        }
                                                        async for pool in r.pool_set.order_by('letter').all()
                                                    ]
                                                })

        draw.current_round = r1
        await draw.asave()

        # Make dice box visible
        await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
                                            {'tid': self.tournament_id, 'type': 'draw.dice_visibility',
                                             'visible': True})

        await self.alert(_("Draw started!"), 'success')

        # Update user interface
        await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
                                            {'tid': self.tournament_id, 'type': 'draw.start', 'fmt': fmt, 'draw': draw})
        await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
                                            {'tid': self.tournament_id, 'type': 'draw.set_info',
                                             'info': await self.tournament.draw.ainformation()})
        await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
                                            {'tid': self.tournament_id, 'type': 'draw.set_active', 'round': 1})

        # Send notification to everyone
        await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
                                            {'tid': self.tournament_id, 'type': 'draw.notify',
                                             'title': 'Tirage au sort du TFJM²',
                                             'body': "Le tirage au sort du tournoi de "
                                                     f"{self.tournament.name} a commencé !"})

    async def draw_start(self, content) -> None:
        """
        Send information to users that the draw has started.
        """
        await self.alert(_("The draw for the tournament {tournament} will start.")
                         .format(tournament=self.tournament.name), 'warning')
        await self.send_json({'tid': content['tid'], 'type': 'draw_start', 'fmt': content['fmt'],
                              'trigrams': [p.team.trigram for p in self.participations]})

    @ensure_orga
    async def abort(self, **kwargs) -> None:
        """
        Abort the current draw and delete all associated information.
        """
        if not await Draw.objects.filter(tournament=self.tournament).aexists():
            return await self.alert(_("The draw has not started yet."), 'danger')

        # Delete draw
        # All associated data will be deleted by cascade
        await self.tournament.draw.adelete()
        # Send information to all users
        await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
                                            {'tid': self.tournament_id, 'type': 'draw_abort'})

    async def draw_abort(self, content) -> None:
        """
        Send information to users that the draw was aborted.
        """
        await self.alert(_("The draw for the tournament {tournament} is aborted.")
                         .format(tournament=self.tournament.name), 'danger')
        await self.send_json({'tid': content['tid'], 'type': 'abort'})

    async def process_dice(self, trigram: str | None = None, **kwargs):
        """
        Launch the dice for a team.
        If we are in the first step, that determine the passage order and the pools of each team.
        For the second step, that determines the order of the teams to draw problems.
        :param trigram: The team that we want to force the launch. None if we launch for our team, or for the
                        first free team in the case of volunteers.
        """
        if not await Draw.objects.filter(tournament=self.tournament).aexists():
            return await self.alert(_("The draw has not started yet."), 'danger')

        state = self.tournament.draw.get_state()

        if self.registration.is_volunteer:
            # A volunteer can either force the launch for a specific team,
            # or launch for the first team that has not launched its dice.
            if trigram:
                participation = await Participation.objects.filter(team__trigram=trigram)\
                    .prefetch_related('team').aget()
            else:
                # First free team
                if state == 'DICE_ORDER_POULE':
                    participation = await Participation.objects\
                        .filter(teamdraw__pool=self.tournament.draw.current_round.current_pool,
                                teamdraw__choice_dice__isnull=True).prefetch_related('team').afirst()
                else:
                    participation = await Participation.objects\
                        .filter(teamdraw__round=self.tournament.draw.current_round,
                                teamdraw__passage_dice__isnull=True).prefetch_related('team').afirst()
        else:
            # Fetch the participation of the current user
            participation = await Participation.objects.filter(team__participants=self.registration)\
                .prefetch_related('team').aget()

        if participation is None:
            # Should not happen in normal cases
            return await self.alert(_("This is not the time for this."), 'danger')

        trigram = participation.team.trigram

        team_draw = await TeamDraw.objects.filter(participation=participation,
                                                  round_id=self.tournament.draw.current_round_id).aget()

        # Ensure that this is the right state to launch a dice and that the team didn't already launch the dice
        # and that it can launch a dice yet.
        # Prevent some async issues
        match state:
            case 'DICE_SELECT_POULES':
                if team_draw.passage_dice is not None:
                    return await self.alert(_("You've already launched the dice."), 'danger')
            case 'DICE_ORDER_POULE':
                if team_draw.choice_dice is not None:
                    return await self.alert(_("You've already launched the dice."), 'danger')
                if not await self.tournament.draw.current_round.current_pool.teamdraw_set\
                        .filter(participation=participation).aexists():
                    return await self.alert(_("It is not your turn."), 'danger')
            case _:
                return await self.alert(_("This is not the time for this."), 'danger')

        # Launch the dice and get the result
        res = randint(1, 100)
        if self.registration.is_admin and 'result' in kwargs \
                and isinstance(kwargs['result'], int) and (1 <= kwargs['result'] <= 100):
            # Admins can force the result
            res = int(kwargs['result'])
        if state == 'DICE_SELECT_POULES':
            team_draw.passage_dice = res
        else:
            team_draw.choice_dice = res
        await team_draw.asave()

        # Send the dice result to all users
        await self.channel_layer.group_send(
            f"tournament-{self.tournament.id}", {'tid': self.tournament_id, 'type': 'draw.dice',
                                                 'team': trigram, 'result': res})

        if state == 'DICE_SELECT_POULES' and \
                not await TeamDraw.objects.filter(round_id=self.tournament.draw.current_round_id,
                                                  passage_dice__isnull=True).aexists():
            # Check duplicates
            if await self.check_duplicate_dices():
                return
            # All teams launched their dice, we can process the result
            await self.process_dice_select_poules()
        elif state == 'DICE_ORDER_POULE' and \
                not await TeamDraw.objects.filter(pool=self.tournament.draw.current_round.current_pool,
                                                  choice_dice__isnull=True).aexists():
            # Check duplicates
            if await self.check_duplicate_dices():
                return
            # All teams launched their dice for the choice order, we can process the result
            await self.process_dice_order_poule()

    async def check_duplicate_dices(self) -> bool:
        """
        Check that all dices are distinct, and reset some dices if necessary.
        :return: True if there are duplicate dices, False otherwise.
        """
        state = self.tournament.draw.get_state()

        # Get concerned TeamDraw objects
        if state == 'DICE_SELECT_POULES':
            tds = [td async for td in TeamDraw.objects.filter(round_id=self.tournament.draw.current_round_id)
                   .prefetch_related('participation__team')]
            dices = {td: td.passage_dice for td in tds}
        else:
            tds = [td async for td in TeamDraw.objects
                   .filter(pool_id=self.tournament.draw.current_round.current_pool_id)
                   .prefetch_related('participation__team')]
            dices = {td: td.choice_dice for td in tds}

        values = list(dices.values())
        error = False
        for v in set(values):
            if values.count(v) > 1:
                # v is a duplicate value
                # Get all teams that have the same result
                dups = [td for td in tds if (td.passage_dice if state == 'DICE_SELECT_POULES' else td.choice_dice) == v]

                for dup in dups:
                    # Reset the dice
                    if state == 'DICE_SELECT_POULES':
                        dup.passage_dice = None
                    else:
                        dup.choice_dice = None
                    await dup.asave()
                    await self.channel_layer.group_send(
                        f"tournament-{self.tournament.id}",
                        {'tid': self.tournament_id, 'type': 'draw.dice',
                         'team': dup.participation.team.trigram, 'result': None})

                    # Send notification to concerned teams
                    await self.channel_layer.group_send(
                        f"team-{dup.participation.team.trigram}",
                        {'tid': self.tournament_id, 'type': 'draw.notify', 'title': 'Tirage au sort du TFJM²',
                         'body': 'Votre score de dé est identique à celui de une ou plusieurs équipes. '
                                 'Veuillez le relancer.'}
                    )
                # Alert the tournament
                await self.channel_layer.group_send(
                    f"tournament-{self.tournament.id}",
                    {'tid': self.tournament_id, 'type': 'draw.alert',
                     'message': _('Dices from teams {teams} are identical. Please relaunch your dices.').format(
                         teams=', '.join(td.participation.team.trigram for td in dups)),
                     'alert_type': 'warning'})
                error = True

        return error

    async def process_dice_select_poules(self):
        """
        Called when all teams launched their dice.
        Place teams into pools and order their passage.
        """
        r = self.tournament.draw.current_round
        tds = [td async for td in TeamDraw.objects.filter(round=r).prefetch_related('participation__team')]
        # Sort teams per dice results
        tds.sort(key=lambda td: td.passage_dice)
        tds_copy = tds.copy()

        # For each pool of size N, put the N next teams into this pool
        async for p in Pool.objects.filter(round_id=self.tournament.draw.current_round_id).order_by('letter').all():
            # Fetch the N teams
            pool_tds = tds_copy[:p.size].copy()
            # Remove the head
            tds_copy = tds_copy[p.size:]
            for i, td in enumerate(pool_tds):
                # Set the pool and the passage index for each team of the pool
                td.pool = p
                td.passage_index = i
                await td.asave()

        # The passages of the second round are determined from the order of the passages of the first round.
        # We order teams by increasing passage index, and then by decreasing pool number.
        # We keep teams that were at the last position in a 5-teams pool apart, as "jokers".
        # Then, we fill pools one team by one team.
        # As we fill one pool for the second round, we check if we can place a joker in it.
        # We can add a joker team if there is not already a team in the pool that was in the same pool
        # in the first round, and such that the number of such jokers is exactly the free space of the current pool.
        # Exception: if there is one only pool with 5 teams, we exchange the first and the last teams of the pool.
        if not self.tournament.final:
            tds_copy = sorted(tds, key=lambda td: (td.passage_index, -td.pool.letter,))
            jokers = [td for td in tds if td.passage_index == 4]
            round2 = await self.tournament.draw.round_set.filter(number=2).aget()
            round2_pools = [p async for p in Pool.objects.filter(round__draw__tournament=self.tournament, round=round2)
                            .order_by('letter').all()]
            current_pool_id, current_passage_index = 0, 0
            for i, td in enumerate(tds_copy):
                td2 = await TeamDraw.objects.filter(participation=td.participation, round=round2).aget()
                td2.pool = round2_pools[current_pool_id]
                td2.passage_index = current_passage_index
                if len(round2_pools) == 1:
                    # Exchange first and last team if there is only one pool
                    if i == 0 or i == len(tds) - 1:
                        td2.passage_index = len(tds) - 1 - i
                current_passage_index += 1
                await td2.asave()

                valid_jokers = []
                # A joker is valid if it was not in the same pool in the first round
                # as a team that is already in the current pool in the second round
                for joker in jokers:
                    async for td2 in round2_pools[current_pool_id].teamdraw_set.all():
                        if await joker.pool.teamdraw_set.filter(participation_id=td2.participation_id).aexists():
                            break
                    else:
                        valid_jokers.append(joker)

                # We can add a joker if there is exactly enough free space in the current pool
                if valid_jokers and current_passage_index + len(valid_jokers) == td2.pool.size:
                    for joker in valid_jokers:
                        tds_copy.remove(joker)
                        jokers.remove(joker)
                        td2_joker = await TeamDraw.objects.filter(participation_id=joker.participation_id,
                                                                  round=round2).aget()
                        td2_joker.pool = round2_pools[current_pool_id]
                        td2_joker.passage_index = current_passage_index
                        current_passage_index += 1
                        await td2_joker.asave()
                    jokers = []

                    current_passage_index = 0
                    current_pool_id += 1

                if current_passage_index == round2_pools[current_pool_id].size:
                    current_passage_index = 0
                    current_pool_id += 1

        # The current pool is the first pool of the current (first) round
        pool = await Pool.objects.filter(round=self.tournament.draw.current_round, letter=1).aget()
        self.tournament.draw.current_round.current_pool = pool
        await self.tournament.draw.current_round.asave()

        # Display dice result in the header of the information alert
        msg = "Les résultats des dés sont les suivants : "
        msg += ", ".join(f"<strong>{td.participation.team.trigram}</strong> ({td.passage_dice})" for td in tds)
        msg += ". L'ordre de passage et les compositions des différentes poules sont affiché⋅es sur le côté. "
        msg += "Les ordres de passage pour le premier tour sont déterminés à partir des scores des dés, "
        msg += "dans l'ordre croissant. Pour le deuxième tour, les ordres de passage sont déterminés à partir "
        msg += "des ordres de passage du premier tour."
        self.tournament.draw.last_message = msg
        await self.tournament.draw.asave()

        # Reset team dices
        for td in tds:
            await self.channel_layer.group_send(
                f"tournament-{self.tournament.id}",
                {'tid': self.tournament_id, 'type': 'draw.dice', 'team': td.participation.team.trigram, 'result': None})

        # Hide dice interface
        await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
                                            {'tid': self.tournament_id, 'type': 'draw.dice_visibility',
                                             'visible': False})

        # Display dice interface only for the teams in the first pool, and for volunteers
        async for td in pool.teamdraw_set.prefetch_related('participation__team').all():
            await self.channel_layer.group_send(f"team-{td.participation.team.trigram}",
                                                {'tid': self.tournament_id, 'type': 'draw.dice_visibility',
                                                 'visible': True})
        await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
                                            {'tid': self.tournament_id, 'type': 'draw.dice_visibility',
                                             'visible': True})

        # First send the second pool to have the good team order
        r2 = await self.tournament.draw.round_set.filter(number=2).aget()
        await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
                                            {'tid': self.tournament_id, 'type': 'draw.send_poules',
                                             'round': r2.number,
                                             'poules': [
                                                 {
                                                     'letter': pool.get_letter_display(),
                                                     'teams': await pool.atrigrams(),
                                                 }
                                                 async for pool in r2.pool_set.order_by('letter').all()
                                             ]})
        await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
                                            {'tid': self.tournament_id, 'type': 'draw.send_poules',
                                             'round': r.number,
                                             'poules': [
                                                 {
                                                     'letter': pool.get_letter_display(),
                                                     'teams': await pool.atrigrams(),
                                                 }
                                                 async for pool in r.pool_set.order_by('letter').all()
                                             ]})

        # Update information header and the active team on the recap menu
        await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
                                            {'tid': self.tournament_id, 'type': 'draw.set_info',
                                             'info': await self.tournament.draw.ainformation()})
        await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
                                            {'tid': self.tournament_id, 'type': 'draw.set_active',
                                             'round': r.number,
                                             'pool': pool.get_letter_display()})

    async def process_dice_order_poule(self):
        """
        Called when all teams of the current launched their dice to determine the choice order.
        Place teams into pools and order their passage.
        """
        r = self.tournament.draw.current_round
        pool = r.current_pool

        tds = [td async for td in TeamDraw.objects.filter(pool=pool).prefetch_related('participation__team')]
        # Order teams by decreasing dice score
        tds.sort(key=lambda x: -x.choice_dice)
        for i, td in enumerate(tds):
            td.choose_index = i
            await td.asave()

        # The first team to draw its problem is the team that has the highest dice score
        pool.current_team = tds[0]
        await pool.asave()

        self.tournament.draw.last_message = ""
        await self.tournament.draw.asave()

        # Update information header
        await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
                                            {'tid': self.tournament_id, 'type': 'draw.set_info',
                                             'info': await self.tournament.draw.ainformation()})
        await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
                                            {'tid': self.tournament_id, 'type': 'draw.set_active',
                                             'round': r.number,
                                             'pool': pool.get_letter_display(),
                                             'team': pool.current_team.participation.team.trigram})

        # Hide dice button to everyone
        await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
                                            {'tid': self.tournament_id, 'type': 'draw.dice_visibility',
                                             'visible': False})

        # Display the box button to the first team and to volunteers
        trigram = pool.current_team.participation.team.trigram
        await self.channel_layer.group_send(f"team-{trigram}",
                                            {'tid': self.tournament_id, 'type': 'draw.box_visibility', 'visible': True})
        await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
                                            {'tid': self.tournament_id, 'type': 'draw.box_visibility', 'visible': True})

        # Notify the team that it can draw a problem
        await self.channel_layer.group_send(f"team-{tds[0].participation.team.trigram}",
                                            {'tid': self.tournament_id, 'type': 'draw.notify',
                                             'title': "À votre tour !",
                                             'body': "C'est à vous de tirer un nouveau problème !"})

    async def select_problem(self, **kwargs):
        """
        Called when a team draws a problem.
        We choose randomly a problem that is available and propose it to the current team.
        """
        state = self.tournament.draw.get_state()

        if state != 'WAITING_DRAW_PROBLEM':
            return await self.alert(_("This is not the time for this."), 'danger')

        pool = self.tournament.draw.current_round.current_pool
        td = pool.current_team

        if not self.registration.is_volunteer:
            participation = await Participation.objects.filter(team__participants=self.registration)\
                .prefetch_related('team').aget()
            # Ensure that the user can draws a problem at this time
            if participation.id != td.participation_id:
                return await self.alert("This is not your turn.", 'danger')

        while True:
            # Choose a random problem
            problem = randint(1, len(settings.PROBLEMS))
            if self.registration.is_admin and 'problem' in kwargs \
                    and isinstance(kwargs['problem'], int) and (1 <= kwargs['problem'] <= len(settings.PROBLEMS)):
                # Admins can force the draw
                problem = int(kwargs['problem'])
                break

            # Check that the user didn't already accept this problem for the first round
            # if this is the second round
            if await TeamDraw.objects.filter(participation_id=td.participation_id,
                                             round__draw__tournament=self.tournament,
                                             round__number=1,
                                             accepted=problem).aexists():
                continue
            # Check that the problem is not already chosen once (or twice for a 5-teams pool)
            if await pool.teamdraw_set.filter(accepted=problem).acount() < (2 if pool.size == 5 else 1):
                break

        td.purposed = problem
        await td.asave()

        # Update interface
        trigram = td.participation.team.trigram
        await self.channel_layer.group_send(f"team-{trigram}",
                                            {'tid': self.tournament_id, 'type': 'draw.box_visibility',
                                             'visible': False})
        await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
                                            {'tid': self.tournament_id, 'type': 'draw.box_visibility',
                                             'visible': False})
        await self.channel_layer.group_send(f"team-{trigram}",
                                            {'tid': self.tournament_id, 'type': 'draw.buttons_visibility',
                                             'visible': True})
        await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
                                            {'tid': self.tournament_id, 'type': 'draw.buttons_visibility',
                                             'visible': True})
        await self.channel_layer.group_send(f"team-{self.tournament.id}",
                                            {'tid': self.tournament_id, 'type': 'draw.draw_problem', 'team': trigram,
                                             'problem': problem})

        self.tournament.draw.last_message = ""
        await self.tournament.draw.asave()
        await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
                                            {'tid': self.tournament_id, 'type': 'draw.set_info',
                                             'info': await self.tournament.draw.ainformation()})

    async def accept_problem(self, **kwargs):
        """
        Called when a team accepts a problem.
        We pass to the next team is there is one, or to the next pool, or the next round, or end the draw.
        """

        if not await Draw.objects.filter(tournament=self.tournament).aexists():
            return await self.alert(_("The draw has not started yet."), 'danger')

        state = self.tournament.draw.get_state()

        if state != 'WAITING_CHOOSE_PROBLEM':
            return await self.alert(_("This is not the time for this."), 'danger')

        r = self.tournament.draw.current_round
        pool = r.current_pool
        td = pool.current_team
        if not self.registration.is_volunteer:
            participation = await Participation.objects.filter(team__participants=self.registration)\
                .prefetch_related('team').aget()
            # Ensure that the user can accept a problem at this time
            if participation.id != td.participation_id:
                return await self.alert("This is not your turn.", 'danger')

        td.accepted = td.purposed
        td.purposed = None
        await td.asave()

        trigram = td.participation.team.trigram
        msg = f"L'équipe <strong>{trigram}</strong> a accepté le problème <strong>{td.accepted} : " \
              f"{settings.PROBLEMS[td.accepted - 1]}</strong>. "
        if pool.size == 5 and await pool.teamdraw_set.filter(accepted=td.accepted).acount() < 2:
            msg += "Une équipe peut encore l'accepter."
        else:
            msg += "Plus personne ne peut l'accepter."
        self.tournament.draw.last_message = msg
        await self.tournament.draw.asave()

        # Send the accepted problem to the users
        await self.channel_layer.group_send(f"team-{trigram}",
                                            {'tid': self.tournament_id, 'type': 'draw.buttons_visibility',
                                             'visible': False})
        await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
                                            {'tid': self.tournament_id, 'type': 'draw.buttons_visibility',
                                             'visible': False})
        await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
                                            {'tid': self.tournament_id, 'type': 'draw.set_problem',
                                             'round': r.number,
                                             'team': trigram,
                                             'problem': td.accepted})

        if await pool.teamdraw_set.filter(accepted__isnull=True).aexists():
            # Continue this pool since there is at least one team that does not have selected its problem
            # Get next team
            next_td = await pool.next_td()
            pool.current_team = next_td
            await pool.asave()

            new_trigram = next_td.participation.team.trigram
            await self.channel_layer.group_send(f"team-{new_trigram}",
                                                {'tid': self.tournament_id, 'type': 'draw.box_visibility',
                                                 'visible': True})
            await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
                                                {'tid': self.tournament_id, 'type': 'draw.box_visibility',
                                                 'visible': True})

            # Notify the team that it can draw a problem
            await self.channel_layer.group_send(f"team-{new_trigram}",
                                                {'tid': self.tournament_id, 'type': 'draw.notify',
                                                 'title': "À votre tour !",
                                                 'body': "C'est à vous de tirer un nouveau problème !"})
        else:
            # Pool is ended
            await self.end_pool(pool)
            r = self.tournament.draw.current_round
            pool = r.current_pool

        await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
                                            {'tid': self.tournament_id, 'type': 'draw.set_info',
                                             'info': await self.tournament.draw.ainformation()})
        await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
                                            {'tid': self.tournament_id, 'type': 'draw.set_active',
                                             'round': r.number,
                                             'pool': pool.get_letter_display(),
                                             'team': pool.current_team.participation.team.trigram
                                             if pool.current_team else None})

    async def end_pool(self, pool: Pool) -> None:
        """
        End the pool, and pass to the next one, or to the next round, or end the draw.
        :param pool: The pool to end.
        """
        msg = self.tournament.draw.last_message
        r = self.tournament.draw.current_round

        if pool.size == 5:
            # Maybe reorder teams if the same problem is presented twice
            problems = OrderedDict()
            async for td in pool.team_draws:
                problems.setdefault(td.accepted, [])
                problems[td.accepted].append(td)
            p_index = 0
            for pb, tds in problems.items():
                if len(tds) == 2:
                    # Le règlement demande à ce que l'ordre soit tiré au sort
                    shuffle(tds)
                    tds[0].passage_index = p_index
                    tds[1].passage_index = p_index + 1
                    p_index += 2
                    await tds[0].asave()
                    await tds[1].asave()
            for pb, tds in problems.items():
                if len(tds) == 1:
                    tds[0].passage_index = p_index
                    p_index += 1
                    await tds[0].asave()

            # Send the reordered pool
            await self.channel_layer.group_send(f"tournament-{self.tournament.id}", {
                'tid': self.tournament_id,
                'type': 'draw.reorder_pool',
                'round': r.number,
                'pool': pool.get_letter_display(),
                'teams': [td.participation.team.trigram
                          async for td in pool.team_draws.prefetch_related('participation__team')],
                'problems': [td.accepted async for td in pool.team_draws],
            })

        msg += f"<br><br>Le tirage de la poule {pool.get_letter_display()}{r.number} est terminé. " \
               f"Le tableau récapitulatif est en bas."
        self.tournament.draw.last_message = msg
        await self.tournament.draw.asave()

        if await r.teamdraw_set.filter(accepted__isnull=True).aexists():
            # There is a pool that does not have selected its problem, so we continue to the next pool
            next_pool = await r.next_pool()
            r.current_pool = next_pool
            await r.asave()

            async for td in next_pool.team_draws.prefetch_related('participation__team').all():
                await self.channel_layer.group_send(f"team-{td.participation.team.trigram}",
                                                    {'tid': self.tournament_id, 'type': 'draw.dice_visibility',
                                                     'visible': True})
                # Notify the team that it can draw a dice
                await self.channel_layer.group_send(f"team-{td.participation.team.trigram}",
                                                    {'tid': self.tournament_id, 'type': 'draw.notify',
                                                     'title': "À votre tour !",
                                                     'body': "C'est à vous de lancer le dé !"})

            await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
                                                {'tid': self.tournament_id, 'type': 'draw.dice_visibility',
                                                 'visible': True})
        else:
            # Round is ended
            await self.end_round(r)

    async def end_round(self, r: Round) -> None:
        """
        End the round, and pass to the next one, or end the draw.
        :param r: The current round.
        """
        msg = self.tournament.draw.last_message

        if r.number == 1 and not self.tournament.final:
            # Next round
            r2 = await self.tournament.draw.round_set.filter(number=2).aget()
            self.tournament.draw.current_round = r2
            msg += "<br><br>Le tirage au sort du tour 1 est terminé."
            self.tournament.draw.last_message = msg
            await self.tournament.draw.asave()

            for participation in self.participations:
                await self.channel_layer.group_send(
                    f"tournament-{self.tournament.id}",
                    {'tid': self.tournament_id, 'type': 'draw.dice',
                     'team': participation.team.trigram, 'result': None})

                # Notify the team that it can draw a dice
                await self.channel_layer.group_send(f"team-{participation.team.trigram}",
                                                    {'tid': self.tournament_id, 'type': 'draw.notify',
                                                     'title': "À votre tour !",
                                                     'body': "C'est à vous de lancer le dé !"})

            # Reorder dices
            await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
                                                {'tid': self.tournament_id, 'type': 'draw.send_poules',
                                                 'round': r2.number,
                                                 'poules': [
                                                     {
                                                         'letter': pool.get_letter_display(),
                                                         'teams': await pool.atrigrams(),
                                                     }
                                                     async for pool in r2.pool_set.order_by('letter').all()
                                                 ]})

            # The passage order for the second round is already determined by the first round
            # Start the first pool of the second round
            p1: Pool = await r2.pool_set.filter(letter=1).aget()
            r2.current_pool = p1
            await r2.asave()

            async for td in p1.teamdraw_set.prefetch_related('participation__team').all():
                await self.channel_layer.group_send(f"team-{td.participation.team.trigram}",
                                                    {'tid': self.tournament_id, 'type': 'draw.dice_visibility',
                                                     'visible': True})
            await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
                                                {'tid': self.tournament_id, 'type': 'draw.dice_visibility',
                                                 'visible': True})
        elif r.number == 1 and self.tournament.final:
            # For the final tournament, we wait for a manual update between the two rounds.
            msg += "<br><br>Le tirage au sort du tour 1 est terminé."
            self.tournament.draw.last_message = msg
            await self.tournament.draw.asave()

        await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
                                            {'tid': self.tournament_id, 'type': 'draw.export_visibility',
                                             'visible': True})

    async def reject_problem(self, **kwargs):
        """
        Called when a team accepts a problem.
        We pass then to the next team.
        """
        if not await Draw.objects.filter(tournament=self.tournament).aexists():
            return await self.alert(_("The draw has not started yet."), 'danger')

        state = self.tournament.draw.get_state()

        if state != 'WAITING_CHOOSE_PROBLEM':
            return await self.alert(_("This is not the time for this."), 'danger')

        r = self.tournament.draw.current_round
        pool = r.current_pool
        td = pool.current_team
        if not self.registration.is_volunteer:
            participation = await Participation.objects.filter(team__participants=self.registration)\
                .prefetch_related('team').aget()
            # Ensure that the user can reject a problem at this time
            if participation.id != td.participation_id:
                return await self.alert("This is not your turn.", 'danger')

        # Add the problem to the rejected problems list
        problem = td.purposed
        already_refused = problem in td.rejected
        if not already_refused:
            td.rejected.append(problem)
        td.purposed = None
        await td.asave()

        remaining = len(settings.PROBLEMS) - 5 - len(td.rejected)

        # Update messages
        trigram = td.participation.team.trigram
        msg = f"L'équipe <strong>{trigram}</strong> a refusé le problème <strong>{problem} : " \
              f"{settings.PROBLEMS[problem - 1]}</strong>. "
        if remaining >= 0:
            msg += f"Il lui reste {remaining} refus sans pénalité."
        else:
            if already_refused:
                msg += "Cela n'ajoute pas de pénalité."
            else:
                msg += "Cela ajoute une pénalité de 25&nbsp;% sur le coefficient de l'oral de la défense."
        self.tournament.draw.last_message = msg
        await self.tournament.draw.asave()

        # Update interface
        await self.channel_layer.group_send(f"team-{trigram}",
                                            {'tid': self.tournament_id, 'type': 'draw.buttons_visibility',
                                             'visible': False})
        await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
                                            {'tid': self.tournament_id, 'type': 'draw.buttons_visibility',
                                             'visible': False})
        await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
                                            {'tid': self.tournament_id, 'type': 'draw.reject_problem',
                                             'round': r.number, 'team': trigram, 'rejected': td.rejected})

        if already_refused:
            # The team already refused this problem, and can immediately draw a new one
            next_td = td
        else:
            # We pass to the next team
            next_td = await pool.next_td()

        pool.current_team = next_td
        await pool.asave()

        new_trigram = next_td.participation.team.trigram
        await self.channel_layer.group_send(f"team-{new_trigram}",
                                            {'tid': self.tournament_id, 'type': 'draw.box_visibility', 'visible': True})
        await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
                                            {'tid': self.tournament_id, 'type': 'draw.box_visibility', 'visible': True})

        await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
                                            {'tid': self.tournament_id, 'type': 'draw.set_info',
                                             'info': await self.tournament.draw.ainformation()})
        await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
                                            {'tid': self.tournament_id, 'type': 'draw.set_active',
                                             'round': r.number,
                                             'pool': pool.get_letter_display(),
                                             'team': new_trigram})

        # Notify the team that it can draw a problem
        await self.channel_layer.group_send(f"team-{new_trigram}",
                                            {'tid': self.tournament_id, 'type': 'draw.notify',
                                             'title': "À votre tour !",
                                             'body': "C'est à vous de tirer un nouveau problème !"})

    @ensure_orga
    async def export(self, **kwargs):
        """
        Exports the draw information in the participation app, for the solutions and notes management
        """
        if not await Draw.objects.filter(tournament=self.tournament).aexists():
            return await self.alert(_("The draw has not started yet."), 'danger')

        await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
                                            {'tid': self.tournament_id, 'type': 'draw.export_visibility',
                                             'visible': False})

        # Export each exportable pool
        async for r in self.tournament.draw.round_set.all():
            async for pool in r.pool_set.all():
                if await pool.is_exportable():
                    await pool.export()

        # Update Google Sheets final sheet
        if os.getenv('GOOGLE_PRIVATE_KEY_ID', None):
            await sync_to_async(self.tournament.update_ranking_spreadsheet)()

    @ensure_orga
    async def continue_final(self, **kwargs):
        """
        For the final tournament, continue the draw for the second round
        """
        if not await Draw.objects.filter(tournament=self.tournament).aexists():
            return await self.alert(_("The draw has not started yet."), 'danger')

        if not self.tournament.final:
            return await self.alert(_("This is only available for the final tournament."), 'danger')

        r2 = await self.tournament.draw.round_set.filter(number=2).aget()
        self.tournament.draw.current_round = r2
        msg = "Le tirage au sort pour le tour 2 va commencer. " \
              "L'ordre de passage est déterminé à partir du classement du premier tour, " \
              "de sorte à mélanger les équipes entre les deux jours."
        self.tournament.draw.last_message = msg
        await self.tournament.draw.asave()

        # Send notification to everyone
        await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
                                            {'tid': self.tournament_id, 'type': 'draw.notify',
                                             'title': 'Tirage au sort du TFJM²',
                                             'body': "Le tirage au sort pour le second tour de la finale a commencé !"})

        # Set the first pool of the second round as the active pool
        pool = await Pool.objects.filter(round=self.tournament.draw.current_round, letter=1).aget()
        r2.current_pool = pool
        await r2.asave()

        # Fetch notes from the first round
        notes = dict()
        async for participation in self.tournament.participations.filter(valid=True).prefetch_related('team').all():
            notes[participation] = sum([await pool.aaverage(participation)
                                        async for pool in self.tournament.pools.filter(participations=participation)
                                        .prefetch_related('passages')])
        # Sort notes in a decreasing order
        ordered_participations = sorted(notes.keys(), key=lambda x: -notes[x])
        # Define pools and passage orders from the ranking of the first round
        async for pool in r2.pool_set.order_by('letter').all():
            for i in range(pool.size):
                participation = ordered_participations.pop(0)
                td = await TeamDraw.objects.aget(round=r2, participation=participation)
                td.pool = pool
                td.passage_index = i
                await td.asave()

        # Send pools to users
        await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
                                            {'tid': self.tournament_id, 'type': 'draw.send_poules',
                                             'round': r2.number,
                                             'poules': [
                                                 {
                                                     'letter': pool.get_letter_display(),
                                                     'teams': await pool.atrigrams(),
                                                 }
                                                 async for pool in r2.pool_set.order_by('letter').all()
                                             ]})

        # Reset dices and update interface
        for participation in self.participations:
            await self.channel_layer.group_send(
                f"tournament-{self.tournament.id}",
                {'tid': self.tournament_id, 'type': 'draw.dice', 'team': participation.team.trigram, 'result': None})

        async for td in r2.current_pool.team_draws.prefetch_related('participation__team'):
            await self.channel_layer.group_send(f"team-{td.participation.team.trigram}",
                                                {'tid': self.tournament_id, 'type': 'draw.dice_visibility',
                                                 'visible': True})

            # Notify the team that it can draw a problem
            await self.channel_layer.group_send(f"team-{td.participation.team.trigram}",
                                                {'tid': self.tournament_id, 'type': 'draw.notify',
                                                 'title': "À votre tour !",
                                                 'body': "C'est à vous de tirer un nouveau problème !"})

        await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
                                            {'tid': self.tournament_id, 'type': 'draw.dice_visibility',
                                             'visible': True})
        await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
                                            {'tid': self.tournament_id, 'type': 'draw.continue_visibility',
                                             'visible': False})

        await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
                                            {'tid': self.tournament_id, 'type': 'draw.set_info',
                                             'info': await self.tournament.draw.ainformation()})
        await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
                                            {'tid': self.tournament_id, 'type': 'draw.set_active',
                                             'round': r2.number,
                                             'pool': r2.current_pool.get_letter_display()})

    @ensure_orga
    async def cancel_last_step(self, **kwargs):
        """
        Cancel the last step of the draw.
        """
        if not await Draw.objects.filter(tournament=self.tournament).aexists():
            return await self.alert(_("The draw has not started yet."), 'danger')

        state = self.tournament.draw.get_state()

        self.tournament.draw.last_message = ""
        await self.tournament.draw.asave()

        if state == 'DRAW_ENDED' or state == 'WAITING_FINAL':
            await self.undo_end_draw()
        elif state == 'WAITING_CHOOSE_PROBLEM':
            await self.undo_draw_problem()
        elif state == 'WAITING_DRAW_PROBLEM':
            await self.undo_process_problem()
        elif state == 'DICE_ORDER_POULE':
            await self.undo_pool_dice()
        elif state == 'DICE_SELECT_POULES':
            await self.undo_order_dice()

        await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
                                            {'tid': self.tournament_id, 'type': 'draw.set_info',
                                             'info': await self.tournament.draw.ainformation()})
        r = self.tournament.draw.current_round
        p = r.current_pool
        await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
                                            {'tid': self.tournament_id, 'type': 'draw.set_active',
                                             'round': r.number,
                                             'pool': p.get_letter_display() if p else None,
                                             'team': p.current_team.participation.team.trigram
                                             if p and p.current_team else None})

    async def undo_end_draw(self) -> None:
        """
        If the draw is ended, or if we are between the two rounds of the final,
        then we cancel the last problem that was accepted.
        """
        r = self.tournament.draw.current_round
        td = r.current_pool.current_team
        td.purposed = td.accepted
        td.accepted = None
        await td.asave()

        await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
                                            {'tid': self.tournament_id, 'type': 'draw.continue_visibility',
                                             'visible': False})

        await self.channel_layer.group_send(f"team-{td.participation.team.trigram}",
                                            {'tid': self.tournament_id, 'type': 'draw.buttons_visibility',
                                             'visible': True})
        await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
                                            {'tid': self.tournament_id, 'type': 'draw.buttons_visibility',
                                             'visible': True})

        await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
                                            {'tid': self.tournament_id, 'type': 'draw.set_problem',
                                             'round': r.number,
                                             'team': td.participation.team.trigram,
                                             'problem': td.accepted})

    async def undo_draw_problem(self):
        """
        A problem was drawn and we wait for the current team to accept or reject the problem.
        Then, we just reset the problem draw.
        :return:
        """
        td = self.tournament.draw.current_round.current_pool.current_team
        td.purposed = None
        await td.asave()

        await self.channel_layer.group_send(f"team-{td.participation.team.trigram}",
                                            {'tid': self.tournament_id, 'type': 'draw.buttons_visibility',
                                             'visible': False})
        await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
                                            {'tid': self.tournament_id, 'type': 'draw.buttons_visibility',
                                             'visible': False})
        await self.channel_layer.group_send(f"team-{td.participation.team.trigram}",
                                            {'tid': self.tournament_id, 'type': 'draw.box_visibility', 'visible': True})
        await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
                                            {'tid': self.tournament_id, 'type': 'draw.box_visibility', 'visible': True})

    async def undo_process_problem(self):
        """
        Now, a team must draw a new problem. Multiple cases are possible:
        * In the same pool, a previous team accepted a problem ;
        * In the same pool, a previous team rejected a problem ;
        * The current team rejected a problem that was previously rejected ;
        * The last team drawn its dice to choose the draw order.

        In the two first cases, we explore the database history to fetch what team accepted or rejected
        its problem at last.
        The third case is ignored, because too hard and too useless to manage.
        For the last case, we cancel the last dice.
        """
        content_type = await ContentType.objects.aget(app_label=TeamDraw._meta.app_label,
                                                      model=TeamDraw._meta.model_name)

        r = self.tournament.draw.current_round
        p = r.current_pool
        accepted_tds = {td.id: td async for td in p.team_draws.filter(accepted__isnull=False)
                        .prefetch_related('participation__team')}
        has_rejected_one_tds = {td.id: td async for td in p.team_draws.exclude(rejected=[])
                                .prefetch_related('participation__team')}

        last_td = None

        if accepted_tds or has_rejected_one_tds:
            # One team of the already accepted or its problem, we fetch the last one
            changelogs = Changelog.objects.filter(
                model=content_type,
                action='edit',
                instance_pk__in=set(accepted_tds.keys()).union(set(has_rejected_one_tds.keys()))
            ).order_by('-timestamp')

            async for changelog in changelogs:
                previous = json.loads(changelog.previous)
                data = json.loads(changelog.data)
                pk = int(changelog.instance_pk)

                if 'accepted' in data and data['accepted'] and pk in accepted_tds:
                    # Undo the last acceptance
                    last_td = accepted_tds[pk]
                    last_td.purposed = last_td.accepted
                    last_td.accepted = None
                    await last_td.asave()

                    await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
                                                        {'tid': self.tournament_id, 'type': 'draw.set_problem',
                                                         'round': r.number,
                                                         'team': last_td.participation.team.trigram,
                                                         'problem': last_td.accepted})
                    break
                if 'rejected' in data and len(data['rejected']) > len(previous['rejected']) \
                        and pk in has_rejected_one_tds:
                    # Undo the last reject
                    last_td = has_rejected_one_tds[pk]
                    rejected_problem = set(data['rejected']).difference(previous['rejected']).pop()
                    if rejected_problem not in last_td.rejected:
                        # This is an old diff
                        continue
                    last_td.rejected.remove(rejected_problem)
                    last_td.purposed = rejected_problem
                    await last_td.asave()

                    await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
                                                        {'tid': self.tournament_id, 'type': 'draw.reject_problem',
                                                         'round': r.number,
                                                         'team': last_td.participation.team.trigram,
                                                         'rejected': last_td.rejected})
                    break

            r.current_pool.current_team = last_td
            await r.current_pool.asave()

            await self.channel_layer.group_send(f"team-{last_td.participation.team.trigram}",
                                                {'tid': self.tournament_id, 'type': 'draw.buttons_visibility',
                                                 'visible': True})
            await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
                                                {'tid': self.tournament_id, 'type': 'draw.buttons_visibility',
                                                 'visible': True})
        else:
            # Return to the dice choice
            pool_tds = {td.id: td async for td in p.team_draws.prefetch_related('participation__team')}
            changelogs = Changelog.objects.filter(
                model=content_type,
                action='edit',
                instance_pk__in=set(pool_tds.keys())
            ).order_by('-timestamp')

            # Find the last dice that was launched
            async for changelog in changelogs:
                data = json.loads(changelog.data)
                if 'choice_dice' in data and data['choice_dice']:
                    last_td = pool_tds[int(changelog.instance_pk)]
                    # Reset the dice
                    last_td.choice_dice = None
                    await last_td.asave()

                    # Reset the dice on the interface
                    await self.channel_layer.group_send(
                        f"tournament-{self.tournament.id}", {'tid': self.tournament_id, 'type': 'draw.dice',
                                                             'team': last_td.participation.team.trigram,
                                                             'result': None})
                    break

            p.current_team = None
            await p.asave()

            # Make dice box visible
            for td in pool_tds.values():
                await self.channel_layer.group_send(f"team-{td.participation.team.trigram}",
                                                    {'tid': self.tournament_id, 'type': 'draw.dice_visibility',
                                                     'visible': True})
            await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
                                                {'tid': self.tournament_id, 'type': 'draw.dice_visibility',
                                                 'visible': True})

        await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
                                            {'tid': self.tournament_id, 'type': 'draw.box_visibility',
                                             'visible': False})

    async def undo_pool_dice(self):
        """
        Teams of a pool are launching their dices to define the draw order.
        We reset the last dice if possible, or we go to the last pool, or the last round,
        or the passage dices.
        """
        content_type = await ContentType.objects.aget(app_label=TeamDraw._meta.app_label,
                                                      model=TeamDraw._meta.model_name)

        r = self.tournament.draw.current_round
        p = r.current_pool
        already_launched_tds = {td.id: td async for td in p.team_draws.filter(choice_dice__isnull=False)
                                .prefetch_related('participation__team')}

        if already_launched_tds:
            # Reset the last dice
            changelogs = Changelog.objects.filter(
                model=content_type,
                action='edit',
                instance_pk__in=set(already_launched_tds.keys())
            ).order_by('-timestamp')

            # Find the last dice that was launched
            async for changelog in changelogs:
                data = json.loads(changelog.data)
                if 'choice_dice' in data and data['choice_dice']:
                    last_td = already_launched_tds[int(changelog.instance_pk)]
                    # Reset the dice
                    last_td.choice_dice = None
                    await last_td.asave()

                    # Reset the dice on the interface
                    await self.channel_layer.group_send(
                        f"tournament-{self.tournament.id}", {'tid': self.tournament_id, 'type': 'draw.dice',
                                                             'team': last_td.participation.team.trigram,
                                                             'result': None})
                    break
        else:
            # Go to the previous pool if possible
            if p.letter > 1:
                # Go to the previous pool
                previous_pool = await r.pool_set.prefetch_related('current_team__participation__team') \
                    .aget(letter=p.letter - 1)
                r.current_pool = previous_pool
                await r.asave()

                td = previous_pool.current_team
                td.purposed = td.accepted
                td.accepted = None
                await td.asave()

                await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
                                                    {'tid': self.tournament_id, 'type': 'draw.dice_visibility',
                                                     'visible': False})

                await self.channel_layer.group_send(f"team-{td.participation.team.trigram}",
                                                    {'tid': self.tournament_id, 'type': 'draw.buttons_visibility',
                                                     'visible': True})
                await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
                                                    {'tid': self.tournament_id, 'type': 'draw.buttons_visibility',
                                                     'visible': True})

                await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
                                                    {'tid': self.tournament_id, 'type': 'draw.set_problem',
                                                     'round': r.number,
                                                     'team': td.participation.team.trigram,
                                                     'problem': td.accepted})
            elif r.number == 2:
                if not self.tournament.final:
                    # Go to the previous round
                    r1 = await self.tournament.draw.round_set \
                        .prefetch_related('current_pool__current_team__participation__team').aget(number=1)
                    self.tournament.draw.current_round = r1
                    await self.tournament.draw.asave()

                    async for td in r1.team_draws.prefetch_related('participation__team').all():
                        await self.channel_layer.group_send(
                            f"tournament-{self.tournament.id}", {'tid': self.tournament_id, 'type': 'draw.dice',
                                                                 'team': td.participation.team.trigram,
                                                                 'result': td.choice_dice})

                    await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
                                                        {'tid': self.tournament_id, 'type': 'draw.send_poules',
                                                         'round': r1.number,
                                                         'poules': [
                                                             {
                                                                 'letter': pool.get_letter_display(),
                                                                 'teams': await pool.atrigrams(),
                                                             }
                                                             async for pool in r1.pool_set.order_by('letter').all()
                                                         ]})

                    previous_pool = r1.current_pool

                    td = previous_pool.current_team
                    td.purposed = td.accepted
                    td.accepted = None
                    await td.asave()

                    await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
                                                        {'tid': self.tournament_id, 'type': 'draw.dice_visibility',
                                                         'visible': False})

                    await self.channel_layer.group_send(f"team-{td.participation.team.trigram}",
                                                        {'tid': self.tournament_id, 'type': 'draw.buttons_visibility',
                                                         'visible': True})
                    await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
                                                        {'tid': self.tournament_id, 'type': 'draw.buttons_visibility',
                                                         'visible': True})

                    await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
                                                        {'tid': self.tournament_id, 'type': 'draw.set_problem',
                                                         'round': r1.number,
                                                         'team': td.participation.team.trigram,
                                                         'problem': td.accepted})
                else:
                    # Don't continue the final tournament
                    r1 = await self.tournament.draw.round_set \
                        .prefetch_related('current_pool__current_team__participation__team').aget(number=1)
                    self.tournament.draw.current_round = r1
                    await self.tournament.draw.asave()

                    async for td in r.teamdraw_set.all():
                        td.pool = None
                        td.choose_index = None
                        td.choice_dice = None
                        await td.asave()

                    await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
                                                        {
                                                            'tid': self.tournament_id,
                                                            'type': 'draw.send_poules',
                                                            'round': 2, 'poules': [
                                                                {
                                                                    'letter': pool.get_letter_display(),
                                                                    'teams': [],
                                                                }
                                                                async for pool in r.pool_set.order_by('letter').all()
                                                            ]
                                                        })

                    async for td in r1.team_draws.prefetch_related('participation__team').all():
                        await self.channel_layer.group_send(
                            f"tournament-{self.tournament.id}", {'tid': self.tournament_id, 'type': 'draw.dice',
                                                                 'team': td.participation.team.trigram,
                                                                 'result': td.choice_dice})

                    await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
                                                        {'tid': self.tournament_id, 'type': 'draw.dice_visibility',
                                                         'visible': False})
                    await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
                                                        {'tid': self.tournament_id, 'type': 'draw.continue_visibility',
                                                         'visible': True})
            else:
                # Go to the dice order
                async for r0 in self.tournament.draw.round_set.all():
                    async for td in r0.teamdraw_set.all():
                        td.pool = None
                        td.passage_index = None
                        td.choose_index = None
                        td.choice_dice = None
                        await td.asave()

                r.current_pool = None
                await r.asave()

                round_tds = {td.id: td async for td in r.team_draws.prefetch_related('participation__team')}

                # Reset the last dice
                changelogs = Changelog.objects.filter(
                    model=content_type,
                    action='edit',
                    instance_pk__in=set(round_tds.keys())
                ).order_by('-timestamp')

                # Find the last dice that was launched
                async for changelog in changelogs:
                    data = json.loads(changelog.data)
                    if 'passage_dice' in data and data['passage_dice']:
                        last_td = round_tds[int(changelog.instance_pk)]
                        # Reset the dice
                        last_td.passage_dice = None
                        await last_td.asave()

                        # Reset the dice on the interface
                        await self.channel_layer.group_send(
                            f"tournament-{self.tournament.id}", {'tid': self.tournament_id, 'type': 'draw.dice',
                                                                 'team': last_td.participation.team.trigram,
                                                                 'result': None})
                        break

                async for td in r.team_draws.prefetch_related('participation__team').all():
                    await self.channel_layer.group_send(
                        f"tournament-{self.tournament.id}", {'tid': self.tournament_id, 'type': 'draw.dice',
                                                             'team': td.participation.team.trigram,
                                                             'result': td.passage_dice})

                await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
                                                    {'tid': self.tournament_id, 'type': 'draw.dice_visibility',
                                                     'visible': True})

    async def undo_order_dice(self):
        """
        Undo the last dice for the passage order, or abort the draw if we are at the beginning.
        """
        content_type = await ContentType.objects.aget(app_label=TeamDraw._meta.app_label,
                                                      model=TeamDraw._meta.model_name)

        r = self.tournament.draw.current_round
        already_launched_tds = {td.id: td async for td in r.team_draws.filter(passage_dice__isnull=False)
                                .prefetch_related('participation__team')}

        if already_launched_tds:
            # Reset the last dice
            changelogs = Changelog.objects.filter(
                model=content_type,
                action='edit',
                instance_pk__in=set(already_launched_tds.keys())
            ).order_by('-timestamp')

            # Find the last dice that was launched
            async for changelog in changelogs:
                data = json.loads(changelog.data)
                if 'passage_dice' in data and data['passage_dice']:
                    last_td = already_launched_tds[int(changelog.instance_pk)]
                    # Reset the dice
                    last_td.passage_dice = None
                    await last_td.asave()

                    # Reset the dice on the interface
                    await self.channel_layer.group_send(
                        f"tournament-{self.tournament.id}", {'tid': self.tournament_id, 'type': 'draw.dice',
                                                             'team': last_td.participation.team.trigram,
                                                             'result': None})
                    break
        else:
            await self.abort()

    async def draw_alert(self, content):
        """
        Send alert to the current user.
        """
        return await self.alert(**content)

    async def draw_notify(self, content):
        """
        Send a notification (with title and body) to the current user.
        """
        await self.send_json({'tid': content['tid'], 'type': 'notification',
                              'title': content['title'], 'body': content['body']})

    async def draw_set_info(self, content):
        """
        Set the information banner to the current user.
        """
        await self.send_json({'tid': content['tid'], 'type': 'set_info', 'information': content['info']})

    async def draw_dice(self, content):
        """
        Update the dice of a given team for the current user interface.
        """
        await self.send_json({'tid': content['tid'], 'type': 'dice',
                              'team': content['team'], 'result': content['result']})

    async def draw_dice_visibility(self, content):
        """
        Update the visibility of the dice button for the current user.
        """
        await self.send_json({'tid': content['tid'], 'type': 'dice_visibility', 'visible': content['visible']})

    async def draw_box_visibility(self, content):
        """
        Update the visibility of the box button for the current user.
        """
        await self.send_json({'tid': content['tid'], 'type': 'box_visibility', 'visible': content['visible']})

    async def draw_buttons_visibility(self, content):
        """
        Update the visibility of the accept/reject buttons for the current user.
        """
        await self.send_json({'tid': content['tid'], 'type': 'buttons_visibility', 'visible': content['visible']})

    async def draw_export_visibility(self, content):
        """
        Update the visibility of the export button for the current user.
        """
        await self.send_json({'tid': content['tid'], 'type': 'export_visibility', 'visible': content['visible']})

    async def draw_continue_visibility(self, content):
        """
        Update the visibility of the continue button for the current user.
        """
        await self.send_json({'tid': content['tid'], 'type': 'continue_visibility', 'visible': content['visible']})

    async def draw_send_poules(self, content):
        """
        Send the pools and the teams to the current user to update the interface.
        """
        await self.send_json({'tid': content['tid'], 'type': 'set_poules', 'round': content['round'],
                              'poules': content['poules']})

    async def draw_set_active(self, content):
        """
        Update the user interface to highlight the current team.
        """
        await self.send_json({
            'tid': content['tid'],
            'type': 'set_active',
            'round': content.get('round', None),
            'poule': content.get('pool', None),
            'team': content.get('team', None),
        })

    async def draw_set_problem(self, content):
        """
        Send the accepted problem of a team to the current user.
        """
        await self.send_json({'tid': content['tid'], 'type': 'set_problem', 'round': content['round'],
                              'team': content['team'], 'problem': content['problem']})

    async def draw_reject_problem(self, content):
        """
        Send the rejected problems of a team to the current user.
        """
        await self.send_json({'tid': content['tid'], 'type': 'reject_problem', 'round': content['round'],
                              'team': content['team'], 'rejected': content['rejected']})

    async def draw_reorder_pool(self, content):
        """
        Send the new order of a pool to the current user.
        """
        await self.send_json({'tid': content['tid'], 'type': 'reorder_poule', 'round': content['round'],
                              'poule': content['pool'], 'teams': content['teams'],
                              'problems': content['problems']})
