package eu.jonahbauer.wizard.server;

import eu.jonahbauer.wizard.common.messages.data.PlayerData;
import eu.jonahbauer.wizard.common.messages.data.SessionData;
import eu.jonahbauer.wizard.common.messages.observer.ObserverMessage;
import eu.jonahbauer.wizard.common.messages.observer.UserInputMessage;
import eu.jonahbauer.wizard.common.messages.player.ContinueMessage;
import eu.jonahbauer.wizard.common.messages.player.PlayerMessage;
import eu.jonahbauer.wizard.common.messages.server.*;
import eu.jonahbauer.wizard.common.model.Configuration;
import eu.jonahbauer.wizard.core.Game;
import eu.jonahbauer.wizard.core.Observer;
import eu.jonahbauer.wizard.common.util.Pair;
import eu.jonahbauer.wizard.server.machine.Player;
import eu.jonahbauer.wizard.server.management.SessionMBean;
import lombok.AccessLevel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.extern.log4j.Log4j2;
import org.intellij.lang.annotations.Language;

import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ThreadLocalRandom;

@Getter
@Log4j2
@EqualsAndHashCode(of = "uuid")
public class Session implements Observer, SessionMBean {
    @Language("RegExp")
    private static final String PLAYER_NAME_PATTERN = "[a-zA-Z0-9_' ]{1,20}";
    protected static final int MIN_PLAYERS = 3;
    protected static final int MAX_PLAYERS = 6;

    private final UUID uuid;
    private final String name;
    private final long timeout;
    private final Configuration configuration;

    @Getter(AccessLevel.PROTECTED)
    private final Map<UUID, SessionPlayer> players = new LinkedHashMap<>();

    private Game game;
    private final List<Pair<UUID, ServerMessage>> messages = new ArrayList<>();


    public Session(UUID uuid, String name, long timeout, Configuration configuration) {
        this.uuid = uuid;
        this.name = name;
        this.timeout = timeout;
        this.configuration = configuration;
    }

    @Override
    public boolean isRunning() {
        return game != null;
    }

    /**
     * Associates the given player with this session and notifies all other players in the
     * session with a {@link PlayerJoinedMessage}, the joining player with a {@link SessionJoinedMessage} and all
     * players in the lobby with a {@link SessionModifiedMessage}.
     *
     * @param player the player
     * @param name the players chosen name
     * @return the players uuid
     */
    public synchronized UUID join(Player player, String name) {
        if (isRunning()) {
            throw new NackException(NackMessage.GAME_ALREADY_STARTED, "Game has already started.");
        } else if (players.size() == MAX_PLAYERS) {
            throw new NackException(NackMessage.SESSION_FULL, "Session is full.");
        } else if (players.values().stream().anyMatch(p -> p.getName().equalsIgnoreCase(name))) {
            throw new NackException(NackMessage.PLAYER_NAME_TAKEN, "Player name is already taken.");
        } else if (!name.matches(PLAYER_NAME_PATTERN)) {
            throw new NackException(NackMessage.PLAYER_NAME_NOT_ALLOWED, "Player name is too short, too long or contains illegal characters.");
        }

        SessionPlayer sessionPlayer;
        do {
            sessionPlayer = new SessionPlayer(UUID.randomUUID(), name);
        } while (players.putIfAbsent(sessionPlayer.getUuid(), sessionPlayer) != null);
        sessionPlayer.setPlayer(player);

        notifyJoined(sessionPlayer.toData());
        notifyLobby();

        return sessionPlayer.getUuid();
    }

    public synchronized PlayerData rejoin(Player player, UUID uuid, String secret) {
        if (!players.containsKey(uuid)) {
            throw new NackException(NackMessage.ILLEGAL_ARGUMENT, "An error occurred.");
        }

        SessionPlayer sessionPlayer = players.get(uuid);
        if (sessionPlayer.isConnected()) {
            throw new NackException(NackMessage.ILLEGAL_ARGUMENT, "An error occurred.");
        } else if (!sessionPlayer.getSecret().equals(secret)) {
            throw new NackException(NackMessage.ILLEGAL_ARGUMENT, "An error occurred.");
        }

        sessionPlayer.setPlayer(player);
        player.send(new SessionJoinedMessage(
                this.uuid,
                uuid,
                players.values().stream().map(SessionPlayer::toData).toList(),
                secret
        ));
        for (Pair<UUID, ServerMessage> message : messages) {
            if (message.getKey() == null || message.getKey().equals(uuid)) {
                player.send(message.getValue());
            }
        }
        player.send(new AckMessage());

        return sessionPlayer.toData();
    }

    public synchronized void leave(UUID player) {
        if (!isRunning()) {
            if (players.remove(player) != null) {
                if (players.size() == 0) {
                    Lobby.getInstance().removeSession(uuid);
                } else {
                    notifyPlayers(new PlayerLeftMessage(player));
                    notifyLobby();
                }
            }
        } else {
            var sessionPlayer = players.get(player);
            if (sessionPlayer != null) {
                sessionPlayer.setPlayer(null);
                if (players.values().stream().noneMatch(SessionPlayer::isConnected)) {
                    Lobby.getInstance().removeSession(uuid);
                }
            }
        }
    }

