from django.db import models
from typing import TYPE_CHECKING
if TYPE_CHECKING:
    from tournaments.models import group_stage

from . import Event, TournamentPayment, FederalMatchCategory, FederalCategory, FederalLevelCategory, FederalAgeCategory
import uuid
from django.utils import timezone, formats
from datetime import datetime, timedelta
from .. import config_local
from ..utils.cryptography import decrypt_aes_gcm
from ..utils.extensions import plural_format

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

class Tournament(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=True)
    event = models.ForeignKey(Event, blank=True, null=True, on_delete=models.CASCADE)
    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)

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

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

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

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

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

    def short_base_name(self):
        category = self.category()
        if len(category) > 0:
            return f"{self.level()}{category[0]}"
        else:
            return self.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.name:
            components.append(self.event.name)
        elif self.name:
            components.append(self.name)
        return (' ').join(components)

    def level(self):
        if self.federal_level_category == 0:
            return "Anim."
        return self.get_federal_level_category_display()

    def category(self):
        return self.get_federal_category_display()

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

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

    def creator(self):
        return self.event.creator.username

    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()
        if teams is not None and len(teams) > 0:
            return f"{len(teams)} équipes inscrites"
        else:
            return None

    def tournament_status_display(self):
            if self.is_canceled() is True:
                return "Annulé"

            teams = self.teams()
            if self.supposedly_in_progress() or self.end_date is not None:
                teams = [t for t in teams if t.stage != "Attente"]
                if teams is not None and len(teams) > 0:
                    word = "équipe"
                    if len(teams) > 1:
                        word = word + "s"
                    return f"{len(teams)} {word}"
                else:
                    return None
            if teams is not None and len(teams) > 0:
                word = "inscription"
                if len(teams) > 1:
                    word = word + "s"
                return f"{len(teams)} {word}"
            else:
                return None

    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 = []
        for team_registration in self.teamregistration_set.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(names, next_match.start_date, weight, stage, team_registration.logo)
                    summons.append(summon)

        summons.sort(key=lambda s: s.date)
        return summons

    def rankings(self):
        rankings = []
        for team_registration in self.teamregistration_set.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(names, ranking, points, team_registration.logo)
                rankings.append(team)

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

    def teams(self):
        bracket_teams = []
        group_stage_teams = []
        waiting_teams = []
        teams = []
        wildcard_bracket = []
        wildcard_group_stage = []
        complete_teams = []
        closed_registration_date = self.closed_registration_date

        for team_registration in self.teamregistration_set.all():
            is_valid = False
            if closed_registration_date is not None and team_registration.registration_date is not None and team_registration.registration_date <= closed_registration_date:
                is_valid = True
            if closed_registration_date is None:
                is_valid = True
            if team_registration.registration_date is None:
                is_valid = True

            if team_registration.walk_out is False:
                names = team_registration.team_names()
                weight = team_registration.weight
                initial_weight = team_registration.initial_weight()
                date = team_registration.registration_date
                team = TeamList(names, weight, date, initial_weight, team_registration.logo)
                if team_registration.group_stage_position is not None:
                    team.set_stage("Poule")
                elif team_registration.bracket_position is not None:
                    team.set_stage("Tableau")
                else:
                    team.set_stage("Attente")

                teams.append(team)
                if team_registration.wild_card_bracket:
                    wildcard_bracket.append(team)
                elif team_registration.wild_card_group_stage:
                    wildcard_group_stage.append(team)
                elif is_valid is True:
                    complete_teams.append(team)
                else:
                    waiting_teams.append(team)


        if len(teams) < self.team_count:
            teams.sort(key=lambda s: (s.initial_weight, s.date))
            return teams

        seeds_count = min(self.team_count, len(teams)) - self.group_stage_count * self.teams_per_group_stage - len(wildcard_bracket)
        group_stage_members_count = self.group_stage_count * self.teams_per_group_stage - len(wildcard_group_stage)
        if group_stage_members_count < 0:
            group_stage_members_count = 0
        if seeds_count < 0:
            seeds_count = 0

        if self.team_sorting == TeamSortingType.INSCRIPTION_DATE:
            complete_teams.sort(key=lambda s: (s.date, s.initial_weight))
        else:
            complete_teams.sort(key=lambda s: (s.initial_weight, s.date))

        selected_teams = complete_teams[:self.team_count]
        selected_teams.sort(key=lambda s: s.initial_weight)

        if seeds_count > 0:
            bracket_teams = selected_teams[:seeds_count] + wildcard_bracket
        else:
            bracket_teams = []

        if group_stage_members_count:
            group_stage_teams = selected_teams[-group_stage_members_count:] + wildcard_group_stage
        else:
            group_stage_teams = []

        waiting_list_count = len(complete_teams) - self.team_count
        if waiting_list_count < 0:
            waiting_list_count = 0

        if waiting_list_count > 0 or len(waiting_teams) > 0:
            if waiting_list_count > 0:
                waiting_teams = waiting_teams + complete_teams[-waiting_list_count:]
            if self.team_sorting == TeamSortingType.INSCRIPTION_DATE:
                waiting_teams.sort(key=lambda s: (s.date, s.initial_weight))
            else:
                waiting_teams.sort(key=lambda s: (s.initial_weight, s.date))
        else:
             waiting_teams = []

        bracket_teams.sort(key=lambda s: s.weight)
        group_stage_teams.sort(key=lambda s: s.weight)

        for team in bracket_teams:
            if team.stage == "Attente":
                team.set_stage("Tableau")

        for team in group_stage_teams:
            if team.stage == "Attente":
                team.set_stage("Poule")

        for team in waiting_teams:
            team.set_stage("Attente")

        final_teams = bracket_teams + group_stage_teams + waiting_teams
        return final_teams

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

        match_groups = []
        if group_stage_id:
            group_stage = self.groupstage_set.filter(id=group_stage_id).first()
            match_groups.append(self.group_stage_match_group(group_stage, broadcasted, hide_empty_matches=False))
        elif round_id:
            round = self.round_set.filter(id=round_id).first()
            if round:
                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 = []
        for round in self.round_set.filter(parent=None).all().order_by('index'):
            groups.extend(self.round_match_groups(round, broadcasted, hide_empty_matches=True))

        if self.display_group_stages():
            for group_stage in self.groupstage_set.all().order_by('index'):
                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):
        matches = group_stage.match_set.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 matches:
            return self.create_match_group(group_stage.display_name(), matches, broadcasted)
        else:
            return None

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

        matches = round.match_set.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 matches:
            group = self.create_match_group(round.name(), matches, broadcasted)
            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:
                group = self.create_match_group('Matchs de classement', ranking_matches, broadcasted)
                groups.append(group)

        return groups

    def create_match_group(self, name, matches, broadcasted):
        matches = list(matches)
        matches.sort(key=lambda m: m.index)
        live_matches = [match.live_match() for match in matches]
        return MatchGroup(name, live_matches)

    def live_group_stages(self):
        group_stages = list(self.groupstage_set.all())
        group_stages.sort(key=lambda gs: gs.index)
        return [gs.live_group_stages() for gs in group_stages]

    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.
        if datetime.now().date() < self.start_date.date():
            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,
                }
            else:
                live_matches_dicts = [match.live_match().to_dict() for match in matches]
                return {
                    'matches': live_matches_dicts,
                    'group_stages': [],
                    'summons': team_summons_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': [],
            }

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

        if len(self.groupstage_set.all()) > 0 and self.no_bracket_match_has_started():
            group_stages = self.live_group_stages()
            matches = self.broadcasted_group_stages_matches()
            first_round = self.first_round()
            if first_round:
                matches.extend(first_round.get_matches_recursive(True))
        else:
            current_round = self.round_to_show()
            if current_round:

                # Add full matches from the next rounds
                next_round = self.round_for_index(current_round.index - 1)
                if next_round:
                    matches.extend(next_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:
                    matches.extend(current_round.get_matches_recursive(True))
                    matches.extend(previous_round.get_matches_recursive(True))
                else:
                    matches.extend(current_round.all_matches(True))
                    group_stages = self.live_group_stages()

        return matches, group_stages

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

    def all_matches(self, hide_empty_matches):
        matches = []
        for round in self.round_set.all():
            matches.extend(round.all_matches(hide_empty_matches))
        for group_stage in self.groupstage_set.all():
            matches.extend(group_stage.match_set.all())

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

        return matches

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

    def group_stages_running(self):
        if len(self.groupstage_set.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]
        matches.sort(key=lambda m: m.start_date)
        if matches:
            return matches[0]
        else:
            return None

    def round_to_show(self):
        last_started_match = self.first_unfinished_match()
        if last_started_match:
            current_round = last_started_match.round.root_round()
            if current_round:
                return current_round
        main_rounds = list(self.round_set.filter(parent=None).all())
        main_rounds.sort(key=lambda r: r.index)
        if main_rounds:
            return main_rounds[0]
        else:
            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]

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

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

    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.match_set.all())
        matches = [m for m in matches if m.should_appear()]
        matches.sort(key=lambda m: (m.start_date is None, m.end_date is not None, m.start_date, m.index))
        return matches

    def elected_broadcast_group_stages(self):
        group_stages = list(self.groupstage_set.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.publish_rankings is True and self.end_date is not None:
            return True
        return False

    def display_tournament(self):
        if self.publish_tournament:
            return True
        is_build_and_not_empty = self.is_build_and_not_empty()
        if self.end_date is not None:
            if self.is_canceled is True:
                return is_build_and_not_empty
            return True
        if is_build_and_not_empty is True:
            if datetime.now().date() >= self.start_date.date():
                return True
            minimum_publish_date = self.creation_date.replace(hour=9, minute=0) + timedelta(days=1)
            return timezone.now() >= minimum_publish_date
        return False

    def display_teams(self):
        if self.end_date is not None:
            return True
        if self.publish_teams:
            return True
        if timezone.now().date() >= self.start_date.date():
            return True
        return False

    def display_summons(self):
        if self.end_date is not None:
            return False
        if self.publish_summons:
            return True
        if timezone.now() >= self.start_date:
            return True
        return False

    def display_group_stages(self):
        if self.end_date is not None:
            return True
        if len(self.groupstage_set.all()) == 0:
            return False
        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 timezone.now() >= self.start_date
        else:
            return timezone.now() >= first_group_stage_start_date

        # group_stage_start_date = self.getEightAm(first_group_stage_start_date)

        # if group_stage_start_date < self.start_date:
        #     group_stage_start_date = self.start_date

        # if datetime.now().date() >= group_stage_start_date.date():
        #     return True

        # return False

    def group_stage_start_date(self):
        group_stages = [gs for gs in self.groupstage_set.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 datetime.now().date() >= self.start_date.date()

        bracket_start_date = self.getEightAm(first_match_start_date)
        if bracket_start_date < self.start_date:
            bracket_start_date = self.start_date

        if datetime.now().date() >= bracket_start_date.date():
            return True

        return False

    def bracket_matches(self):
        matches = []
        for round in self.round_set.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)

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

    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.federal_level_category == FederalLevelCategory.UNLISTED

    def is_build_and_not_empty(self):
        return (len(self.groupstage_set.all()) > 0 or len(self.round_set.all()) > 0) and len(self.teamregistration_set.all()) >= 4

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

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

class MatchGroup:
    def __init__(self, name, matches):
        self.name = name
        self.matches = matches

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

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

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

    def formatted_date(self):
        if self.date:
            timezoned_datetime = timezone.localtime(self.date)
            return formats.date_format(timezoned_datetime, format='l H:i')
        else:
            return None

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

class TeamList:
    def __init__(self, names, weight, date, initial_weight, image):
        self.names = names
        self.date = date
        self.weight = weight
        self.initial_weight = initial_weight
        self.image = image
        self.stage = ""

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

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

class TeamRanking:
    def __init__(self, names, ranking, points, image):
        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):
        suffixes = {1: 'er', 2: 'ème', 3: 'rd'}
        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,
        }
