package eu.jonahbauer.wizard.server;

import eu.jonahbauer.wizard.common.messages.server.*;
import eu.jonahbauer.wizard.common.model.Configuration;
import eu.jonahbauer.wizard.server.debug.DebugSession;
import eu.jonahbauer.wizard.server.machine.Player;
import eu.jonahbauer.wizard.server.management.LobbyMBean;
import eu.jonahbauer.wizard.server.management.SessionMBean;
import lombok.extern.log4j.Log4j2;
import org.intellij.lang.annotations.Language;
import org.jetbrains.annotations.NotNull;

import javax.management.MBeanServer;
import javax.management.ObjectName;
import javax.management.StandardMBean;
import java.lang.management.ManagementFactory;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantReadWriteLock;

@Log4j2
public class Lobby implements LobbyMBean {
    @Language("RegExp")
    private static final String SESSION_NAME_PATTERN = "[a-zA-Z0-9_' ]{1,20}";
    private static final Lobby INSTANCE = new Lobby();
    public static Lobby getInstance() {
        return INSTANCE;
    }

    private final MBeanServer mBeanServer;

    private final Map<UUID, Session> sessions = new ConcurrentHashMap<>();
    private final List<Player> players = new ArrayList<>();
    // read lock is required whenever players are read or sessions are modified, write lock is required when players are modified
    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    private Lobby() {
        mBeanServer = ManagementFactory.getPlatformMBeanServer();
        registerLobbyMBean();
    }

    public Session createSession(@NotNull String name, long timeout, @NotNull Configuration configuration) {
        if (!name.matches(SESSION_NAME_PATTERN)) {
            throw new NackException(NackMessage.SESSION_NAME_NOT_ALLOWED, "Session name is too short, too long or contains illegal characters.");
        }

        lock.readLock().lock();
        try {
            if (sessions.values().stream().anyMatch(s -> s.getName().equalsIgnoreCase(name))) {
                throw new NackException(NackMessage.SESSION_NAME_TAKEN, "Session name is already taken.");
            }

            Session session;
            do {
                session = new Session(UUID.randomUUID(), name, timeout, configuration);
            } while (sessions.putIfAbsent(session.getUuid(), session) != null);

            log.info("Created session {}.", session.getUuid());
            registerSessionMBean(session);

            notifyPlayers(new SessionCreatedMessage(session.toData()));

            return session;
        } finally {
            lock.readLock().unlock();
        }
    }

    public DebugSession createDebugSession(@NotNull String name, long timeout, @NotNull Configuration configuration) {
        lock.readLock().lock();
        try {
            DebugSession session;
            do {
                session = new DebugSession(UUID.randomUUID(), name, timeout, configuration);
            } while (sessions.putIfAbsent(session.getUuid(), session) != null);

            log.info("Created debug session {}.", session.getUuid());
            registerSessionMBean(session);

            notifyPlayers(new SessionCreatedMessage(session.toData()));

            return session;
        } finally {
            lock.readLock().unlock();
        }
    }

    public Session getSession(UUID uuid) {
        return sessions.get(uuid);
    }

    public void removeSession(UUID uuid) {
        lock.readLock().lock();
        try {
            if (sessions.remove(uuid) != null) {
                log.info("Removed session {}.", uuid);
                unregisterSessionMBean(uuid);
                notifyPlayers(new SessionRemovedMessage(uuid));
            }
        } finally {
            lock.readLock().unlock();
        }
    }

    /**
     * Adds a player to the lobby.
     */
    public void join(Player player) {
        lock.writeLock().lock();
        try {
            players.add(player);
            player.send(new SessionListMessage(sessions.values().stream().map(Session::toData).toList()));
        } finally {
            lock.writeLock().unlock();
        }
    }

    /**
     * Removes a player from the lobby. It is ensured that no further lobby related messages
     * ({@link SessionCreatedMessage}, {@link SessionModifiedMessage}, {@link SessionRemovedMessage}) are set to this
     * player after the call to this method returns.
     */
    public void leave(Player player) {
        lock.writeLock().lock();
        try {
            players.remove(player);
        } finally {
            lock.writeLock().unlock();
        }
    }

    public void notifyPlayers(ServerMessage message) {
        lock.readLock().lock();
        try {
            for (Player player : players) {
                player.send(message);
            }
        } finally {
            lock.readLock().unlock();
        }
    }

    //<editor-fold desc="JMX" defaultstate="collapsed">
    private void registerLobbyMBean() {
        try {
            var name = new ObjectName("eu.jonahbauer.wizard.server:type=Lobby");
            mBeanServer.registerMBean(new StandardMBean(this, LobbyMBean.class), name);
        } catch (Exception e) {
            log.warn("Could not register LobbyMBean.", e);
        }
    }

    private void registerSessionMBean(@NotNull Session session) {
        try {
            var name = new ObjectName("eu.jonahbauer.wizard.server:type=Session,name=" + session.getUuid());
            mBeanServer.registerMBean(new StandardMBean(session, SessionMBean.class), name);
        } catch (Exception e) {
            log.warn("Could not register SessionMBean for session {}.", session.getUuid(), e);
        }
    }

    private void unregisterSessionMBean(@NotNull UUID uuid) {
        try {
            var name = new ObjectName("eu.jonahbauer.wizard.server:type=Session,name=" + uuid);
            mBeanServer.unregisterMBean(name);
        } catch (Exception e) {
            log.warn("Could not unregister SessionMBean for session {}.", uuid, e);
        }
    }

    @Override
    public int getPlayerCount() {
        lock.readLock().lock();
        try {
            return players.size();
        } finally {
            lock.readLock().unlock();
        }
    }

    @Override
    public int getTotalPlayerCount() {
        return sessions.values().stream().mapToInt(Session::getPlayerCount).sum() + getPlayerCount();
    }

    @Override
    public int getSessionCount() {
        return sessions.size();
    }
    //</editor-fold>
}
