import logging
import uuid
from datetime import datetime
from typing import Optional
from math import ceil, log2

from src.ezgg_lan_manager.types.Match import Match, FFAMatch
from src.ezgg_lan_manager.types.Participant import Participant
from src.ezgg_lan_manager.types.TournamentBase import GameTitle, TournamentFormat, TournamentStatus, TournamentError, Bracket, MatchStatus, ParticipantType

logger = logging.getLogger(__name__.split(".")[-1])

class Tournament:
    def __init__(self,
                 id_: int,
                 name: str,
                 description: str,
                 game_title: GameTitle,
                 format_: TournamentFormat,
                 start_time: datetime,
                 status: TournamentStatus,
                 participants: list[Participant],
                 matches: Optional[tuple[Match]],
                 rounds: list[list[Match]],
                 max_participants: int,
                 participant_type: ParticipantType) -> None:
        self._id = id_
        self._name = name
        self._description = description
        self._game_title = game_title
        self._format = format_
        self._start_time = start_time
        self._status = status
        self._participants = participants
        self._matches = matches
        self._rounds = rounds
        self._max_participants = max_participants
        self._participant_type = participant_type

    @property
    def id(self) -> int:
        return self._id

    @property
    def name(self) -> str:
        return self._name

    @property
    def game_title(self) -> GameTitle:
        return self._game_title

    @property
    def format(self) -> TournamentFormat:
        return self._format

    @property
    def start_time(self) -> datetime:
        return self._start_time

    @property
    def status(self) -> TournamentStatus:
        return self._status

    @status.setter
    def status(self, new_status: TournamentStatus) -> None:
        if new_status == TournamentStatus.OPEN and self._status == TournamentStatus.CLOSED and self._matches is not None:
            # Deletes all tournament preparation !
            self._matches = None
        self._status = new_status


    @property
    def participants(self) -> list[Participant]:
        return self._participants

    @property
    def matches(self) -> list[Match]:
        return self._matches if self._matches else []

    @property
    def max_participants(self) -> int:
        return self._max_participants

    @property
    def description(self) -> str:
        return self._description

    @property
    def is_full(self) -> bool:
        return len(self._participants) >= self._max_participants

    @property
    def participant_type(self) -> ParticipantType:
        return self._participant_type

    def add_participant(self, participant: Participant) -> None:
        if participant.id in (p.id for p in self._participants):
            raise TournamentError(f"Participant with ID {participant.id} already registered for tournament")
        self._participants.append(participant)

    def remove_participant(self, participant: Participant) -> None:
        if participant.id not in (p.id for p in self._participants):
            raise TournamentError(f"Participant with ID {participant.id} not registered for tournament")
        # ToDo: Check if tournament already started => correctly resolve matches with now missing participant
        self._participants.remove(participant)

    def cancel(self):
        self.status = TournamentStatus.CANCELED

    def match_has_ended_callback(self, match: Match) -> None:
        if self._matches is None:
            return

        winner = match.winner
        next_match = next((m for m in self._matches if m.match_id == match.next_match_win_id), None)
        if next_match is not None:
            try:
                next_match.assign_participant(winner, 1)
            except TournamentError:
                next_match.assign_participant(winner, 2)
        else:  # No next match = final round
            pass

        if match.next_match_lose_id is not None:
            loser = next(p for p in match.participants if p.participant_id != winner)
            next_match = next((m for m in self._matches if m.match_id == match.next_match_lose_id), None)
            if next_match is not None:
                try:
                    next_match.assign_participant(loser.participant_id, 1)
                except TournamentError:
                    next_match.assign_participant(loser.participant_id, 2)
            else:  # No next match = final round
                pass

    def start(self) -> None:
        """ This builds the tournament tree and sets it to ONGOING """

        def parse_format(fmt: TournamentFormat) -> tuple[str, int]:
            if fmt.name.startswith("SINGLE_ELIMINATION"):
                bracket = "SINGLE"
            elif fmt.name.startswith("DOUBLE_ELIMINATION"):
                bracket = "DOUBLE"
            elif fmt.name.startswith("FFA"):
                bracket = "FINAL"
            else:
                raise TournamentError(f"Unsupported tournament format: {fmt}")

            if fmt.name.endswith("_BO_1") or fmt.name.endswith("FFA"):
                bo = 1
            elif fmt.name.endswith("_BO_3"):
                bo = 3
            elif fmt.name.endswith("_BO_5"):
                bo = 5
            else:
                raise TournamentError(f"Unsupported best-of in format: {fmt}")

            return bracket, bo

        if len(self._participants) < 2:
            raise TournamentError("Cannot start tournament: not enough participants")

        bracket_type, best_of = parse_format(self._format)
        num_participants = len(self.participants)
        match_id_counter = 1

        if bracket_type == "FINAL":
            rounds: list[list[Match]] = []
            round_matches = []
            match = FFAMatch(
                match_id=match_id_counter,
                tournament_id=self._id,
                round_number=1,
                bracket=Bracket.FINAL,
                best_of=best_of,
                status=MatchStatus.WAITING,
                next_match_win_lose_ids=(None, None),
                match_has_ended_callback=self.match_has_ended_callback
            )

            for participant in self.participants:
                match.assign_participant(participant.id, -1)

            round_matches.append(match)
            rounds.append(round_matches)
            self._matches = [match]

        elif bracket_type == "SINGLE":
            # --- single-elimination as before ---
            num_rounds = ceil(log2(num_participants))
            rounds: list[list[Match]] = []

            for round_number in range(1, num_rounds + 1):
                num_matches = 2 ** (num_rounds - round_number)
                round_matches = []
                for _ in range(num_matches):
                    match = Match(
                        match_id=match_id_counter,
                        tournament_id=self._id,
                        round_number=round_number,
                        bracket=Bracket.UPPER if round_number != num_rounds else Bracket.FINAL,
                        best_of=best_of,
                        status=MatchStatus.WAITING,
                        next_match_win_lose_ids=(None, None),
                        match_has_ended_callback=self.match_has_ended_callback
                    )
                    round_matches.append(match)
                    match_id_counter += 1
                rounds.append(round_matches)

            # Link winner IDs
            for i in range(len(rounds) - 1):
                current_round = rounds[i]
                next_round = rounds[i + 1]
                for idx, match in enumerate(current_round):
                    next_match = next_round[idx // 2]
                    match._next_match_win_id = next_match.match_id

            # Assign participants to first round
            participant_iter = iter(self.participants)
            first_round = rounds[0]
            for match in first_round:
                try:
                    p1 = next(participant_iter)
                    match.assign_participant(p1.id, 1)
                except StopIteration:
                    continue
                try:
                    p2 = next(participant_iter)
                    match.assign_participant(p2.id, 2)
                except StopIteration:
                    if not match.is_fully_seeded:  # Auto-Bye
                        for game in match.games:
                            match.report_game_result(game.number, p1.id, (1, 0))
                    continue

                if match.is_fully_seeded:
                    match.status = MatchStatus.PENDING


            # Flatten all rounds
            self._matches = [m for round_matches in rounds for m in round_matches]

        elif bracket_type == "DOUBLE":
            # --- double-elimination bracket generation ---
            # ToDo: Rounds are not correctly persisted into self._rounds here. What data structure to use?
            # ToDo: Bye-Handling not done
            # Implementation Notice: Do not implement yet!
            num_rounds_upper = ceil(log2(num_participants))
            upper_rounds: list[list[Match]] = []
            for round_number in range(1, num_rounds_upper + 1):
                num_matches = 2 ** (num_rounds_upper - round_number)
                round_matches = []
                for _ in range(num_matches):
                    match = Match(
                        match_id=match_id_counter,
                        tournament_id=self._id,
                        round_number=round_number,
                        bracket=Bracket.UPPER,
                        best_of=best_of,
                        status=MatchStatus.WAITING,
                        next_match_win_lose_ids=(None, None),  # will fill later
                        match_has_ended_callback=self.match_has_ended_callback
                    )
                    round_matches.append(match)
                    match_id_counter += 1
                upper_rounds.append(round_matches)

            # Lower bracket (Losers)
            # Double-elim lower bracket has roughly (2*num_rounds_upper - 2) rounds
            num_rounds_lower = 2 * (num_rounds_upper - 1)
            lower_rounds: list[list[Match]] = []
            for round_number in range(1, num_rounds_lower + 1):
                num_matches = 2 ** (num_rounds_lower - round_number - 1) if round_number != 1 else 2 ** (num_rounds_upper - 1)
                round_matches = []
                for _ in range(num_matches):
                    match = Match(
                        match_id=match_id_counter,
                        tournament_id=self._id,
                        round_number=round_number,
                        bracket=Bracket.LOWER,
                        best_of=best_of,
                        status=MatchStatus.WAITING,
                        next_match_win_lose_ids=(None, None),
                        match_has_ended_callback=self.match_has_ended_callback
                    )
                    round_matches.append(match)
                    match_id_counter += 1
                lower_rounds.append(round_matches)

            # Link upper bracket winners to next upper-round matches
            for i in range(len(upper_rounds) - 1):
                for idx, match in enumerate(upper_rounds[i]):
                    next_match = upper_rounds[i + 1][idx // 2]
                    match._next_match_win_id = next_match.match_id

            # Link upper bracket losers to lower bracket first rounds
            lower_round1 = lower_rounds[0] if lower_rounds else []
            for idx, match in enumerate(upper_rounds[0]):
                if idx < len(lower_round1):
                    match._next_match_lose_id = lower_round1[idx].match_id

            # Link lower bracket winners to next lower-round matches
            for i in range(len(lower_rounds) - 1):
                for idx, match in enumerate(lower_rounds[i]):
                    next_match = lower_rounds[i + 1][idx // 2]
                    match._next_match_win_id = next_match.match_id

            # Final match
            final_match = Match(
                match_id=match_id_counter,
                tournament_id=self._id,
                round_number=max(num_rounds_upper, num_rounds_lower) + 1,
                bracket=Bracket.FINAL,
                best_of=best_of,
                status=MatchStatus.WAITING,
                next_match_win_lose_ids=(None, None),
                match_has_ended_callback=self.match_has_ended_callback
            )
            match_id_counter += 1

            # Last upper winner and last lower winner feed into final
            if upper_rounds:
                upper_last = upper_rounds[-1][0]
                upper_last._next_match_win_id = final_match.match_id
            if lower_rounds:
                lower_last = lower_rounds[-1][0]
                lower_last._next_match_win_id = final_match.match_id

            # Flatten all matches
            self._matches = [m for round_matches in upper_rounds + lower_rounds for m in round_matches] + [final_match]

            # Assign participants to first upper round
            participant_iter = iter(self._participants)
            first_upper = upper_rounds[0]
            for match in first_upper:
                try:
                    p1 = next(participant_iter)
                    match.assign_participant(p1.id, 1)
                except StopIteration:
                    continue
                try:
                    p2 = next(participant_iter)
                    match.assign_participant(p2.id, 2)
                except StopIteration:
                    if not match.is_fully_seeded:  # Auto-Bye
                        for game in match.games:
                            match.report_game_result(game.number, p1.id, (1, 0))
                        match.check_completion()
                    continue

                if match.is_fully_seeded:
                    match.status = MatchStatus.PENDING

        else:
            raise TournamentError(f"Unknown bracket type: {bracket_type}")

        self._status = TournamentStatus.ONGOING
        logger.info(f"New tournament status for {self._name}: {self._status}")
        for match in self._matches:
            match.check_completion()


def generate_new_tournament(name: str, description: str, game_title: GameTitle, format_: TournamentFormat, start_time: datetime, max_participants: int, initial_status: TournamentStatus = TournamentStatus.CLOSED) -> Tournament:
    id_ = uuid.uuid4().int
    return Tournament(
        id_,
        name,
        description,
        game_title,
        format_,
        start_time,
        initial_status,
        list(),
        None,
        list(),
        max_participants
    )
