import asyncio
import json
from sys import stderr
from typing import List, Dict, Optional

from pydantic.main import BaseModel
from socketio import AsyncServer

from server.game.manager import ClientManager
from server.game.message import MessageToPlayer, MessageFromPlayer
from server.model.game import Game
from server.model.known import CARRE_ACE
from server.model.players import Player, Announce


class Metadata(BaseModel):
    ready: bool = False
    sid: str = ""
    last_announce: Optional[Announce] = None
    fresh_announce: bool = True


class LobbyManager(ClientManager):
    """ A ClientManager that handles a lobby, then orchestrates games.
    """

    def __init__(self, sio: AsyncServer):
        super().__init__()
        self.lobby: Dict[str, Player] = {}
        self.metadata: Dict[str, Metadata] = {}
        self.games: List[Game] = []
        self.sio = sio

    @property
    def nb_ready(self):
        return len(self.players_ready)

    @property
    def players_ready(self):
        return [self.lobby[k] for k, m in self.metadata.items() if m.ready]

    async def new_player(self, player: Player, sid: str) -> None:
        """ Registers a new player. """
        self.lobby[player.name] = player
        self.players.append(player)
        self.metadata[player.name] = Metadata(sid=sid)
        print(f"Added {player} to a lobby with {len(self.lobby)} players.")
        await self.send(to=player, message=MessageToPlayer.Welcome, extras={"name": player.name})
        await self.sendall(MessageToPlayer.NewPlayer, but=player)

    async def player_left(self, sid: str) -> None:
        """ Reports the disconnection of a player, both internally and to other players."""

        for name, meta in self.metadata.items():
            if meta.sid == sid:
                print(f"Player {name} disconnected.")
                player = self.lobby.pop(name)
                self.players.remove(player)

                self.metadata.pop(name)
                await self.sendall(MessageToPlayer.Disconnected, extras={"justLeft": name})

                return
        print(f"No player for sid {sid}...", file=stderr)

    def wants_to_play(self, player: Player):
        """
        Notes that a player wants to play, starting a game if others too.
        """
        self.metadata[player.name].ready = True
        print(f"Lobby | {player} wants to play!")

        self.sendall(MessageToPlayer.PlayerReady, extras={"wantsToPlay": player.name}, but=player)

        ready = self.players_ready
        return ready

    async def maybe_start_game(self):
        print("Should we start a game?")
        ready = self.players_ready
        if len(ready) > 1:
            await self.start_game(ready)
            print(f"Lobby: Game started. with {ready}.")

    def just_announced(self, player: Player, announce: Announce):
        meta = self.metadata[player.name]
        meta.last_announce = announce
        meta.fresh_announce = True

    async def start_game(self, players: Optional[List[Player]] = None, max_players=2):
        if not players:
            players = self.players_ready[:max_players]

        game = Game(players, manager=self)
        self.games.append(game)

        print(f"Game started : {' vs '.join([p.name for p in players])}")
        winner = await game.new_game()
        print(f"Game ended : winner is {winner.name}! {len(players)} players back waiting in lobby.")

        for p in players:
            self.metadata[p.name].ready = False

    async def reset(self) -> str:
        players = len(self.players)
        games = len(self.games)
        msg = f"Resetting! sorry for the {players} players / {games} games..."
        print(msg)

        for p in self.players:
            m = self.metadata[p.name]
            await self.send(p, MessageToPlayer.Reset)
            await self.sio.disconnect(m.sid)
            self.lobby.clear()
            self.players.clear()
            self.metadata.clear()
            self.games.clear()
        return msg

    async def sendall(self, message: MessageToPlayer, extras=None,
                      to: List[Player] = None,
                      but: Optional[Player] = None):
        if to is None:
            to = self.players

        for p in to:
            if but and p.name == but.name:
                continue
            self.send(p, message, extras)

    async def send(self, to: Player, message: MessageToPlayer, extras=None) -> None:
        sid: str = "?"

        try:
            sid = self.metadata[to.name].sid
        except KeyError as e:
            print(f"Error: Cannot send message to player {to.name}", file=stderr)
            return  # TODO: Warn other players and kill game

        game: Game = self.game_with(to)

        data = to.hand.json()  # Start with "cards": []
        data = json.loads(data)  # JSONEncode and decode, starting with dict would miss `json()` encoders
        data["state"] = str(message.name)
        data["players"] = [
            {
                "name": p.name,
                "nbCards": len(p.hand),
                "isReady": self.metadata[p.name].ready,
                "isPlaying": bool(game and p.name in [p.name for p in game.players]),
            }
            for p in self.players]

        if extras:
            data["extras"] = extras

        print(f"MSGOUT|{sid} ({to.name}), {data}...", end='')
        await self.sio.emit('messageChannel', json.dumps(data), room=sid)
        print(f"sent!")

    async def handle_message(self, sid, data):
        message = None
        extras = {}
        sender = self.which_player(sid)
        if not sender:
            print(f"ERROR: Unrecognized sender {sid}")
            return

        print(f"MSGIN| {sid} ({sender.name}): {data}.")

        for option in MessageFromPlayer:
            if option.value in data:

                if option == MessageFromPlayer.Waiting:
                    self.metadata[sender.name].ready = False

                    print(f"Lobby| Player {sender.name} waiting")

                    message = MessageToPlayer.ReadyToStart \
                        if len(self.players_ready) > 1 else MessageToPlayer.Waiting

                elif option == MessageFromPlayer.Ready:
                    print(f"Lobby|Player {sender.name} ready")

                    gamers = self.wants_to_play(player=sender)
                    if len(gamers) > 1:
                        print(f"Game ready! gamers={gamers}")
                        message = MessageToPlayer.NewGame
                        extras["playersPlaying"] = [p.name for p in gamers]
                    else:
                        print(f"Still alone ready!")
                        message = MessageToPlayer.ReadyToStart
                        extras["text"] = "You're alone, but hold on"

                elif option == MessageFromPlayer.Bet:
                    # FIXME vraie annonce, pas juste carre d'as lol
                    announce = Announce(bet=CARRE_ACE)
                    self.just_announced(player=sender,
                                        announce=announce)

                    print(f"Lobby| Player {sender.name} just announced {announce}.")

                    extras["youBet"] = announce.bet

                elif option == MessageFromPlayer.Menteur:
                    self.just_announced(player=sender, announce=Announce())
                    extras["youBet"] = False

        if message:
            if message == MessageToPlayer.NewGame:
                print("Creating game task...")
                game = asyncio.create_task(self.maybe_start_game())

                print("Awaiting game...")
                await game

            print("Sending message...")
            await self.send(sender, message, extras)
            print("Message sent.")

        print("LOBBY: Message handled.")

    def which_player(self, sid) -> Player:
        sender: Optional[Player] = None
        for name, metadata in self.metadata.items():
            if metadata.sid == sid:
                sender = self.lobby[name]
        return sender

    def game_with(self, player: Player) -> Optional[Game]:
        matching = [g for g in self.games if player in g.players]
        return matching[0] if matching else None

    async def send_waiting_for(self, player: Player):
        game = self.game_with(player)
        # FIXME: Seems to block here, game doesn't move past Asking for...

        bet = game.current_bet
        extras = {"bet": bet.json() if bet else None}
        print(f"Announcing player {player.name} that we await their reply to {bet}...")
        await self.send(player, MessageToPlayer.YourTurn, extras)

        extras["waitingFor"] = player.name
        await self.sendall(MessageToPlayer.Waiting, extras, but=player)
