from zoneinfo import ZoneInfo
from django.db import models
from . import BaseModel, Event, TournamentPayment, FederalMatchCategory, FederalCategory, FederalLevelCategory, FederalAgeCategory, OnlineRegistrationStatus, RegistrationStatus, AnimationType

import uuid
from django.utils import timezone, formats
from datetime import datetime, timedelta, time
from tournaments.utils.player_search import get_player_name_from_csv
from shared.cryptography import encryption_util
from ..utils.extensions import plural_format
from django.utils.formats import date_format
from ..utils.licence_validator import LicenseValidator
from django.apps import apps
from django.conf import settings
from tournaments.services.currency_service import CurrencyService

class TeamSortingType(models.IntegerChoices):
    RANK = 1, 'Rank'
    INSCRIPTION_DATE = 2, 'Inscription Date'

class Tournament(BaseModel):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=True)
    event = models.ForeignKey(Event, blank=True, null=True, on_delete=models.SET_NULL, related_name='tournaments')
    name = models.CharField(max_length=200, null=True, blank=True)
    start_date = models.DateTimeField()
    end_date = models.DateTimeField(null=True, blank=True)
    creation_date = models.DateTimeField()
    is_private = models.BooleanField(default=False)
    # format = models.IntegerField(default=FederalMatchCategory.NINE_GAMES, choices=FederalMatchCategory.choices, null=True, blank=True)
    round_format = models.IntegerField(default=FederalMatchCategory.NINE_GAMES, choices=FederalMatchCategory.choices, null=True, blank=True)
    group_stage_format = models.IntegerField(default=FederalMatchCategory.NINE_GAMES, choices=FederalMatchCategory.choices, null=True, blank=True)
    loser_round_format = models.IntegerField(default=FederalMatchCategory.NINE_GAMES, choices=FederalMatchCategory.choices, null=True, blank=True)
    group_stage_sort_mode = models.IntegerField(default=0)
    group_stage_count = models.IntegerField(default=0)
    rank_source_date = models.DateTimeField(null=True, blank=True)
    day_duration = models.IntegerField(default=0)
    team_count = models.IntegerField(default=0)
    team_sorting = models.IntegerField(default=TeamSortingType.INSCRIPTION_DATE, choices=TeamSortingType.choices)
    federal_category = models.IntegerField(default=FederalCategory.MEN, choices=FederalCategory.choices) # optional ?
    federal_level_category = models.IntegerField(default=FederalLevelCategory.P100, choices=FederalLevelCategory.choices)
    federal_age_category = models.IntegerField(default=FederalAgeCategory.SENIOR, choices=FederalAgeCategory.choices)
    #group_stage_court_count = models.IntegerField(null=True, blank=True)
    #seed_count = models.IntegerField(default=0)
    closed_registration_date = models.DateTimeField(null=True, blank=True)
    group_stage_additional_qualified = models.IntegerField(default=0)
    court_count = models.IntegerField(default=2)
    prioritize_club_members = models.BooleanField()
    qualified_per_group_stage = models.IntegerField(default=0)
    teams_per_group_stage = models.IntegerField(default=0)
    entry_fee = models.FloatField(default=20.0, null=True, blank=True)
    global_id = models.CharField(max_length=100, null=True, blank=True) #represent the payment crypted string
    is_deleted = models.BooleanField(default=False)
    local_id = models.CharField(max_length=100, null=True, blank=True) #represent the is_canceled crypted string
    additional_estimation_duration = models.IntegerField(default=0)
    publish_teams = models.BooleanField(default=False)
    hide_teams_weight = models.BooleanField(default=False)
    publish_summons = models.BooleanField(default=False)
    publish_group_stages = models.BooleanField(default=False)
    publish_brackets = models.BooleanField(default=False)
    should_verify_bracket = models.BooleanField(default=False)
    should_verify_group_stage = models.BooleanField(default=False)
    publish_tournament = models.BooleanField(default=False)
    hide_points_earned = models.BooleanField(default=False)
    publish_rankings = models.BooleanField(default=False)
    loser_bracket_mode = models.IntegerField(default=0)
    initial_seed_round = models.IntegerField(default=0)
    initial_seed_count = models.IntegerField(default=0)
    enable_online_registration = models.BooleanField(default=False)  # Equivalent to Bool = false
    team_count_limit = models.BooleanField(default=True)
    registration_date_limit = models.DateTimeField(null=True, blank=True)  # Equivalent to Date? = nil
    opening_registration_date = models.DateTimeField(null=True, blank=True)  # Equivalent to Date? = nil
    waiting_list_limit = models.IntegerField(null=True, blank=True)  # Equivalent to Int? = nil
    account_is_required = models.BooleanField(default=True)
    license_is_required = models.BooleanField(default=True)
    minimum_player_per_team = models.IntegerField(default=2)
    maximum_player_per_team = models.IntegerField(default=2)
    information = models.CharField(max_length=4000, null=True, blank=True)
    umpire_custom_mail = models.EmailField(null=True, blank=True)
    umpire_custom_contact = models.CharField(max_length=200, null=True, blank=True)
    umpire_custom_phone = models.CharField(max_length=15, null=True, blank=True)
    hide_umpire_mail = models.BooleanField(default=False)
    hide_umpire_phone = models.BooleanField(default=True)
    disable_ranking_federal_ruling = models.BooleanField(default=False)
    reserved_spots = models.IntegerField(default=0)
    enable_online_payment = models.BooleanField(default=False)
    online_payment_is_mandatory = models.BooleanField(default=False)
    enable_online_payment_refund = models.BooleanField(default=False)
    refund_date_limit = models.DateTimeField(null=True, blank=True)  # Equivalent to Date? = nil
    stripe_account_id = models.CharField(max_length=255, blank=True, null=True)
    enable_time_to_confirm = models.BooleanField(default=False)
    is_corporate_tournament = models.BooleanField(default=False)
    is_template = models.BooleanField(default=False)
    animation_type = models.IntegerField(default=AnimationType.TOURNAMENT, choices=AnimationType.choices)
    publish_prog = models.BooleanField(default=False)
    show_teams_in_prog = models.BooleanField(default=False)
    club_member_fee_deduction = models.FloatField(null=True, blank=True)
    unregister_delta_in_hours = models.IntegerField(default=24)
    currency_code = models.CharField(null=True, blank=True, max_length=3, default='EUR')

    def delete_dependencies(self):
        for team_registration in self.team_registrations.all():
            team_registration.delete_dependencies()
            team_registration.delete()
        for gs in self.group_stages.all():
            gs.delete_dependencies()
            gs.delete()
        for round in self.rounds.filter(parent=None).all():
            round.delete_dependencies()
            round.delete()
        for draw_log in self.draw_logs.all():
            # draw_log.delete_dependencies()
            draw_log.delete()

    def __str__(self):
        if self.name:
            return self.name
        else:
            return self.display_name()

    def is_canceled(self):
        if self.local_id:
            decrypted = encryption_util.decrypt_aes_gcm(self.local_id)
            value = int(decrypted[18])
            if 0 <= value <= 4:
                return True
            else:
                return False
        else:
            return False

    def payment(self):
        if self.global_id:
            decrypted = encryption_util.decrypt_aes_gcm(self.global_id)
            value = int(decrypted[18])
            return TournamentPayment(value)
        else:
            return None

    def display_name(self):
        if self.name:
            return self.short_base_name() + " " + self.name
        else:
            return self.base_name()

    def broadcast_display_name(self):
        if self.name:
            return self.short_base_name() + " " + self.name
        else:
            return self.base_name()

    def broadcast_event_display_name(self):
        if self.event is not None:
            return self.event.display_name()
        else:
            return " "

    def base_name(self):
        return f"{self.level()} {self.category()}"

    def full_name(self):
        age = self.age()
        str = f"{self.level()} {self.category()}"
        if self.name:
            str = f"{self.level()} {self.name} {self.category()}"
        if age is not None:
            str = f"{str} {age}"
        return str

    def short_full_name(self):
        # For animation tournaments with custom names, just return the name
        if self.federal_level_category == 0 and self.name:  # FederalLevelCategory.UNLISTED (Animation) with custom name
            return self.name

        age = self.age()
        str = f"{self.level()}{self.category()[0]}"
        if age is not None and self.federal_age_category != 200:
            str = f"{str} {age}"
        return str

    def short_base_name(self):
        category = self.category()
        if len(category) > 0 and self.federal_level_category > 1:
            return f"{self.short_level()}{category[0]}"
        else:
            return self.short_level()

    def filter_name(self):
        components = [self.formatted_start_date(), self.short_base_name()]
        if self.event and self.event.club and self.event.club.name:
            components.append(self.event.club.name)
        elif self.event and self.event.name:
            components.append(self.event.name)
        elif self.name:
            components.append(self.name)
        return (' ').join(components)

    def timezone(self):
        tz = 'CET'
        if self.event and self.event.club:
            tz = self.event.club.timezone
        return ZoneInfo(tz)

    def local_start_date(self):
        timezone = self.timezone()
        return self.start_date.astimezone(timezone)

    def local_start_date_formatted(self):
        return formats.date_format(self.local_start_date(), format='l j F Y H:i').capitalize()

    def level(self):
        return self.get_federal_level_category_display()

    def is_custom_animation(self):
        return self.animation_type == AnimationType.CUSTOM

    def short_level(self):
        if self.federal_level_category == 0:
            match self.animation_type:
                case AnimationType.TOURNAMENT:
                    return "Anim."
                case AnimationType.MELEE:
                    return "Mêlée"
                case AnimationType.LOSER_BRACKET:
                    return "Classement"
                case AnimationType.CONSOLATION_BRACKET:
                    return "Consolante"
                case AnimationType.CUSTOM:
                    return "Spécial"
                case _:
                    return "Anim."
        if self.federal_level_category == 1:
            return "CHPT"
        return self.get_federal_level_category_display()

    def category(self):
        if self.federal_age_category > 100 and self.federal_age_category < 200:
            if self.federal_category == 0:
                return "Garçon"
            if self.federal_category == 1:
                return "Fille"
        return self.get_federal_category_display()

    def age(self):
        if self.federal_age_category == 0:
            return None
        return self.get_federal_age_category_display()

    def formatted_start_date(self):
        return self.local_start_date().strftime("%d/%m/%y")

    def in_progress(self):
        return self.end_date is None

    def sorting_finished_date(self):
        if self.end_date:
            return self.end_date
        else:
            return self.start_date

    def creator(self):
        if self.event and self.event.creator:
            return self.event.creator.username
        else:
            return None

    def private_label(self):
        if self.is_private:
            return "Privé"
        else:
            return "Public"

    def summon_count_display(self):
        teams = self.team_summons()
        if teams is not None and len(teams) > 0:
            return f"{len(teams)} équipes convoquées"
        else:
            return None

    def ranking_count_display(self):
        teams = self.rankings()
        if teams is not None and len(teams) > 0:
            return f"{len(teams)} équipes"
        else:
            return None

    def registration_count_display(self):
        teams = self.teams(True)
        if teams is not None and len(teams) == 1:
            return f"{len(teams)} équipe inscrite"
        elif teams is not None and len(teams) > 1:
            return f"{len(teams)} équipes inscrites"
        else:
            return None

    def get_tournament_status(self):
        return self.get_online_registration_status().status_localized()

    def get_tournament_status_team_count(self):
        active_teams_count = self.team_registrations.filter(walk_out=False).count()
        return min(active_teams_count, self.team_count)

    def name_and_event(self):
        event_name = None
        if self.event:
            event_name = self.event.name

        if event_name and self.name:
            return event_name + " : " + self.name
        elif event_name:
            return event_name
        elif self.name:
            return self.name
        else:
            return None

    def team_summons(self):
        summons = []
        if self.supposedly_in_progress() and self.end_date is None:
            # print('>>> team_summons supposedly_in_progress')
            for team in self.teams(False):
                names = team.names
                stage = team.stage
                weight = team.weight
                summon = TeamSummon(team.team_registration.id, names, team.date, weight, stage, "", team.image, self.day_duration)
                summons.append(summon)
        else:
            # print('>>> team_summons')
            for team_registration in self.team_registrations.all():
                if team_registration.is_valid_for_summon():
                    next_match = team_registration.next_match()
                    if next_match and next_match.start_date is not None:
                        names = team_registration.team_names()
                        stage = next_match.summon_stage_name()
                        weight = team_registration.weight
                        summon = TeamSummon(team_registration.id, names, next_match.local_start_date(), weight, stage, next_match.court_name(next_match.court_index), team_registration.logo,  self.day_duration)
                        summons.append(summon)

        summons.sort(key=lambda s: (s.date is None, s.date or datetime.min))
        return summons

    def has_summons(self):
        for team_registration in self.team_registrations.all():
            if team_registration.is_valid_for_summon():
                next_match = team_registration.next_match()
                if next_match and next_match.start_date is not None:
                    return True
        return False

    def rankings(self):
        rankings = []
        for team_registration in self.team_registrations.all():
            if team_registration.walk_out is False and team_registration.final_ranking is not None:
                names = team_registration.team_names()
                ranking = team_registration.final_ranking
                points = team_registration.points_earned
                team = TeamRanking(team_registration.id, names, ranking, points, team_registration.logo)
                rankings.append(team)

        rankings.sort(key=lambda r: r.ranking)
        return rankings

    def get_team_waiting_list_position(self, team_registration):
        # Use the teams method to get sorted list of teams
        now_utc = timezone.now()
        current_time = now_utc.astimezone(self.timezone())
        local_registration_federal_limit = self.local_registration_federal_limit()
        if self.team_sorting == TeamSortingType.RANK and local_registration_federal_limit is not None:
            if current_time < local_registration_federal_limit:
                return -1

        # Check if team_count exists
        if self.team_count_limit == True:
            all_teams = self.teams(True)
            index = -1
            # Find position of team in all teams list
            for i, team in enumerate(all_teams):
                if team.team_registration.id == team_registration.id:
                    index = i

            # Team is not in list
            if index < 0:
                print("get_team_waiting_list_position: Team is not in list", index, self.team_count)
                return -1
            # Return position in waiting list relative to target count
            print("get_team_waiting_list_position: Return position in waiting list relative to target count", index, self.team_count)
            return index - self.team_count
        else:
            print("get_team_waiting_list_position: Return -1", self.team_count)
            return -1

    def group_stage_spots(self):
        """Returns the total number of spots in all group stages."""
        return sum(gs.size for gs in self.group_stages.all())

    def teams(self, include_waiting_list, un_walk_out_team=None):
        complete_teams, wildcard_bracket, wildcard_group_stage, waiting_teams = self.computed_teams(un_walk_out_team)
        all_teams = self.sort_teams(include_waiting_list, complete_teams, wildcard_bracket, wildcard_group_stage, waiting_teams)
        return all_teams

    def computed_teams(self, un_walk_out_team=None):
        # Initialize team categories
        complete_teams = []
        wildcard_bracket = []
        wildcard_group_stage = []
        waiting_teams = []

        # Get registration cutoff date
        closed_date = self.closed_registration_date

        # Process each team registration
        for db_team_reg in self.team_registrations.all():
            team_reg = db_team_reg
            if un_walk_out_team and team_reg.id == un_walk_out_team.id:
                team_reg = un_walk_out_team
            if team_reg.out_of_tournament():
                continue

            # Create team item
            team = TeamItem(team_reg)

            # Determine if registration is valid based on date
            is_valid = (
                closed_date is None or
                team_reg.registration_date is None or
                (team_reg.registration_date and team_reg.registration_date <= closed_date)
            )

            # Set initial stage
            if team_reg.group_stage_position is not None:
                team.set_stage("Poule")
            elif team_reg.bracket_position is not None:
                team.set_stage("Tableau")
            else:
                team.set_stage("Attente")

            # Categorize team
            if team_reg.wild_card_bracket:
                wildcard_bracket.append(team)
            elif team_reg.wild_card_group_stage:
                wildcard_group_stage.append(team)
            elif is_valid:
                complete_teams.append(team)
            else:
                waiting_teams.append(team)

        return complete_teams, wildcard_bracket, wildcard_group_stage, waiting_teams

    def sort_teams(self, include_waiting_list, complete_teams, wildcard_bracket, wildcard_group_stage, waiting_teams):
        # Initialize group stage spots
        group_stage_spots = self.group_stage_spots()
        bracket_seeds = self.team_count - group_stage_spots - len(wildcard_bracket)
        group_stage_team_count = group_stage_spots - len(wildcard_group_stage)
        if group_stage_team_count < 0:
            group_stage_team_count = 0
        if bracket_seeds < 0:
            bracket_seeds = 0

        # Sort teams based on tournament rules
        if self.team_sorting == TeamSortingType.INSCRIPTION_DATE:
            complete_teams.sort(key=lambda t: (
                t.registration_date is None,
                t.registration_date or datetime.min,
                t.initial_weight,
                t.team_registration.unique_random_index,
                t.team_registration.id
            ))
            waiting_teams.sort(key=lambda t: (
                t.registration_date is None,
                t.registration_date or datetime.min,
                t.initial_weight,
                t.team_registration.unique_random_index,
                t.team_registration.id
            ))
        else:
            complete_teams.sort(key=lambda t: (t.initial_weight, t.team_registration.unique_random_index, t.team_registration.id))
            waiting_teams.sort(key=lambda t: (t.initial_weight, t.team_registration.unique_random_index, t.team_registration.id))

        wildcard_group_stage.sort(key=lambda t: (t.initial_weight, t.team_registration.unique_random_index, t.team_registration.id))
        wildcard_bracket.sort(key=lambda t: (t.initial_weight, t.team_registration.unique_random_index, t.team_registration.id))

        # Split teams into main bracket and waiting list
        computed_team_count = self.team_count - len(wildcard_bracket) - len(wildcard_group_stage)
        if computed_team_count < 0:
            computed_team_count = 0
        qualified_teams = complete_teams[:computed_team_count]
        excess_teams = complete_teams[computed_team_count:]

        qualified_teams.sort(key=lambda t: (t.initial_weight, t.team_registration.unique_random_index, t.team_registration.id))
        excess_teams.sort(key=lambda t: (t.initial_weight, t.team_registration.unique_random_index, t.team_registration.id))

        # Combine all waiting list teams
        waiting_list = excess_teams + waiting_teams
        if self.team_sorting == TeamSortingType.INSCRIPTION_DATE:
            waiting_list.sort(key=lambda t: (
                t.registration_date is None,
                t.registration_date or datetime.min,
                t.initial_weight,
                t.team_registration.unique_random_index,
                t.team_registration.id
            ))
        else:
            waiting_list.sort(key=lambda t: (t.initial_weight, t.team_registration.unique_random_index, t.team_registration.id))

        # Return final sorted list
        bracket_teams = qualified_teams[:bracket_seeds] + wildcard_bracket
        gs_teams = qualified_teams[bracket_seeds:(bracket_seeds+group_stage_team_count)] + wildcard_group_stage
        bracket_teams.sort(key=lambda t: (t.initial_weight, t.team_registration.unique_random_index, t.team_registration.id))
        gs_teams.sort(key=lambda t: (t.initial_weight, t.team_registration.unique_random_index, t.team_registration.id))
        all_teams = bracket_teams + gs_teams
        for team in bracket_teams:
            team.set_stage("Tableau")
        for team in gs_teams:
            team.set_stage("Poule")

        if include_waiting_list:
            all_teams.extend(waiting_list)
        return all_teams

    def match_groups(self, broadcasted, group_stage_id, round_id):

        display_brackets = self.display_matches()

        match_groups = []
        if group_stage_id:
            # Use prefetched data instead of additional query
            group_stage = next((gs for gs in self.group_stages.all() if str(gs.id) == group_stage_id), None)
            if group_stage:
                match_groups.append(self.group_stage_match_group(group_stage, broadcasted, hide_empty_matches=False))
        elif round_id:
            # Use prefetched data instead of additional query
            round = next((r for r in self.rounds.all() if str(r.id) == round_id), None)
            if round and display_brackets is True:
                match_groups = self.round_match_groups(round, broadcasted, hide_empty_matches=False)
        else:
            match_groups = self.all_groups(broadcasted)

        return match_groups

    def all_groups(self, broadcasted):
        groups = []

        if self.display_matches():
            # Use already prefetched rounds to avoid additional queries
            bracket_rounds = [r for r in self.rounds.all() if r.parent is None and not r.group_stage_loser_bracket]
            bracket_rounds.sort(key=lambda r: r.index)
            for round in bracket_rounds:
                groups.extend(self.round_match_groups(round, broadcasted, hide_empty_matches=True))

        if self.display_group_stages():
            # Use already prefetched rounds to avoid additional queries
            loser_bracket_rounds = [r for r in self.rounds.all() if r.parent is None and r.group_stage_loser_bracket]
            loser_bracket_rounds.sort(key=lambda r: r.index)
            for round in loser_bracket_rounds:
                groups.extend(self.round_match_groups(round, broadcasted, hide_empty_matches=True))

            group_stages = sorted(self.sorted_group_stages(), key=lambda s: (-s.step, s.index))
            for group_stage in group_stages:
                group = self.group_stage_match_group(group_stage, broadcasted, hide_empty_matches=True)
                if group:
                    groups.append(group)

        return groups

    def group_stage_match_group(self, group_stage, broadcasted, hide_empty_matches):

        if group_stage is None:
            return None
        matches = group_stage.matches.all()
        if hide_empty_matches:
            matches = [m for m in matches if m.should_appear()]
        else:
            matches = [m for m in matches if m.disabled is False]

        matches.sort(key=lambda m: (m.start_date is None, m.end_date is not None, m.start_date, m.index))

        if matches:
            return self.create_match_group(group_stage.display_name(), matches)
        else:
            return None

    def round_match_groups(self, round, broadcasted, hide_empty_matches):
        groups = []

        matches = round.matches.order_by('index').all()
        if hide_empty_matches:
            matches = [m for m in matches if m.should_appear()]
        else:
            matches = [m for m in matches if m.disabled is False]

        if round and matches:
            matches.sort(key=lambda m: m.index)
            group = self.create_match_group(round.plural_name(), matches)
            groups.append(group)

            ranking_matches = round.ranking_matches(hide_empty_matches)
            if hide_empty_matches:
                ranking_matches = [m for m in ranking_matches if m.should_appear()]
            else:
                ranking_matches = [m for m in ranking_matches if m.disabled is False]

            if len(ranking_matches) > 0:
                ranking_matches.sort(
                    key=lambda m: (
                        m.round.index,       # Sort by Round index first
                        m.round.get_depth(),
                        m.name or '', # Then by Round depth, using empty string if name is None
                    )
                )
                group = self.create_match_group('Matchs de classement', ranking_matches)
                groups.append(group)

        return groups

    def create_match_group(self, name, matches, round_id=None, round_index=None, hide_teams=False, event_mode=False, short_names=False, broadcast=False):
        matches = list(matches)
        live_matches = [match.live_match(hide_teams, event_mode, short_names, broadcast) for match in matches]
        # Filter out matches that have a start_date of None
        valid_matches = [match for match in matches if match.start_date is not None]

        formatted_schedule = ''
        if valid_matches and self.day_duration >= 7:
            # Find the first match by start date
            first_match = min(valid_matches, key=lambda match: match.start_date)

            # Format the date
            timezone = first_match.get_tournament().timezone()
            local_start = first_match.start_date.astimezone(timezone)
            time_format = 'l d F'
            formatted_schedule = f" - {formats.date_format(local_start, format=time_format)}"

        return MatchGroup(name, live_matches, formatted_schedule, round_id, round_index)

    def live_group_stages(self):
        group_stages = self.sorted_group_stages()
        return [gs.live_group_stages() for gs in group_stages]

    def sorted_group_stages(self):
        # Use already prefetched group stages to avoid additional queries
        group_stages = list(self.group_stages.all())
        group_stages.sort(key=lambda gs: (-gs.step, gs.index))

        # List to collect live group stages from finished steps
        filtered = []
        steps_completed = set()

        for group_stage in group_stages:
            if group_stage.step > 0:
                # Check the previous step's group stages using already loaded data
                prev_step = group_stage.step - 1
                if prev_step not in steps_completed:
                    previous_step_group_stages = [gs for gs in group_stages if gs.step == prev_step]
                    # Check if all previous step group stages are completed
                    if previous_step_group_stages and all(gs.is_completed() for gs in previous_step_group_stages):
                        steps_completed.add(prev_step)

                if prev_step in steps_completed:
                    filtered.append(group_stage)
            else:
                # Always include step 0
                filtered.append(group_stage)

        return filtered

    def get_previous_live_group_stages(self, step):
        previous_step_group_stages = self.group_stages.filter(step=step).order_by('index')
        return [gs.live_group_stages() for gs in previous_step_group_stages]

    def last_group_stage_step(self):
        live_group_stages = self.sorted_group_stages()

        # Filter to find the last running step
        last_running_step = max(gs.step for gs in live_group_stages) if live_group_stages else None

        if last_running_step is not None:
            # Get only group stages from the last running step
            group_stages_last_step = [gs for gs in live_group_stages if gs.step == last_running_step]
            return group_stages_last_step
        else:
            return []

    def broadcast_content(self):

        matches, group_stages = self.broadcasted_matches_and_group_stages()
        group_stages_dicts = [gs.to_dict() for gs in group_stages]

        # if now is before the first match, we want to show the summons + group stage or first matches
        # change timezone to datetime to avoid the bug RuntimeWarning: DateTimeField Tournament.start_date received a naive datetime (2024-05-16 00:00:00) while time zone support is active.
        current_time = timezone.localtime()
        tournament_start = self.local_start_date()
        one_hour_before_start = tournament_start - timedelta(hours=1)

        if current_time < one_hour_before_start:
            team_summons_dicts = [summon.to_dict() for summon in self.team_summons()]
            if group_stages:
                return {
                    'matches': [],
                    'group_stages': group_stages_dicts,
                    'summons': team_summons_dicts,
                    'event_title' : self.broadcast_event_display_name(),
                    'tournament_title' : self.broadcast_display_name(),
                    'rankings' : []
                }
            else:
                live_matches_dicts = [match.live_match().to_dict() for match in matches]
                return {
                    'matches': live_matches_dicts,
                    'group_stages': [],
                    'summons': team_summons_dicts,
                    'event_title' : self.broadcast_event_display_name(),
                    'tournament_title' : self.broadcast_display_name(),
                    'rankings' : []
                }
        elif self.end_date is not None:
            live_matches_dicts = [match.live_match().to_dict() for match in matches]
            team_rankings_dicts = [ranking.to_dict() for ranking in self.rankings()]
            return {
                'matches': live_matches_dicts,
                'group_stages': [],
                'summons': [],
                'event_title' : self.broadcast_event_display_name(),
                'tournament_title' : self.broadcast_display_name(),
                'rankings' : team_rankings_dicts
            }
        else: # we want to display the broadcasted content
            live_matches_dicts = [match.live_match().to_dict() for match in matches]
            return {
                'matches': live_matches_dicts,
                'group_stages': group_stages_dicts,
                'summons': [],
                'rankings' : [],
                'event_title' : self.broadcast_event_display_name(),
                'tournament_title' : self.broadcast_display_name(),
            }

    def broadcasted_matches_and_group_stages(self):
        matches = []
        group_stages = []

        if self.group_stages.count() > 0 and self.no_bracket_match_has_started():
            group_stages = [gs.live_group_stages() for gs in self.last_group_stage_step()]
            matches = self.broadcasted_group_stages_matches()
            first_round = self.first_round()
            if first_round and self.has_all_group_stages_started():
                matches.extend(first_round.get_matches_recursive(True))
        else:
            current_round = self.round_to_show()
            if current_round:
                #print(f'current_round = {current_round.index} / parent = {current_round.parent}')
                all_upper_matches_are_over = current_round.all_matches_are_over()
                if all_upper_matches_are_over is False:
                    matches.extend(current_round.get_matches_recursive(True))

                # Add full matches from the next rounds
                next_round = self.round_for_index(current_round.index - 1)
                if next_round:
                    # print('next round')
                    matches.extend(next_round.get_matches_recursive(True))

                if all_upper_matches_are_over is True:
                    # print('all_upper_matches_are_over')
                    matches.extend(current_round.get_matches_recursive(True))

                # Add matches from the previous round or group_stages
                previous_round = self.round_for_index(current_round.index + 1)
                if previous_round:
                    # print('previous_round')
                    matches.extend(previous_round.get_matches_recursive(True))

                    previous_previous_round = self.round_for_index(current_round.index + 2)
                    if previous_previous_round:
                        previous_previous_matches = previous_previous_round.get_matches_recursive(True)
                        previous_previous_matches = [m for m in previous_previous_matches if m.end_date is None]
                        matches.extend(previous_previous_matches)
                else:
                    #print('group_stages')
                    group_stages = [gs.live_group_stages() for gs in self.last_group_stage_step()]
            else:
                first_round = self.first_round()
                if first_round:
                    matches.extend(first_round.get_matches_recursive(True))

        return matches, group_stages

    def no_bracket_match_has_started(self):
        matches = []
        for round in self.rounds.all():
            for match in round.matches.all():
                if match.started():
                    return False
        return True

    def all_matches(self, hide_empty_matches):
        matches = []
        for round in self.rounds.all():
            matches.extend(round.all_matches(hide_empty_matches))
        for group_stage in self.group_stages.all():
            matches.extend(group_stage.matches.all())

        matches = [m for m in matches if m.should_appear()]

        return matches

    def group_stage_matches(self):
        matches = []
        for group_stage in self.group_stages.all():
            matches.extend(group_stage.matches.all())
        return matches

    def group_stages_running(self):
        if len(self.group_stages.all()) > 0:
            # check le debut des match de Round
            matches = self.group_stage_matches()
            running_group_stage_matches = [m for m in matches if m.end_date is None]
            return len(running_group_stage_matches) > 0
        else:
            return False

    def first_unfinished_match(self):
        matches = [m for m in self.all_matches(False) if m.start_date and m.end_date is None]
        # print(f'first_unfinished_match > match len: {len(matches)}')
        matches.sort(key=lambda m: m.start_date)
        main_bracket_matches = [m for m in matches if m.round and m.round.parent is None]
        if main_bracket_matches:
            return main_bracket_matches[0]

        if matches:
            return matches[0]
        else:
            return None

    def round_to_show(self):
        # print('===== round_to_show')
        last_started_match = self.first_unfinished_match()
        if last_started_match and last_started_match.round:
            # print(f'last_started_match = {last_started_match.name}')
            current_round = last_started_match.round.root_round()
            # print(f'round_to_show > current_round: {current_round.name()}')
            if current_round:
                return current_round

        # all started matches have ended, possibly
        last_finished_match = self.last_finished_match()
        if last_finished_match:
            round = last_finished_match.round
            if round is None: # when the last finished match is in the group stage
                round = self.rounds.filter(parent__isnull=True).order_by('-index').first()

            if round:
                # print(f'last_finished_match = {last_finished_match.name}')
                round_root_index = round.root_round().index
                # print(f'round_index = {round_root_index}')
                if round_root_index == 0:
                    return round
                else:
                    round = self.rounds.filter(parent=None,index=round_root_index-1).first()
                    if round:
                        return round
                    else:
                        return None
        return None

    def last_started_match(self):
        matches = [m for m in self.all_matches(False) if m.start_date]
        matches.sort(key=lambda m: m.start_date, reverse=True)
        return matches[0] if matches else None

    def last_finished_match(self):
        matches = [m for m in self.all_matches(False) if m.end_date]
        matches.sort(key=lambda m: m.end_date, reverse=True)
        return matches[0] if matches else None

    def round_for_index(self, index):
        return self.rounds.filter(index=index, parent=None).first()

    def first_round(self):
        main_rounds = list(self.rounds.filter(parent=None))
        main_rounds.sort(key=lambda r: r.index, reverse=True)
        return main_rounds[0] if main_rounds else None

    def broadcasted_group_stages_matches(self):
        matches = []
        group_stages = self.elected_broadcast_group_stages()
        group_stages.sort(key=lambda gs: (gs.index, gs.start_date is None, gs.start_date))
        for group_stage in group_stages:

            matches.extend(group_stage.matches.all())

        if len(matches) > 16:
            # if more than 16 groupstage matches
            now = timezone.now()
            future_threshold = now + timedelta(hours=1)
            past_threshold = now - timedelta(hours=1)
            matches = [m for m in matches if m.should_appear() and
                    (m.start_date is None or m.start_date <= future_threshold) and  # Not starting in more than 1h
                    (m.end_date is None or m.end_date >= past_threshold)]  # Not finished for more than 1h
            matches.sort(key=lambda m: (m.start_date is None)) # display started matches
            matches = matches[:16]
        matches.sort(key=lambda m: (m.start_date is None, m.end_date is not None, m.start_date, m.index))

        group_stage_loser_bracket = list(self.rounds.filter(parent=None, group_stage_loser_bracket=True).all())
        if len(group_stage_loser_bracket) > 0:
            loser_matches = group_stage_loser_bracket[0].all_matches(True)
            loser_matches = [m for m in loser_matches if m.should_appear()]
            loser_matches.sort(key=lambda m: (m.start_date is None, m.end_date is not None, m.start_date, m.index))
            matches.extend(loser_matches)

        return matches

    def elected_broadcast_group_stages(self):
        group_stages = list(self.group_stages.all())
        started = [gs for gs in group_stages if gs.starts_soon()]
        if len(started) > 0:
            return started
        else:
            return group_stages

    def display_rankings(self):
        if self.supposedly_in_progress():
            return True
        if self.end_date is not None:
            return True
        return False

    def display_tournament(self):
        if self.publish_tournament or self.enable_online_registration:
            return True

        is_build_and_not_empty = self.is_build_and_not_empty()

        if self.end_date is not None:
            return is_build_and_not_empty
        if self.has_started():
            return is_build_and_not_empty
        minimum_publish_date = self.creation_date.replace(hour=7, minute=0) + timedelta(days=1)
        return timezone.now() >= minimum_publish_date

    def display_teams(self):
        if self.end_date is not None:
            return self.has_team_registrations()
        if self.publish_teams:
            return self.has_team_registrations()
        if self.has_started():
            return self.has_team_registrations()
        if self.will_start_soon(1):
            return True
        return False

    def display_prog(self):
        if self.publish_prog:
            return True
        if self.has_started():
            return True
        if self.will_start_soon(1):
            return True
        return False

    def has_team_registrations(self):
        return self.team_registrations.count() > 0

    def display_summons(self):
        if self.end_date is not None:
            return False
        if self.publish_summons:
            return self.has_summons()
        if self.has_started():
            return self.has_summons()
        if self.will_start_soon(1):
            return True
        return False

    def display_group_stages(self):
        if len(self.group_stages.all()) == 0:
            return False
        if self.end_date is not None:
            return True
        if self.publish_group_stages:
            return True

        first_group_stage_start_date = self.group_stage_start_date()
        if first_group_stage_start_date is None:
            return self.has_started()
        else:
            return timezone.now() >= first_group_stage_start_date

    def group_stage_start_date(self):
        group_stages = [gs for gs in self.group_stages.all() if gs.start_date is not None]
        if len(group_stages) == 0:
            return None
        return min(group_stages, key=lambda gs: gs.start_date).start_date

    def display_matches(self):
        if self.end_date is not None:
            return True
        bracket_matches = self.bracket_matches()
        if len(bracket_matches) == 0:
            return self.display_group_stages()
        if self.publish_brackets:
            return True

        first_match_start_date = self.first_match_start_date(bracket_matches)
        if first_match_start_date is None:
            return self.has_started()

        bracket_start_date = self.getEightAm(first_match_start_date)

        if bracket_start_date < self.start_date:
            bracket_start_date = self.start_date

        group_stage_start_date = self.group_stage_start_date()

        now = timezone.now()

        if group_stage_start_date is not None:
            if bracket_start_date < group_stage_start_date:
                return now >=first_match_start_date

        if now >= bracket_start_date:
            return True

        return False


    def bracket_matches(self):
        matches = []
        for round in self.rounds.all():
            matches.extend(round.all_matches(False))
        return matches

    def first_match_start_date(self, bracket_matches):
        matches = [m for m in bracket_matches if m.start_date is not None]
        if len(matches) == 0:
            return None
        return min(matches, key=lambda m: m.start_date).start_date

    def getEightAm(self, date):
        return date.replace(hour=8, minute=0, second=0, microsecond=0, tzinfo=date.tzinfo)

    def has_started(self, hour_delta=None):
        timezoned_datetime = self.local_start_date()
        now_utc = timezone.now()
        now = now_utc.astimezone(self.timezone())
        if hour_delta is not None:
            timezoned_datetime -= timedelta(hours=hour_delta)
        return now >= timezoned_datetime

    def will_start_soon(self, hour_delta=2):
        return self.has_started(hour_delta=hour_delta)

    def has_ongoing_matches(self):
        """
        Returns True if tournament has any matches that are currently ongoing
        """
        # Check matches in rounds
        for round_obj in self.rounds.all():
            for match in round_obj.matches.all():
                if match.is_ongoing():
                    return True

        # Check matches in group stages
        for group_stage in self.group_stages.all():
            for match in group_stage.matches.all():
                if match.is_ongoing():
                    return True

        return False

    def has_matches_starting_soon(self):
        """
        Returns True if tournament has any matches starting within the next hour
        """
        # Check matches in rounds
        for round_obj in self.rounds.all():
            for match in round_obj.matches.all():
                if match.will_start():
                    return True

        # Check matches in group stages
        for group_stage in self.group_stages.all():
            for match in group_stage.matches.all():
                if match.will_start():
                    return True

        return False

    def get_next_match_start_time(self):
        """
        Returns the datetime of the earliest upcoming match, or None if no upcoming matches
        """
        next_times = []

        # Check matches in rounds
        for round_obj in self.rounds.all():
            for match in round_obj.matches.all():
                if match.disabled or match.end_date:
                    continue

                if match.start_date and match.start_date > timezone.now():
                    next_times.append(match.start_date)
                elif match.planned_start_date and match.planned_start_date > timezone.now():
                    next_times.append(match.planned_start_date)

        # Check matches in group stages
        for group_stage in self.group_stages.all():
            for match in group_stage.matches.all():
                if match.disabled or match.end_date:
                    continue

                if match.start_date and match.start_date > timezone.now():
                    next_times.append(match.start_date)
                elif match.planned_start_date and match.planned_start_date > timezone.now():
                    next_times.append(match.planned_start_date)

        return min(next_times) if next_times else None

    def are_teams_positioned(self):
        teams = self.team_registrations.all()
        filtered_teams = [t for t in teams if t.is_positioned()]
        if len(filtered_teams) > 3:
            return True
        return False

    def supposedly_in_progress(self):
        start = self.start_date - timedelta(hours=1)
        end = self.start_date + timedelta(days=self.day_duration + 1)
        return start <= timezone.now() <= end

        # end = self.start_date + timedelta(days=self.day_duration + 1)
        # return self.start_date.replace(hour=0, minute=0) <= timezone.now() <= end

        # timezoned_datetime = self.local_start_date()
        # end = timezoned_datetime + timedelta(days=self.day_duration + 1)
        # now_utc = timezone.now()
        # now = now_utc.astimezone(self.timezone())
        # start = timezoned_datetime.replace(hour=0, minute=0)

        # # print(f"timezoned_datetime: {timezoned_datetime}")
        # # print(f"tournament end date: {end}")
        # # print(f"current time: {now}")
        # # print(f"tournament start: {start}")
        # # print(f"start <= now <= end: {start <= now <= end}")

        # return start <= now <= end

    def starts_in_the_future(self):
        # tomorrow = datetime.now().date() + timedelta(days=1)
        # timezoned_datetime = self.local_start_date()
        # start = timezoned_datetime.replace(hour=0, minute=0)
        # now_utc = timezone.now()
        # now = now_utc.astimezone(self.timezone())
        start = self.start_date - timedelta(hours=1)
        return start >= timezone.now()

    def has_ended(self):
        return self.end_date is not None

    def should_be_over(self):
        if self.has_ended():
            return True

        timezoned_datetime = self.local_start_date()
        end = timezoned_datetime + timedelta(days=self.day_duration + 1)
        now_utc = timezone.now()
        now = now_utc.astimezone(self.timezone())
        return now >= end and self.is_build_and_not_empty() and self.nearly_over()

    def nearly_over(self):
        # First check group stages if they exist
        group_stages = list(self.group_stages.all())  # Use prefetched data
        if group_stages:
            # Check if all group stages are completed
            for group_stage in group_stages:
                # Use the is_completed method
                if group_stage.is_completed():
                    return True

        # If no group stages, check semi-finals
        semifinals = self.rounds.filter(index=1, parent=None).first()  # Use prefetched data
        if semifinals:
            # Check if any match in semi-finals has started
            for match in semifinals.matches.all():  # Use prefetched data
                if match.start_date is not None and match.is_ready():
                    return True

        return False

    def display_points_earned(self):
        return self.federal_level_category != FederalLevelCategory.UNLISTED and self.hide_points_earned is False

    def hide_weight(self):
        return self.hide_teams_weight

    def is_build_and_not_empty(self):
        if hasattr(self, '_prefetched_objects_cache'):
            # Use prefetched data if available
            has_group_stages = 'group_stages' in self._prefetched_objects_cache and len(self.group_stages.all()) > 0
            has_rounds = 'rounds' in self._prefetched_objects_cache and len(self.rounds.all()) > 0
            has_team_registrations = 'team_registrations' in self._prefetched_objects_cache and len(self.team_registrations.all()) >= 4
        else:
            # Fall back to database queries if not prefetched
            has_group_stages = self.group_stages.count() > 0
            has_rounds = self.rounds.count() > 0
            has_team_registrations = self.team_registrations.count() >= 4

        return (has_group_stages or has_rounds) and has_team_registrations

    def day_duration_formatted(self):
        return plural_format("jour", self.day_duration)

    def has_club_address(self):
        if self.event and self.event.club:
            return self.event.club.has_address()
        else:
            return False

    def has_all_group_stages_started(self):
        for group_stage in self.group_stages.all():
            if group_stage.has_at_least_one_started_match() is False:
                return False
        return True

    def options_fee(self):
        options = []
        currency_service = CurrencyService()

        # Entry fee
        if self.entry_fee is not None and self.entry_fee > 0:
            formatted_fee = currency_service.format_amount(self.entry_fee, self.currency_code)
            if self.is_custom_animation():
                options.append(f"{formatted_fee} par personne")
            else:
                options.append(f"Frais d'inscription: {formatted_fee} par joueur")

        # Club member fee reduction
        if self.club_member_fee_deduction and self.club_member_fee_deduction > 0:
            formatted_deduction = currency_service.format_amount(self.club_member_fee_deduction, self.currency_code)
            options.append(f"Réduction de {formatted_deduction} pour les membres du club")

        return options

    def options_online_registration(self):
        options = []
        timezone = self.timezone()

        # Date d'ouverture
        if self.opening_registration_date:
            date = formats.date_format(self.opening_registration_date.astimezone(timezone), format='j F Y H:i')
            options.append(f"Ouverture des inscriptions le {date}")

        # Date limite
        if self.registration_date_limit:
            date = formats.date_format(self.registration_date_limit.astimezone(timezone), format='j F Y H:i')
            options.append(f"Clôture des inscriptions le {date}")

        # Période de désinscription
        formatted_period = self.format_time_period(self.unregister_delta_in_hours)
        options.append(f"Désinscription possible jusqu'à {formatted_period} avant le tournoi")

        options.append(self.get_selection_status_localized)

        # Cible d'équipes
        if self.team_count_limit is True:
            options.append(f"Maximum {self.team_count} équipes")

        # Liste d'attente
        if self.waiting_list_limit:
            options.append(f"Liste d'attente limitée à {self.waiting_list_limit} équipes")

        # Options d'inscription
        if self.account_is_required:
            options.append("Compte requis")
        if self.license_is_required:
            options.append("Licence requise")

        # Options de paiement en ligne
        if self.enable_online_payment:
            if self.online_payment_is_mandatory:
                options.append("Paiement en ligne obligatoire")
            else:
                options.append("Paiement en ligne disponible")

            if self.enable_online_payment_refund and self.refund_date_limit:
                date = formats.date_format(self.refund_date_limit.astimezone(timezone), format='j F Y H:i')
                options.append(f"Remboursement en ligne possible jusqu'au {date}")
            elif self.enable_online_payment_refund:
                options.append("Remboursement en ligne possible")
            else:
                options.append("Remboursement en ligne impossible")

        # Joueurs par équipe
        min_players = self.minimum_player_per_team
        max_players = self.maximum_player_per_team
        if min_players == max_players:
            options.append(f"{min_players} joueurs par équipe")
        else:
            options.append(f"Entre {min_players} et {max_players} joueurs par équipe")

        return options

    def format_time_period(self, hours):
        """
        Format time period in hours to a more readable format.
        Examples:
        - 24 hours -> "24h"
        - 48 hours -> "2 jours"
        - 168 hours -> "7 jours"
        - 25 hours -> "25h"
        """
        if hours % 24 == 0 and hours > 24:
            days = hours // 24
            return f"{days} jours"
        else:
            return f"{hours}h"

    def get_selection_status_localized(self):
        if self.team_sorting == TeamSortingType.RANK:
            return "La sélection se fait par le poids de l'équipe"
        else:
            return "La sélection se fait par date d'inscription"

    def automatic_waiting_list(self):
        """
        Determines if automatic waiting list processing should be applied based on the tournament's registration status.
        Returns True if automatic waiting list processing should be applied, False otherwise.
        """

        if self.enable_time_to_confirm is False:
            return False
        # Get the current registration status
        status = self.get_online_registration_status()
        # Define which status values should allow automatic waiting list
        status_map = {
            OnlineRegistrationStatus.OPEN: True,
            OnlineRegistrationStatus.NOT_ENABLED: False,
            OnlineRegistrationStatus.NOT_STARTED: False,
            OnlineRegistrationStatus.ENDED: False,
            OnlineRegistrationStatus.WAITING_LIST_POSSIBLE: True,
            OnlineRegistrationStatus.WAITING_LIST_FULL: True,  # Still manage in case spots open up
            OnlineRegistrationStatus.IN_PROGRESS: False,  # Allow for last-minute changes
            OnlineRegistrationStatus.ENDED_WITH_RESULTS: False,
            OnlineRegistrationStatus.CANCELED: False
        }

        # Return the mapped value or False as default for any unmapped status
        return status_map.get(status, False)

    def get_online_registration_status(self):
        if self.is_canceled():
            return OnlineRegistrationStatus.CANCELED
        if self.end_date is not None:
            return OnlineRegistrationStatus.ENDED_WITH_RESULTS
        if self.enable_online_registration is False:
            return OnlineRegistrationStatus.NOT_ENABLED
        if self.has_started():
            return OnlineRegistrationStatus.ENDED
        if self.closed_registration_date is not None:
            return OnlineRegistrationStatus.WAITING_LIST_POSSIBLE

        now = timezone.now()

        if self.opening_registration_date is not None:
            if now < self.opening_registration_date:
                return OnlineRegistrationStatus.NOT_STARTED

        if self.registration_date_limit is not None:
            if now > self.registration_date_limit:
                return OnlineRegistrationStatus.WAITING_LIST_POSSIBLE

        if self.team_sorting == TeamSortingType.RANK:
            return OnlineRegistrationStatus.OPEN

        if self.team_count_limit is True:
            # Get all team registrations excluding walk_outs
            current_team_count = self.team_registrations.exclude(walk_out=True).count()
            if current_team_count >= self.team_count:
                if self.waiting_list_limit is not None:
                    waiting_list_count = current_team_count - self.team_count
                    if waiting_list_count >= self.waiting_list_limit:
                        return OnlineRegistrationStatus.WAITING_LIST_FULL
                return OnlineRegistrationStatus.WAITING_LIST_POSSIBLE
        return OnlineRegistrationStatus.OPEN

    def get_registration_status_short_label(self):
        """Returns a short label for the registration status"""
        status = self.get_online_registration_status()
        return status.short_label()

    def get_registration_status_class(self):
        """Returns the CSS class for the registration status box"""
        status = self.get_online_registration_status()
        return status.box_class()

    def should_display_status_box(self):
        """Returns whether the registration status box should be displayed"""
        status = self.get_online_registration_status()
        return status.display_box()

    def is_unregistration_possible(self):
        # Check if tournament has started
        if self.supposedly_in_progress():
            return False

        if self.will_start_soon(self.unregister_delta_in_hours):
            return False

        if self.closed_registration_date is not None:
            return False

        # Check if tournament is finished
        if self.end_date is not None:
            return False

        # Check if registration is closed
        if self.registration_date_limit is not None:
            if timezone.now() > self.registration_date_limit:
                return False

        # Otherwise unregistration is allowed
        return True

    def get_waiting_list_position(self):
        now_utc = timezone.now()
        current_time = now_utc.astimezone(self.timezone())
        local_registration_federal_limit = self.local_registration_federal_limit()
        if self.team_sorting == TeamSortingType.RANK and local_registration_federal_limit is not None:
            if current_time < local_registration_federal_limit:
                return -1

        # If no target team count exists, no one goes to waiting list
        if self.team_count_limit is False:
            return -1

        self.reserved_spots = max(0, self.reserved_spots - 1)
        self.reserved_spots += 1
        self.save()

        # Get count of active teams (not walked out)
        current_team_count = self.team_registrations.exclude(walk_out=True).count() + self.reserved_spots

        # If current count is less than target count, next team is not in waiting list
        if current_team_count < self.team_count:
            return -1

        # If we have a waiting list limit
        if self.waiting_list_limit is not None:
            waiting_list_count = current_team_count - self.team_count
            # If waiting list is full
            if waiting_list_count >= self.waiting_list_limit:
                return -1
            # Return waiting list position
            return waiting_list_count

        # In waiting list with no limit
        return current_team_count - self.team_count

    def build_tournament_type_str(self):
        tournament_details = []
        tournament_details.append(self.level())
        if self.category():
            tournament_details.append(self.category())
        if self.age() and self.federal_age_category != FederalAgeCategory.SENIOR:
            tournament_details.append(self.age())

        return " ".join(filter(None, tournament_details))

    def build_tournament_details_str(self):
        name_str = self.build_name_details_str()
        tournament_details = []
        if self.federal_level_category > 0:
            tournament_details.append(self.level())
        if self.category():
            tournament_details.append(self.category())
        if self.age() and self.federal_age_category != FederalAgeCategory.SENIOR:
            tournament_details.append(self.age())

        if len(name_str) > 0:
            tournament_details.append(name_str)

        return " ".join(filter(None, tournament_details))

    def build_name_details_str(self):
        name_details = []
        if self.name:
            name_details.append(self.name)
        if self.event.name:
            name_details.append(self.event.name)
        name_str = " - ".join(filter(None, name_details))
        if name_str:
            name_str = f"{name_str}"
        return name_str

    def player_register_check(self, licence_id):
        reasons = []
        if not licence_id:
            return None

        data, found = get_player_name_from_csv(self.federal_category, licence_id)
        if not found or not data:
            print("not found or not data")
            return None

        birth_year = data.get('birth_year', None)
        is_woman = data.get('is_woman', None)

        # Check gender category restrictions
        if is_woman is not None and self.federal_category == FederalCategory.WOMEN and is_woman is False:
            reasons.append("Ce tournoi est réservé aux femmes")

        if birth_year is None:
            return reasons if reasons else None

        tournament_start_year = self.season_year()
        user_age = tournament_start_year - int(birth_year)
        # Check age category restrictions

        if self.federal_age_category == FederalAgeCategory.A09_10 and user_age > 10:
            reasons.append("Ce tournoi est réservé aux 10 ans et moins")
        if self.federal_age_category == FederalAgeCategory.A11_12 and user_age > 12:
            reasons.append("Ce tournoi est réservé aux 12 ans et moins")
        if self.federal_age_category == FederalAgeCategory.A13_14 and user_age > 14:
            reasons.append("Ce tournoi est réservé aux 14 ans et moins")
        if self.federal_age_category == FederalAgeCategory.A15_16 and user_age < 11:
            reasons.append("Ce tournoi est réservé aux 11 ans et plus")
        if self.federal_age_category == FederalAgeCategory.A17_18 and user_age < 11:
            reasons.append("Ce tournoi est réservé aux 11 ans et plus")
        if self.federal_age_category == FederalAgeCategory.A15_16 and user_age > 16:
            reasons.append("Ce tournoi est réservé aux 16 ans et moins")
        if self.federal_age_category == FederalAgeCategory.A17_18 and user_age > 18:
            reasons.append("Ce tournoi est réservé aux 18 ans et moins")
        if self.federal_age_category == FederalAgeCategory.SENIOR and user_age < 11:
            reasons.append("Ce tournoi est réservé aux 11 ans et plus")
        if self.federal_age_category == FederalAgeCategory.A45 and user_age < 45:
            reasons.append("Ce tournoi est réservé aux +45 ans")
        if self.federal_age_category == FederalAgeCategory.A55 and user_age < 55:
            reasons.append("Ce tournoi est réservé aux +55 ans")

        addon = 0
        computedRank = int(data.get("rank", 0))
        if is_woman and self.federal_category == FederalCategory.MEN:
            addon = FederalCategory.female_in_male_assimilation_addition(computedRank, tournament_start_year)
        computedRank = computedRank + addon

        if computedRank <= self.min_player_rank():
            name = data['first_name'] + " " + data['last_name'].upper()
            reasons.append(f"{name} ({licence_id}): trop bien classé pour ce tournoi")

        return reasons if reasons else None

    def min_player_rank(self):
        return FederalLevelCategory.min_player_rank(self.federal_level_category, self.federal_category, self.federal_age_category, self.season_year())

    def season_year(self):
        local_date = self.local_start_date()
        if local_date.month >= 9:  # September or later
            return local_date.year + 1
        else:
            return local_date.year

    def local_registration_federal_limit(self):
        timezone = self.timezone()
        if self.registration_date_limit is not None:
            return self.registration_date_limit.astimezone(timezone)

        if self.closed_registration_date is not None:
            return self.closed_registration_date.astimezone(timezone)

        local_start_date = self.local_start_date()

        if local_start_date is None:
            return None

        if self.federal_level_category == FederalLevelCategory.P500:
            # 7 days before at 23:59
            return (local_start_date - timedelta(days=7)).replace(hour=23, minute=59, second=59)
        elif self.federal_level_category in [FederalLevelCategory.P1000,
                                            FederalLevelCategory.P1500,
                                            FederalLevelCategory.P2000]:
            # 14 days before at 23:59
            return (local_start_date - timedelta(days=14)).replace(hour=23, minute=59, second=59)
        return None

    def waiting_list_teams(self, teams):
        now_utc = timezone.now()
        current_time = now_utc.astimezone(self.timezone())
        local_registration_federal_limit = self.local_registration_federal_limit()
        if self.team_sorting == TeamSortingType.RANK and local_registration_federal_limit is not None:
            if current_time < local_registration_federal_limit:
                return None

        if len(teams)<=self.team_count:
            return None
        waiting_teams = [team for team in teams if team.stage == "Attente"]
        return waiting_teams

    def first_waiting_list_team(self, teams):
        waiting_list_team = self.waiting_list_teams(teams)
        if waiting_list_team is None:
            return None
        if len(waiting_list_team) > 0:
            return waiting_list_team[0].team_registration
        else:
            return None

    def broadcasted_prog(self):
        # Get matches from broadcasted_matches_and_group_stages
        matches, _ = self.broadcasted_matches_and_group_stages()

        if not matches:
            return []

        # Get all unfinished matches for courts
        active_matches = [
            m for m in matches
            if m.end_date is None  # Not finished
            and m.court_index is not None
        ]

        now = timezone.now()
        # Group matches by court
        matches_by_court = {}
        courts = set()
        for match in active_matches:
            if match.court_index not in matches_by_court:
                matches_by_court[match.court_index] = []
                courts.add(match.court_index)
            matches_by_court[match.court_index].append(match)

        # Sort matches within each court by start time
        for court in matches_by_court:
            matches_by_court[court].sort(key=lambda m: (
                m.start_date is None,  # None dates come last
                m.start_date if m.start_date else now
            ))

        # Sort courts and organize them into groups of 4
        sorted_courts = sorted(list(courts))
        court_groups = [sorted_courts[i:i+4] for i in range(0, len(sorted_courts), 4)]

        ordered_matches = []
        # For each group of up to 4 courts
        for court_group in court_groups:
            # First row: earliest match for each court
            for court in court_group:
                if court in matches_by_court and matches_by_court[court]:
                    ordered_matches.append(matches_by_court[court][0])
                else:
                    ordered_matches.append({"empty": True})
            # Pad to 4 courts if needed
            while len(ordered_matches) % 4 != 0:
                ordered_matches.append({"empty": True})

            # Second row: next match for each court
            for court in court_group:
                if court in matches_by_court and len(matches_by_court[court]) > 1:
                    ordered_matches.append(matches_by_court[court][1])
                else:
                    ordered_matches.append({"empty": True})
            # Pad to 4 courts if needed
            while len(ordered_matches) % 4 != 0:
                ordered_matches.append({"empty": True})

        # Add unassigned matches at the end if needed
        unassigned_matches = [
            m for m in matches
            if m.end_date is None and m.court_index is None
        ]
        if unassigned_matches:
            ordered_matches.extend(unassigned_matches)

        return ordered_matches

    def get_butterfly_bracket_match_group(self, parent_round=None, double_butterfly_mode=False, display_loser_final=False):
        loser_final = None
        main_rounds_reversed = []

        # Get main bracket rounds (excluding children/ranking matches)
        if double_butterfly_mode:
            main_rounds = self.rounds.filter(
                parent=parent_round,
                group_stage_loser_bracket=False,
                index__lte=3
            ).order_by('-index')
        else:
            main_rounds = self.rounds.filter(
                parent=parent_round,
                group_stage_loser_bracket=False
            ).order_by('-index')

        count = main_rounds.count()
        if display_loser_final and count > 1:
            semi = main_rounds[count - 2]
            loser_final = self.rounds.filter(
                parent=semi,
                group_stage_loser_bracket=False
            ).order_by('index').first()

        # Create serializable match groups data
        serializable_match_groups = []

        # Add first half of each round (from last to semi-finals)
        for round in main_rounds:
            next_round = main_rounds.filter(index=round.index - 1).first()
            match_group = round.prepare_match_group(next_round, parent_round, loser_final, double_butterfly_mode, False)
            if match_group:
                serializable_match_groups.append(match_group)

        if double_butterfly_mode:
            main_rounds_reversed = list(main_rounds)
            main_rounds_reversed.reverse()
            for round in main_rounds_reversed:
                if round.index > 0:
                    next_round = main_rounds.filter(index=round.index - 1).first()
                    match_group = round.prepare_match_group(next_round, parent_round, None, double_butterfly_mode, True)
                    if match_group:
                        serializable_match_groups.append(match_group)

        return serializable_match_groups

    def has_bracket(self):
        main_rounds = self.rounds.filter(
            parent=None,
            group_stage_loser_bracket=False
        )

        count = main_rounds.count()
        if count == 0:
            return False
        else:
            return True

    def umpire_contact(self):
        if self.umpire_custom_contact is not None:
            return self.umpire_custom_contact
        if self.event and self.event.creator:
            return self.event.creator.full_name()
        else:
            return None

    def umpire_mail(self):
        if self.umpire_custom_mail is not None:
            return self.umpire_custom_mail
        return self.event.creator.email

    def umpire_phone(self):
        if self.umpire_custom_phone is not None:
            return self.umpire_custom_phone
        return self.event.creator.phone

    def calculate_time_to_confirm(self, waiting_list_count):
        """
        Calculate the time a team has to confirm their registration
        based on tournament proximity, waiting list pressure, and business hours.

        Args:
            tournament: The Tournament instance
            waiting_list_count: Waiting List count

        Returns:
            datetime: The confirmation deadline datetime
        """
        # Skip if feature not enabled
        if self.automatic_waiting_list() is False:
            return None

        config = settings.TOURNAMENT_SETTINGS
        TIME_PROXIMITY_RULES = config['TIME_PROXIMITY_RULES']
        WAITING_LIST_RULES = config['WAITING_LIST_RULES']
        BUSINESS_RULES = config['BUSINESS_RULES']

        # 1. Get current time in tournament's timezone
        current_time = timezone.now()
        current_time = current_time.astimezone(self.timezone())
        tournament_start_date = self.local_start_date()

        # 2. Calculate tournament proximity (hours until tournament starts)
        hours_until_tournament = (tournament_start_date - current_time).total_seconds() / 3600

        # 3. Calculate waiting list pressure

        # teams = self.teams(True)
        # waiting_teams = self.waiting_list_team(teams)
        # if waiting_teams is None:
        #     return None

        # waiting_list_count = len(waiting_teams)

        # 4. Determine base minutes to confirm based on time proximity
        time_based_minutes = TIME_PROXIMITY_RULES["default"]
        for hours_threshold, minutes in TIME_PROXIMITY_RULES.items():
            if hours_threshold != "default" and hours_until_tournament <= hours_threshold:
                time_based_minutes = minutes
                break

        # 5. Determine waiting list based minutes
        waitlist_based_minutes = WAITING_LIST_RULES["default"]
        for teams_threshold, minutes in WAITING_LIST_RULES.items():
            if teams_threshold != "default" and waiting_list_count >= teams_threshold:
                waitlist_based_minutes = minutes
                break

        # 6. Use the more restrictive rule (smaller time window)
        minutes_to_confirm = min(time_based_minutes, waitlist_based_minutes)

        # 7. Check urgency overrides
        apply_business_rules = True

        # Default business hours
        business_start_hour = BUSINESS_RULES["hours"]["start"]
        business_end_hour = BUSINESS_RULES["hours"]["end"]
        # for hours_threshold, override in URGENCY_OVERRIDE["thresholds"].items():
        #     if hours_until_tournament <= hours_threshold:
        #         apply_business_rules = False
        #         # Ensure minimum response time
        #         minutes_to_confirm = max(minutes_to_confirm,
        #                                URGENCY_OVERRIDE["minimum_response_time"] / 10 if getattr(settings, 'LIVE_TESTING', False)
        #                                else URGENCY_OVERRIDE["minimum_response_time"])
        #         break

        # Adjust business hours based on tournament proximity
        if hours_until_tournament <= 24:
            # 24 hours before tournament: 7am - 10pm
            business_start_hour = 7
            business_end_hour = 22
            minutes_to_confirm = config['MINIMUM_RESPONSE_TIME']

        if hours_until_tournament <= 12:
            # 12 hours before tournament: 6am - 1am (next day)
            business_start_hour = 6
            business_end_hour = 25  # 1am next day (25 in 24-hour format)
            minutes_to_confirm = config['MINIMUM_RESPONSE_TIME']

        live_testing = getattr(settings, 'LIVE_TESTING', False)
        # Divide by 10 if LIVE_TESTING is enabled
        if live_testing:
            minutes_to_confirm = minutes_to_confirm / 10

        # 8. Calculate raw deadline
        raw_deadline = current_time + timezone.timedelta(minutes=minutes_to_confirm)

        # 9. Round up to next interval mark based on BACKGROUND_SCHEDULED_TASK_INTERVAL
        interval = settings.BACKGROUND_SCHEDULED_TASK_INTERVAL
        minute = raw_deadline.minute
        if minute % interval != 0:
            # Minutes to next interval mark
            minutes_to_add = interval - (minute % interval)
            raw_deadline += timezone.timedelta(minutes=minutes_to_add)

        # 10. Apply business hours rules if needed
        if apply_business_rules and live_testing is False:
            # Check if deadline falls outside business hours
            before_hours = raw_deadline.hour < business_start_hour
            after_hours = raw_deadline.hour >= business_end_hour

            if before_hours or after_hours:
                # Extend to next business day
                if after_hours:
                    # Move to next day
                    days_to_add = 1
                    raw_deadline += timezone.timedelta(days=days_to_add)

                # Set to business start hour
                raw_deadline = raw_deadline.replace(
                    hour=business_start_hour,
                    minute=0,
                    second=0,
                    microsecond=0
                )

                raw_deadline += timezone.timedelta(minutes=minutes_to_confirm)
                print(f"Before hours: {before_hours}, After hours: {after_hours}")
                print(f"Final deadline after adding confirmation time: {raw_deadline}")

        tournament_start_date_minus_five = tournament_start_date - timedelta(minutes=5)
        if raw_deadline >= tournament_start_date_minus_five:
            print(f"Raw Deadline is after tournament_start_date_minus_five: {raw_deadline}, {tournament_start_date_minus_five}")
            raw_deadline = tournament_start_date_minus_five

        raw_deadline = raw_deadline.replace(
            second=0,
            microsecond=0
        )

        print(f"Live testing: {live_testing}")
        print(f"Current time: {current_time}")
        print(f"Minutes to confirm: {minutes_to_confirm}")
        print(f"Raw deadline before rounding: {current_time + timezone.timedelta(minutes=minutes_to_confirm)}")
        print(f"Raw deadline after rounding: {raw_deadline}")
        print(f"Apply business rules: {apply_business_rules}")

        return raw_deadline

    def is_online_registration_irrevelant(self):
        return self.enable_time_to_confirm is False or self.has_started() or self.has_ended() or self.is_canceled() or self.is_deleted

    @property
    def week_day(self):
        """Return the weekday name (e.g., 'Monday')"""
        date = self.local_start_date()
        return date_format(date, format='D') + '.' # 'l' gives full weekday name

    @property
    def day(self):
        """Return the day of the month"""
        date = self.local_start_date()
        return date.day

    @property
    def month(self):
        """
        Return the month name in lowercase:
        - If full month name is 4 letters or fewer, return as is
        - If more than 4 letters, return first 4 letters with a dot
        """
        date = self.local_start_date()
        # Get full month name and convert to lowercase
        full_month = date_format(date, format='F').lower()

        # Check if the month name is 5 letters or fewer
        if len(full_month) <= 5:
            return full_month
        else:
            # Truncate to 5 letters and add a dot
            return f"{full_month[:5]}."

    @property
    def year(self):
        """Return the year"""
        date = self.local_start_date()
        return date.year

    @property
    def localized_day_duration(self):
        """
        Return localized day duration in French:
        - If multiple days: '2 jours', '3 jours', etc.
        - If 1 day and starts after 18:00: 'soirée'
        - If 1 day and starts before 18:00: 'journée'
        """
        # Assuming day_duration is a property or field that returns the number of days
        days = self.day_duration

        if days > 1:
            return f"{days} jours"
        else:
            # For single day events, check the starting hour
            start_time = self.local_start_date().time()
            evening_threshold = time(18, 0)  # 18:00 (6 PM)

            if start_time >= evening_threshold:
                return "soirée"
            else:
                return "journée"

    def get_player_registration_status_by_licence(self, user):
        user_player = self.get_user_registered(user)
        if user_player:
            return user_player.get_registration_status()
        return None

    def get_user_registered(self, user):
        if not user.is_authenticated:
            return None

        PlayerRegistration = apps.get_model('tournaments', 'PlayerRegistration')

        # First, try to find a registration directly linked to the user
        direct_registration = PlayerRegistration.objects.filter(
            team_registration__tournament=self,
            user=user,
            team_registration__walk_out=False
        ).first()

        if direct_registration:
            return direct_registration

        # If no direct registration found and user has no license, return None
        if not user.licence_id:
            return None

        # Validate the license format
        validator = LicenseValidator(user.licence_id)
        if not validator.validate_license():
            return None

        # Get the stripped license (without check letter)
        stripped_license = validator.stripped_license

        # Fall back to checking by license ID
        return PlayerRegistration.objects.filter(
            team_registration__tournament=self,
            licence_id__icontains=stripped_license,
            team_registration__walk_out=False
        ).first()

    def is_user_registered(self, user):
        return self.get_user_registered(user) is not None

    def get_user_team_registration(self, user):
        user_registered = self.get_user_registered(user)
        if user_registered:
            return user_registered.team_registration
        else:
            return None

    def should_request_payment(self):
        if self.enable_online_payment:
            return True
        else:
            return False

    def is_refund_possible(self):
        if self.enable_online_payment_refund:
            time = timezone.now()
            if self.refund_date_limit:
                if time <= self.refund_date_limit:
                    return True
                else:
                    return False
            else:
                return True
        else:
            return False

    def is_free(self):
        if self.entry_fee is not None and self.entry_fee == 0:
            return True
        elif self.entry_fee is None:
            return True
        else:
            return False

    def effective_commission_rate(self):
        """Get the commission rate for this tournament, falling back to the umpire default if not set"""
        return 1.00  # Fallback default

    def check_all_confirmation_deadlines(self):
        """
        Check all confirmation deadlines for teams in this tournament.
        Send notification emails as needed.

        Returns:
            int: Number of teams processed
        """

        # Calculate these values once for the tournament
        teams = self.teams(True)
        waiting_list_teams = self.waiting_list_teams(teams)
        ttc = self.calculate_time_to_confirm(len(waiting_list_teams)) if waiting_list_teams is not None else None
        first_waiting_list_team = self.first_waiting_list_team(teams)

        # Tournament context dict to pass to each team check
        tournament_context = {
            'ttc': ttc,
            'first_waiting_list_team': first_waiting_list_team,
            'is_online_registration_irrevelant': self.is_online_registration_irrevelant()
        }

        # Find players with expired confirmation deadlines in this tournament
        PlayerRegistration = apps.get_model('tournaments', 'PlayerRegistration')
        expired_confirmations = PlayerRegistration.objects.filter(
            registration_status=RegistrationStatus.PENDING,
            registered_online=True,
            team_registration__tournament=self
        ).select_related('team_registration')

        processed_teams = set()  # To avoid processing the same team multiple times
        teams_processed = 0

        for player in expired_confirmations:
            team_registration = player.team_registration

            # Skip if we've already processed this team
            if team_registration.id in processed_teams:
                continue

            processed_teams.add(team_registration.id)
            teams_processed += 1

            # Process in a transaction to ensure atomic operations
            team_registration.check_confirmation_deadline(tournament_context)

        return teams_processed

    def planned_matches_by_day(self, day=None, all=False, event_mode=False, broadcast=False):
        """
        Collect all matches from tournaments and group them by their planned_start_date.

        Parameters:
        - day: Optional date string in format 'YYYY-MM-DD'. If provided, returns matches for that day only.

        Returns:
        - days: List of unique days found (datetime.date objects)
        - match_groups: Dictionary of match groups by date and hour or just for the selected day
        """
        event = self.event
        tournaments_count = 1
        if event:
            tournaments_count = event.tournaments.count()

        if event_mode is True and tournaments_count == 1:
            event_mode = False

        show_teams_in_prog = False
        if tournaments_count == 1:
            show_teams_in_prog = self.show_teams_in_prog
        elif event:
            show_teams_in_prog = event.tournaments.filter(show_teams_in_prog=True).first() is not None

        # Get all matches from rounds and group stages - use a set to avoid duplicates
        all_matches = set()

        tournaments = [self]
        if event_mode is True and event:
            tournaments = event.tournaments.all()

        # Check if all tournaments have started - if so, always show teams
        all_started = True
        for t in tournaments:
            if not t.has_started():
                all_started = False
                break
        if all_started:
            show_teams_in_prog = True

        for tournament in tournaments:
            # Get matches only from top-level rounds to avoid duplicates
            for round in tournament.rounds.filter(parent=None).all():
                round_matches = round.get_matches_recursive(False)
                # Add to set using IDs to avoid duplicates
                for match in round_matches:
                    all_matches.add(match)

            # Get matches from group stages
            for group_stage in tournament.group_stages.all():
                for match in group_stage.matches.all():
                    all_matches.add(match)

        # Filter matches with planned_start_date - convert back to list
        planned_matches = [match for match in all_matches if match.planned_start_date and not match.disabled]

        if not planned_matches:
            return [], []

        # Group matches by day
        matches_by_day = {}
        days = set()

        for match in planned_matches:
            # Convert to local time zone
            local_date = timezone.localtime(match.planned_start_date)
            day_key = local_date.date()
            days.add(day_key)

            if day_key not in matches_by_day:
                matches_by_day[day_key] = []

            matches_by_day[day_key].append(match)

        # Sort days
        sorted_days = sorted(list(days))
        # Create match groups for the selected day
        match_groups = []
        hide_teams = show_teams_in_prog == False
        # When broadcast=True, handle all days with matches
        if broadcast:
            today = timezone.now().replace(hour=0, minute=0, second=0, microsecond=0).date()
            sorted_days = [day for day in sorted(list(days)) if day >= today]

            # Process all days with matches
            for selected_day in sorted_days:

                # Group matches by hour
                matches_by_hour = {}
                for match in matches_by_day[selected_day]:
                    local_time = timezone.localtime(match.planned_start_date)
                    hour_key = local_time.strftime('%H:%M')

                    if hour_key not in matches_by_hour:
                        matches_by_hour[hour_key] = []

                    matches_by_hour[hour_key].append(match)

                # Create match groups for each hour
                for hour, matches in sorted(matches_by_hour.items()):
                    # Sort matches by court if available
                    matches.sort(key=lambda m: (m.court_index if m.court_index is not None else 999))

                    local_date = matches[0].local_planned_start_date()
                    formatted_name = formats.date_format(local_date, format='l j F à H:i').capitalize()
                    mg = self.create_match_group(
                        name=formatted_name,
                        matches=matches,
                        round_id=None,
                        round_index=None,
                        hide_teams=hide_teams,
                        event_mode=event_mode,
                        broadcast=broadcast
                    )
                    match_groups.append(mg)
            return sorted_days, match_groups

        if all or day is None:
            today = timezone.now().replace(hour=0, minute=0, second=0, microsecond=0).date()
            if today in days:
                selected_day = today
            else:
                # Default to first day if today is not in the list
                if self.has_ended():
                    selected_day = sorted_days[-1]
                else:
                    selected_day = sorted_days[0]

            # Group matches by hour
            matches_by_hour = {}
            for match in matches_by_day[selected_day]:
                local_time = timezone.localtime(match.planned_start_date)
                hour_key = local_time.strftime('%H:%M')

                if hour_key not in matches_by_hour:
                    matches_by_hour[hour_key] = []

                matches_by_hour[hour_key].append(match)

            # Create match groups for each hour
            for hour, matches in sorted(matches_by_hour.items()):
                # Sort matches by court if available
                matches.sort(key=lambda m: (m.court_index if m.court_index is not None else 999))

                local_date = matches[0].local_planned_start_date()
                formatted_name = formats.date_format(local_date, format='l j F à H:i').capitalize()
                mg = self.create_match_group(
                    name=formatted_name,
                    matches=matches,
                    round_id=None,
                    round_index=None,
                    hide_teams=hide_teams,
                    event_mode=event_mode,
                    broadcast=broadcast
                )
                match_groups.append(mg)
            return sorted_days, match_groups

        # If specific day requested, filter to that day
        selected_day = None
        if day:
            try:
                # Parse the day string to a date object
                selected_day = datetime.strptime(day, '%Y-%m-%d').date()
                if selected_day not in days:
                    selected_day = sorted_days[0] if sorted_days else None
            except (ValueError, TypeError):
                selected_day = sorted_days[0] if sorted_days else None
        else:
            selected_day = sorted_days[0] if sorted_days else None

        if selected_day and selected_day in matches_by_day:
            # Group matches by hour
            matches_by_hour = {}
            for match in matches_by_day[selected_day]:
                local_time = timezone.localtime(match.planned_start_date)
                hour_key = local_time.strftime('%H:%M')

                if hour_key not in matches_by_hour:
                    matches_by_hour[hour_key] = []

                matches_by_hour[hour_key].append(match)

            # Create match groups for each hour
            for hour, matches in sorted(matches_by_hour.items()):
                # Sort matches by court if available
                matches.sort(key=lambda m: (m.court_index if m.court_index is not None else 999))

                local_date = matches[0].local_planned_start_date()
                formatted_name = formats.date_format(local_date, format='l j F à H:i').capitalize()
                mg = self.create_match_group(
                    name=formatted_name,
                    matches=matches,
                    round_id=None,
                    round_index=None,
                    hide_teams=hide_teams,
                    event_mode=event_mode,
                    broadcast=broadcast
                )
                match_groups.append(mg)

        return sorted_days, match_groups

    def has_sponsors(self):
        return self.event.images.exists()

    def is_cart_player_from_club(self, player_data):
        player_club_code = player_data.get('club_code', None)
        if player_club_code is None or len(player_club_code) == 0:
            return False
        club_code = self.event.club.code
        if club_code is None or len(club_code) == 0:
            return False
        player_club_code = player_club_code.replace(" ", "")
        club_code = club_code.replace(" ", "")
        return player_club_code.lower() == club_code.lower()

    def localized_start_time(self):
        return formats.date_format(self.local_start_date(), format='H:i').capitalize()

    def court_names(self):
        if self.event.club is None:
            return [f"Piste {i+1}" for i in range(self.court_count)]
        return [f"{self.event.club.court_name(i)}" for i in range(self.court_count)]

