# lobby.py : gestion des lobbies (creation, classe, ready, demarrage)
# les methodes retournent (conn_id, message), c'est main.py qui envoie

import logging
import random
from dataclasses import dataclass, field

logger = logging.getLogger(__name__)

# pas de 0/O/I/1 pour eviter les confusions
_LOBBY_CODE_CHARS = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
_LOBBY_CODE_LENGTH = 6
MAX_PLAYERS = 3
VALID_CLASSES = {"kael", "seris", "aldric"}


@dataclass
class LobbyPlayer:
    conn_id: str
    username: str
    player_class: str | None = None
    is_ready: bool = False
    is_host: bool = False

    def to_dict(self) -> dict:
        return {
            "id": self.conn_id,
            "username": self.username,
            "class": self.player_class,
            "ready": self.is_ready,
            "host": self.is_host,
        }


@dataclass
class Lobby:
    code: str
    players: list[LobbyPlayer] = field(default_factory=list)
    started: bool = False

    def get_conn_ids(self) -> list[str]:
        return [p.conn_id for p in self.players]

    def get_player(self, conn_id):
        return next((p for p in self.players if p.conn_id == conn_id), None)

    def is_class_taken(self, class_name):
        return any(p.player_class == class_name for p in self.players)

    def all_ready(self):
        return (
            len(self.players) == MAX_PLAYERS
            and all(p.is_ready and p.player_class for p in self.players)
        )


class LobbyManager:
    def __init__(self):
        self.lobbies: dict[str, Lobby] = {}
        self.conn_to_lobby: dict[str, str] = {}

    def _generate_code(self):
        while True:
            code = "".join(random.choices(_LOBBY_CODE_CHARS, k=_LOBBY_CODE_LENGTH))
            if code not in self.lobbies:
                return code

    def _error(self, conn_id, code, message):
        return [(conn_id, {"type": "error", "code": code, "message": message})]

    def create_lobby(self, conn_id, username):
        # si on est deja dans un lobby on le libere d'abord, evite les blocages au retour menu
        leave_msgs = []
        if conn_id in self.conn_to_lobby:
            leave_msgs = self.disconnect(conn_id)

        code = self._generate_code()
        player = LobbyPlayer(conn_id=conn_id, username=username, is_host=True)
        lobby = Lobby(code=code, players=[player])

        self.lobbies[code] = lobby
        self.conn_to_lobby[conn_id] = code

        logger.info("Lobby %s créé par %s", code, username)
        return leave_msgs + [(conn_id, {"type": "lobby_created", "code": code})]

    def join_lobby(self, conn_id, username, code):
        code = code.upper()

        leave_msgs = []
        if conn_id in self.conn_to_lobby:
            leave_msgs = self.disconnect(conn_id)

        lobby = self.lobbies.get(code)
        if not lobby:
            return self._error(conn_id, "lobby_not_found", f"Lobby {code} introuvable.")
        if lobby.started:
            return self._error(conn_id, "game_in_progress", "La partie a déjà commencé.")
        if len(lobby.players) >= MAX_PLAYERS:
            return self._error(conn_id, "lobby_full", "Le lobby est plein (3/3).")

        player = LobbyPlayer(conn_id=conn_id, username=username)
        lobby.players.append(player)
        self.conn_to_lobby[conn_id] = code

        messages = [
            (conn_id, {"type": "lobby_joined", "code": code, "players": [p.to_dict() for p in lobby.players]}),
        ]
        # notifier les autres
        for p in lobby.players:
            if p.conn_id != conn_id:
                messages.append((p.conn_id, {"type": "player_joined", "player": player.to_dict()}))

        logger.info("%s a rejoint le lobby %s (%d/3)", username, code, len(lobby.players))
        return leave_msgs + messages

    def select_class(self, conn_id, class_name):
        code = self.conn_to_lobby.get(conn_id)
        if not code:
            return self._error(conn_id, "not_in_lobby", "Tu n'es pas dans un lobby.")

        lobby = self.lobbies[code]
        player = lobby.get_player(conn_id)
        if not player:
            return self._error(conn_id, "not_in_lobby", "Joueur introuvable dans le lobby.")
        if class_name not in VALID_CLASSES:
            return self._error(conn_id, "invalid_class", f"Classe invalide : {class_name}.")
        # un joueur peut reselectionner sa propre classe sans erreur
        if lobby.is_class_taken(class_name) and player.player_class != class_name:
            return self._error(conn_id, "class_taken", f"La classe {class_name} est déjà prise.")

        player.player_class = class_name
        player.is_ready = False  # reset du ready si on change de classe

        logger.info("%s a choisi la classe %s", player.username, class_name)
        return [(p.conn_id, {"type": "class_selected", "player_id": conn_id, "class": class_name}) for p in lobby.players]

    def set_ready(self, conn_id):
        code = self.conn_to_lobby.get(conn_id)
        if not code:
            return self._error(conn_id, "not_in_lobby", "Tu n'es pas dans un lobby.")

        lobby = self.lobbies[code]
        player = lobby.get_player(conn_id)
        if not player:
            return self._error(conn_id, "not_in_lobby", "Joueur introuvable.")
        if not player.player_class:
            return self._error(conn_id, "no_class_selected", "Choisis une classe avant de te mettre prêt.")

        player.is_ready = True
        logger.info("%s est prêt", player.username)
        return [(p.conn_id, {"type": "player_ready", "player_id": conn_id}) for p in lobby.players]

    def start_game(self, conn_id):
        code = self.conn_to_lobby.get(conn_id)
        if not code:
            return self._error(conn_id, "not_in_lobby", "Tu n'es pas dans un lobby.")

        lobby = self.lobbies[code]
        player = lobby.get_player(conn_id)
        if not player or not player.is_host:
            return self._error(conn_id, "not_host", "Seul le chef de partie peut démarrer.")
        if not lobby.all_ready():
            return self._error(
                conn_id, "not_all_ready",
                f"Tous les joueurs doivent être prêts avec une classe ({len(lobby.players)}/3).",
            )

        lobby.started = True
        logger.info("Partie démarrée dans le lobby %s", code)
        return [(p.conn_id, {"type": "game_starting", "countdown": 3}) for p in lobby.players]

    def leave_lobby(self, conn_id):
        # bouton Back : on quitte volontairement, meme logique que disconnect + confirmation
        if conn_id not in self.conn_to_lobby:
            return []
        notifications = self.disconnect(conn_id)
        notifications.insert(0, (conn_id, {"type": "lobby_left"}))
        return notifications

    def disconnect(self, conn_id):
        code = self.conn_to_lobby.pop(conn_id, None)
        if not code:
            return []

        lobby = self.lobbies.get(code)
        if not lobby:
            return []

        player = lobby.get_player(conn_id)
        if not player:
            return []

        lobby.players = [p for p in lobby.players if p.conn_id != conn_id]
        logger.info("%s a quitté le lobby %s", player.username, code)

        if not lobby.players:
            del self.lobbies[code]
            logger.info("Lobby %s supprimé (vide)", code)
            return []

        # transferer l'host au premier restant
        if player.is_host:
            lobby.players[0].is_host = True

        return [(p.conn_id, {"type": "player_left", "player_id": conn_id}) for p in lobby.players]
