from collections import Counter

from math import ceil
from typing import Literal, Optional, Callable

from src.ezgg_lan_manager.types.Game import Game
from src.ezgg_lan_manager.types.TournamentBase import MatchStatus, TournamentError, Bracket


class MatchParticipant:
    def __init__(self, participant_id: int, slot_number: Literal[-1, 1, 2]) -> None:
        self._participant_id = participant_id
        if slot_number not in (-1, 1, 2):
            raise TournamentError("Invalid slot number")
        self.slot_number = slot_number

    @property
    def participant_id(self) -> int:
        return self._participant_id


class Match:
    def __init__(self,
                 match_id: int,
                 tournament_id: int,
                 round_number: int,
                 bracket: Bracket,
                 best_of: int,
                 status: MatchStatus,
                 next_match_win_lose_ids: tuple[Optional[int], Optional[int]],
                 match_has_ended_callback: Callable) -> None:
        self._match_id = match_id
        self._tournament_id = tournament_id
        self._round_number = round_number
        self._bracket = bracket
        self._best_of = best_of
        self._status = status
        self._next_match_win_id = next_match_win_lose_ids[0]
        self._next_match_lose_id = next_match_win_lose_ids[1]
        self._match_has_ended_callback = match_has_ended_callback

        self._participants: list[MatchParticipant] = []
        self._games: tuple[Game] = self._prepare_games()

    def _prepare_games(self) -> tuple[Game]:
        games = []
        for game_number in range(1, self._best_of + 1):
            game_id = (self._match_id, game_number)
            games.append(Game(game_id, self._match_id, game_number, None, None, False))
        return tuple(games)

    @property
    def round_number(self) -> int:
        return self._round_number

    @property
    def best_of(self) -> int:
        return self._best_of

    @property
    def status(self) -> MatchStatus:
        if self._status == MatchStatus.COMPLETED:
            return self._status
        return self._status if self.is_fully_seeded else MatchStatus.WAITING

    @status.setter
    def status(self, new_status: MatchStatus) -> None:
        if new_status in (MatchStatus.COMPLETED, MatchStatus.PENDING) and not self.is_fully_seeded:
            raise TournamentError("Can't complete/pend match that is not fully seeded")
        if self._status == MatchStatus.COMPLETED and new_status != MatchStatus.CANCELED:
            raise TournamentError("Can't change COMPLETED match back to another active status")
        self._status = new_status

    @property
    def games(self) -> tuple[Game]:
        return self._games

    @property
    def winner(self) -> Optional[int]:
        wins_needed = ceil(self._best_of / 2)
        counts = Counter(game.winner for game in self._games if game.is_done)

        for participant_id, wins in counts.items():
            if wins >= wins_needed:
                return participant_id

        return None

    @property
    def is_fully_seeded(self) -> bool:
        slots = {p.slot_number for p in self._participants}
        return slots == {1, 2}

    @property
    def match_id(self) -> int:
        return self._match_id

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

    @property
    def next_match_win_id(self) -> Optional[int]:
        return self._next_match_win_id

    @property
    def next_match_lose_id(self) -> Optional[int]:
        return self._next_match_lose_id

    def assign_participant(self, participant_id: int, slot: Literal[-1, 1, 2]) -> None:
        if slot == -1:
            raise TournamentError("Normal match does not support slot -1")
        new_participant = MatchParticipant(participant_id, slot)
        if len(self._participants) < 2 and not any(p.participant_id == participant_id for p in self._participants):
            if len(self._participants) == 1 and self._participants[-1].slot_number == new_participant.slot_number:
                raise TournamentError(f"Match with ID {self._match_id} encountered slot collision")
            self._participants.append(new_participant)
            return
        raise TournamentError(f"Match with ID {self._match_id} already has the maximum number of participants")

    def check_completion(self) -> None:
        winner = self.winner
        if winner is not None:
            self._match_has_ended_callback(self)
            self._status = MatchStatus.COMPLETED

    def report_game_result(self, game_number: int, winner_id: int, score: tuple[int, int]) -> None:
        if winner_id not in {p.participant_id for p in self._participants}:
            raise TournamentError("Winner is not a participant of this match")

        self._games[game_number - 1].finish(winner_id, score)

        self.check_completion()

    def cancel(self) -> None:
        self._status = MatchStatus.CANCELED

    def __repr__(self) -> str:
        participants = ", ".join(
            f"{p.participant_id} (slot {p.slot_number})" for p in self._participants
        )
        return (f"<Match id={self._match_id} round={self._round_number} bracket={self._bracket.name} "
                f"status={self.status.name} participants=[{participants}] winner={self.winner}>")

class FFAMatch(Match):
    """
    Specialized match that supports infinite participants
    """
    def __init__(self, match_id: int, tournament_id: int, round_number: int, bracket: Bracket, best_of: int, status: MatchStatus,
                 next_match_win_lose_ids: tuple[Optional[int], Optional[int]], match_has_ended_callback: Callable) -> None:
        super().__init__(match_id, tournament_id, round_number, bracket, best_of, status, next_match_win_lose_ids, match_has_ended_callback)

    @property
    def is_fully_seeded(self) -> bool:
        return len(self._participants) > 1

    def assign_participant(self, participant_id: int, slot: Literal[-1, 1, 2]) -> None:
        if slot != -1:
            raise TournamentError("FFAMatch does not support slot 1 and 2")
        new_participant = MatchParticipant(participant_id, slot)
        self._participants.append(new_participant)

    def __repr__(self) -> str:
        participants = ", ".join(
            f"{p.participant_id}" for p in self._participants
        )
        return (f"<Match id={self._match_id} round={self._round_number} bracket={self._bracket.name} "
                f"status={self.status.name} participants=[{participants}] winner={self.winner}>")
