"""
This module defines the `League` class, which represents a league of players in a game.
"""

import os
import random
import shutil
from collections import deque
from dataclasses import dataclass, field
from typing import cast, no_type_check

import plotly.graph_objects as go
from sortedcollections import ValueSortedDict

import chipiron as ch
from chipiron import players
from chipiron.games.game.game_args import GameArgs
from chipiron.games.match.match_args import MatchArgs
from chipiron.games.match.match_factories import create_match_manager
from chipiron.games.match.match_results import MatchReport, MatchResults
from chipiron.games.match.match_settings_args import MatchSettingsArgs
from chipiron.games.match.match_tag import MatchConfigTag
from chipiron.players.player_ids import PlayerConfigTag
from chipiron.scripts.chipiron_args import ImplementationArgs
from chipiron.utils.small_tools import mkdir_if_not_existing, path


@no_type_check
@dataclass(slots=True)
class League:
    """
    Represents a league of players in a game.

    Attributes:
        folder_league (str): The folder path where the league is located.
        seed (int): The seed value for random number generation.
        players_elo (ValueSortedDict): A dictionary that stores the Elo ratings of the players.
        players_args (dict[str, players.PlayerArgs]): A dictionary that stores the arguments of the players.
        players_number_of_games_played (dict[str, int]): A dictionary that stores the number of games played by each player.
        id_for_next_player (int): The ID for the next player to join the league.
        K (int): The K-factor used in the Elo rating system.
        ELO_HISTORY_LENGTH (int): The length of Elo rating history to keep for each player.
        games_already_played (int): The number of games already played in the league.
    """

    folder_league: str
    seed: int
    # todo replace ValueSortedDict ed because it screws type anotation
    players_elo: ValueSortedDict = field(default_factory=ValueSortedDict)
    players_args: dict[str, players.PlayerArgs] = field(
        default_factory=lambda: dict[str, players.PlayerArgs]()
    )
    players_number_of_games_played: dict[str, int] = field(
        default_factory=lambda: dict[str, int]()
    )
    id_for_next_player: int = 0
    K: int = 10
    ELO_HISTORY_LENGTH: int = 500
    games_already_played: int = 0

    def __post_init__(self) -> None:
        """
        Initializes the league object.

        This method is called after the object is created and initializes the necessary attributes.
        It also creates the required folders for logging and storing game data.
        """
        print(f"init league from folder: {self.folder_league}")
        self.check_for_players()
        path_logs_folder: path = os.path.join(self.folder_league, "logs")
        mkdir_if_not_existing(path_logs_folder)
        path_logs_games_folder: path = os.path.join(path_logs_folder, "games")
        mkdir_if_not_existing(path_logs_games_folder)

    def check_for_players(self) -> None:
        """
        Checks for new players joining the league.

        This method checks if there are any new player files in the 'new_players' folder and adds them to the league.
        """
        path: str = os.path.join(self.folder_league, "new_players/")
        if os.path.exists(path):
            files = os.listdir(path)
            if len(files) > 0:
                for file in files:
                    path_file = os.path.join(self.folder_league, "new_players/", file)
                    self.new_player_joins(path_file)

    def new_player_joins(self, file_player: str) -> None:
        """
        Adds a new player to the league.

        Args:
            file_player (str | os.PathLike[str]): The path to the player file.

        This method adds a new player to the league by reading the player file, assigning a unique ID to the player,
        and updating the necessary attributes.
        """
        print("adding player:", file_player)

        player_tag = PlayerConfigTag(
            file_player
        )  # probably wrong / to be fixed when reusing the leagu not a big issue
        args_player: players.PlayerArgs = player_tag.get_players_args()

        print(args_player)

        args_player.name = f"{args_player.name}_{self.id_for_next_player}"
        self.id_for_next_player += 1

        self.players_elo[args_player.name] = deque(
            [1200], maxlen=self.ELO_HISTORY_LENGTH
        )
        self.players_args[args_player.name] = args_player
        self.players_number_of_games_played[args_player.name] = 0

        current_player_folder: path = os.path.join(
            self.folder_league, "current_players"
        )
        mkdir_if_not_existing(current_player_folder)
        shutil.move(file_player, current_player_folder)

        print("elo", self.players_elo)
        print("args", self.players_args)

    @no_type_check
    def run(self) -> None:
        """
        Runs a game in the league.

        This method selects two players from the league, plays a game between them, updates the Elo ratings,
        and saves the results.
        """
        self.print_info()
        self.update_elo_graph()

        args_player_one, args_player_two = self.select_two_players()

        #  play
        file_match_setting: str = "setting_jime.yaml"

        # Recovering args from yaml file for match and game and merging with extra args and converting
        # to standardized dataclass
        match_args = MatchArgs(
            player_one=PlayerConfigTag.CHIPIRON,
            player_two=PlayerConfigTag.CHIPIRON,
            match_setting=MatchConfigTag(
                file_match_setting
            ).get_match_settings_args(),  # probibly to fix as weell and this is a ditry fix to create a martch args please imrpove!!
        )

        path_logs_game_folder: path = os.path.join(
            self.folder_league, f"logs/games/game{self.games_already_played}"
        )
        mkdir_if_not_existing(path_logs_game_folder)
        path_logs_game_folder_temp: path = os.path.join(
            self.folder_league, f"logs/games/game{self.games_already_played}/games"
        )
        mkdir_if_not_existing(path_logs_game_folder_temp)

        assert isinstance(match_args.match_setting, MatchSettingsArgs)
        assert isinstance(match_args.match_setting.game_args, GameArgs)

        match_seed = self.seed + self.games_already_played
        match_manager: ch.game.MatchManager = create_match_manager(
            args_match=match_args.match_setting,
            args_player_one=args_player_one,
            args_player_two=args_player_two,
            args_game=match_args.match_setting.game_args,
            seed=match_seed,
            output_folder_path=path_logs_game_folder,
            implementation_args=ImplementationArgs(),
        )

        # Play the match
        match_report: MatchReport = match_manager.play_one_match()

        # Logs the results
        path_logs_file: path = os.path.join(self.folder_league, "logs/log_results.txt")
        with open(path_logs_file, "a", encoding="utf-8") as log_file:
            log_file.write(
                f"Game #{self.games_already_played} || "
                f"{args_player_one.name} vs {args_player_two.name}: {match_report.match_results.get_player_one_wins()}-"
                f"{match_report.match_results.get_player_two_wins()}-{match_report.match_results.get_draws()}"
                f" with seed {match_seed}\n"
            )
        self.players_number_of_games_played[args_player_one.name] += 1
        self.players_number_of_games_played[args_player_two.name] += 1

        # update the ELO
        self.update_elo(match_report.match_results, path_logs_file)

        self.games_already_played += 1

    @no_type_check
    def update_elo(self, match_results: MatchResults, path_logs_file: path) -> None:
        """
        Updates the Elo ratings of the players based on the match results.

        Args:
            match_results (MatchResults): The results of the match.
            path_logs_file (path): The path to the log file.

        This method calculates the Elo rating changes for the players based on the match results and updates their ratings.
        It also logs the changes in the log file.
        """
        # coded for one single game!!
        player_one_name_id = match_results.player_one_name_id
        elo_player_one: float = cast("float", self.players_elo[player_one_name_id][0])
        power_player_one = 10 ** (elo_player_one / 400)
        player_two_name_id = match_results.player_two_name_id
        elo_player_two: float = cast("float", self.players_elo[player_two_name_id][0])
        power_player_two = 10 ** (elo_player_two / 400)
        Eone = power_player_one / (power_player_one + power_player_two)
        Etwo = power_player_two / (power_player_one + power_player_two)

        Perf_one = match_results.get_player_one_wins() + match_results.get_draws() / 2.0
        Perf_two = match_results.get_player_two_wins() + match_results.get_draws() / 2.0

        print(elo_player_one, cast("float", self.players_elo[player_one_name_id][0]))
        old_elo_player_one = elo_player_one
        old_elo_player_two = elo_player_two
        increment_one = self.K * (Perf_one - Eone)
        increment_two = self.K * (Perf_two - Etwo)
        new_elo_one = old_elo_player_one + increment_one
        new_elo_two = old_elo_player_two + increment_two
        self.players_elo[player_one_name_id].appendleft(new_elo_one)
        self.players_elo[player_two_name_id].appendleft(new_elo_two)

        for player in self.players_elo:
            player = cast("str", player)
            if player != player_one_name_id and player != player_two_name_id:
                player_elo_deque = cast("deque[float]", self.players_elo[player])
                player_elo_deque.appendleft(player_elo_deque[0])

        with open(path_logs_file, "a", encoding="utf-8") as log_file:
            log_file.write(
                f"{player_one_name_id} increments its elo by {increment_one}: {old_elo_player_one} -> {new_elo_one}\n"
            )
            log_file.write(
                f"{player_two_name_id} increments its elo by {increment_two}: {old_elo_player_two} -> {new_elo_two}\n"
            )

        self.update_elo_graph()

    @no_type_check
    def update_elo_graph(self) -> None:
        """
        Updates the Elo rating graph.

        This method updates the Elo rating graph by plotting the Elo ratings of all players in the league.
        """
        fig = go.Figure()

        # Type: ignore because ValueSortedDict doesn't support generics
        for player_name_obj, elo_obj in self.players_elo.items():
            # Convert deque to list for plotting
            player_name: str = player_name_obj
            elo_deque: deque[float] = elo_obj
            elo_list: list[float] = list(elo_deque)
            elo_list.reverse()  # Reverse to show chronological order

            fig.add_trace(
                go.Scatter(
                    y=elo_list,
                    mode="lines+markers",
                    name=player_name,
                    line=dict(width=2),
                    marker=dict(size=4),
                )
            )

        fig.update_layout(
            title="ELO Rating Evolution",
            xaxis_title="Game Number",
            yaxis_title="ELO Rating",
            legend=dict(yanchor="top", y=0.99, xanchor="left", x=0.01),
            hovermode="x unified",
        )

        # Save as HTML (interactive) and SVG (static)
        fig.write_html(self.folder_league + "/elo.plot.html")
        fig.write_image(self.folder_league + "/elo.plot.svg", format="svg")

    def select_two_players(self) -> tuple[players.PlayerArgs, players.PlayerArgs]:
        """
        Selects two players from the league.

        Returns:
            tuple[players.PlayerArgs, players.PlayerArgs]: A tuple containing the arguments of the two selected players.

        This method randomly selects two players from the league to participate in a game.
        """
        if len(self.players_args) < 2:
            raise ValueError(
                'Not enough players in the league. To add players put the yaml files in the folder "new players"'
            )
        picked = random.sample(list(self.players_args.values()), k=2)
        print("picked", picked)
        return picked[0], picked[1]

    def save(self) -> None:
        """
        Saves the league.

        This method saves the league by serializing its state to a file.
        """
        pass

    def print_info(self) -> None:
        """
        Prints information about the league.

        This method prints information about the league, such as the players and their Elo ratings.
        """
        print("print info league")
        print("players", self.players_elo)