    public synchronized void ready(UUID uuid, boolean ready) {
        var player = players.get(uuid);
        if (player == null) {
            throw new NackException(NackMessage.PLAYER_NOT_FOUND, "Who are you?");
        } else if (isRunning()) {
            throw new NackException(NackMessage.GAME_ALREADY_STARTED, "Game has already started.");
        }

        player.send(new AckMessage());
        if (player.isReady() != ready) {
            player.setReady(ready);
            notifyPlayers(new PlayerModifiedMessage(player.toData()));
        }

        if (players.size() >= MIN_PLAYERS && players.values().stream().allMatch(SessionPlayer::isReady)) {
            startGame();
        }
    }

    /**
     * Forwards the message sent by the player to the game.
     *
     * @param uuid the player
     * @param message the players message
     */
    public synchronized void handlePlayerMessage(UUID uuid, PlayerMessage message) {
        var player = players.get(uuid);
        if (player == null) {
            throw new NackException(NackMessage.PLAYER_NOT_FOUND, "Who are you?");
        } else if (!isRunning()) {
            throw new NackException(NackMessage.GAME_NOT_YET_STARTED, "Game hat not yet started.");
        } else {
            try {
                game.onMessage(uuid, message, player::buffer);
                player.send(new AckMessage());
            } catch (IllegalStateException e) {
                throw new NackException(NackMessage.ILLEGAL_STATE, e.getMessage());
            } catch (IllegalArgumentException e) {
                throw new NackException(NackMessage.ILLEGAL_ARGUMENT, e.getMessage());
            }
        }
    }

    protected synchronized void startGame() {
        notifyPlayers(new StartingGameMessage());
        messages.add(Pair.of(null, new StartingGameMessage()));
        game = new Game(configuration, timeout, 10 * 60 * 1000, this);
        notifyLobby();
        game.start(List.copyOf(players.keySet()));
        CompletableFuture.runAsync(() -> {
            while (true) {
                try {
                    game.await();
                    break;
                } catch (InterruptedException e) {
                    // ignored
                } catch (ExecutionException e) {
                    log.warn("Game completed with exception.", e);
                    break;
                }
            }
            finishGame();
        });
    }

    protected synchronized void finishGame() {
        players.forEach((id, player) -> player.setReady(false));
        game = null;
        messages.clear();
        for (SessionPlayer player : List.copyOf(players.values())) {
            if (!player.isConnected()) {
                leave(player.getUuid());
            }
        }
        notifyLobby();
    }

    protected synchronized void notifyJoined(PlayerData joined) {
        var message = new PlayerJoinedMessage(joined);
        for (SessionPlayer player : players.values()) {
            if (player.getUuid().equals(joined.getUuid())) {
                player.send(new SessionJoinedMessage(
                        getUuid(),
                        player.getUuid(),
                        players.values().stream().map(SessionPlayer::toData).toList(),
                        player.getSecret()
                ));
            } else {
                player.send(message);
            }
        }
    }

    protected synchronized void notifyPlayers(ServerMessage message) {
        for (SessionPlayer player : players.values()) {
            player.send(message);
        }
    }

    public SessionData toData() {
        return new SessionData(uuid, name, players.size(), configuration, isRunning());
    }

    @Override
    public synchronized void notify(ObserverMessage message) {
        var gameMessage = new GameMessage(message);
        notifyPlayers(gameMessage);
        messages.add(Pair.of(null, gameMessage));
        autoSync(null, message);
    }

    @Override
    public synchronized void notify(UUID player, ObserverMessage message) {
        var gameMessage = new GameMessage(message);
        players.get(player).send(gameMessage);
        messages.add(Pair.of(player, gameMessage));
        autoSync(player, message);
    }

    /**
     * Automatically sends {@link ContinueMessage}s for disconnected players.
     */
    private void autoSync(UUID player, ObserverMessage observerMessage) {
        if (observerMessage instanceof UserInputMessage userInput) {
            if (userInput.getAction() == UserInputMessage.Action.SYNC) {
                for (var p : (player != null ? List.of(player) : players.keySet())) {
                    var sessionPlayer = players.get(p);
                    if (sessionPlayer != null && !sessionPlayer.isConnected()) {
                        game.onMessage(p, new ContinueMessage());
                    }
                }
            }
        }
    }

    protected void notifyLobby() {
        Lobby.getInstance().notifyPlayers(new SessionModifiedMessage(toData()));
    }

    public void close() {
        for (var sessionPlayer : getPlayers().values()) {
            var player = sessionPlayer.getPlayer();
            if (player != null) {
                player.disconnect("Session was forcibly closed.");
            }
        }
    }

    //<editor-fold desc="JMX" defaulstate="collapsed">
    public boolean isDebug() {
        return false;
    }

    @Override
    public synchronized int getPlayerCount() {
        return players.size();
    }
    //</editor-fold>

    @Data
    @EqualsAndHashCode(of = "uuid")
    protected static class SessionPlayer {
        private final UUID uuid;
        private final String name;
        private final String secret = generateSecret();
        private Player player;
        private boolean ready;

        private static String generateSecret() {
            return ThreadLocalRandom.current()
                    .ints(32, 'a', 'z' + 1)
                    .collect(StringBuilder::new, (sb, i) -> sb.append((char) i), StringBuilder::append)
                    .toString();
        }

        public PlayerData toData() {
            return new PlayerData(uuid, name, ready);
        }

        public void send(ServerMessage message) {
            if (player != null) {
                player.send(message);
            }
        }

        public void buffer() {
            if (player != null) {
                player.buffer();
            }
        }

        public boolean isConnected() {
            return player != null;
        }
    }
}