class MatchGroup:
    def __init__(self, name, matches, formatted_schedule, round_id=None, round_index=None):
        self.name = name
        self.matches = matches
        self.formatted_schedule = formatted_schedule
        self.round_id = round_id
        self.round_index = round_index

    def add_match(self, match):
        self.matches.append(match)

    def add_matches(self, matches):
        self.matches = matches

    def to_dict(self):
        return {
            'name': self.name,
            'round_id': self.round_id,
            'round_index': self.round_index,
            'matches': [match.to_dict() for match in self.matches]
        }

class TeamSummon:
    def __init__(self, id, names, date, weight, stage, court, image, day_duration):
        self.id = str(id)
        self.names = names
        self.date = date
        self.weight = weight
        self.stage = stage
        self.court = court
        self.image = image
        self.day_duration = day_duration

    def formatted_date(self):
        if self.date:
            if self.day_duration >= 7:
                return formats.date_format(self.date, format='l d M H:i')
            else:
                return formats.date_format(self.date, format='l H:i')
        else:
            return ''

    def to_dict(self):
        return {
            "names": self.names,
            "date": self.formatted_date(),
            "weight": self.weight,
            "stage": self.stage,
            "court": self.court,
            "image": self.image,
        }

class TeamItem:
    def __str__(self):
        return f"TeamItem({self.team_registration.id}, names={self.names}, stage={self.stage})"

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

    def __init__(self, team_registration):
        self.names = team_registration.team_names()
        self.date = team_registration.local_call_date()
        self.registration_date = team_registration.registration_date
        if team_registration.players_sorted_by_rank.count() == 0:
            weight = None
        else:
            weight = team_registration.weight
        self.weight = weight
        self.initial_weight = team_registration.initial_weight()
        self.image = team_registration.logo
        self.stage = ""
        self.team_registration = team_registration
        self.wildcard_bracket = team_registration.wild_card_bracket
        self.wildcard_groupstage = team_registration.wild_card_group_stage

    def set_stage(self, stage):
        self.stage = stage

    def to_dict(self):
        return {
            "names": self.names,
            "date": self.date,
            "registration_date": self.registration_date,
            "weight": self.weight,
            "initial_weight": self.initial_weight,
            "image": self.image,
            "stage": self.stage,
            "wildcard_bracket": self.wildcard_bracket,
            "wildcard_groupstage": self.wildcard_groupstage,
        }

class TeamRanking:
    def __init__(self, id, names, ranking, points, image):
        self.id = str(id)
        self.names = names
        self.ranking = ranking
        self.formatted_ranking = self.ordinal(ranking)
        self.points = self.points_earned_display(points)
        self.image = image

    # def ranking_display(self):
    #     return self.ordinal(self.ranking)

    def points_earned_display(self, points):
        if points is None:
            return ""
        return f"+{points} pt{self.plural_suffix(points)}"

    def plural_suffix(self, n):
        if n > 1:
            return 's'
        else:
            return ''

    def ordinal(self, n):
        if n == 1:
            suffix = 'er'
        else:
            suffix = 'ème'
        return str(n) + suffix

    def to_dict(self):
        return {
            "names": self.names,
            "ranking": self.ranking,
            "formatted_ranking": self.formatted_ranking,
            "points": self.points,
            "image": self.image,
        }
