/*
 *   This file is a part of the SteamWar software.
 *
 *   Copyright (C) 2025 SteamWar.de-Serverteam
 *
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU Affero General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 *   GNU Affero General Public License for more details.
 *
 *   You should have received a copy of the GNU Affero General Public License
 *   along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

package de.steamwar.velocitycore;

import com.velocitypowered.api.proxy.Player;
import com.viaversion.viaversion.api.Via;
import com.viaversion.viaversion.velocity.platform.VelocityViaConfig;
import de.steamwar.messages.Chatter;
import de.steamwar.persistent.Arenaserver;
import de.steamwar.persistent.Bauserver;
import de.steamwar.persistent.Builderserver;
import de.steamwar.persistent.Subserver;
import de.steamwar.sql.*;

import java.io.DataOutputStream;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;

public class ServerStarter {

    private static final Portrange BAU_PORTS = VelocityCore.MAIN_SERVER ? new Portrange(10100, 20000) : new Portrange(2100, 2200);
    private static final Portrange ARENA_PORTS = VelocityCore.MAIN_SERVER ? new Portrange(3000, 3100) : (VelocityCore.get().getConfig().isEventmode() ? new Portrange(4000, 5000) : BAU_PORTS);

    public static final String SERVER_PATH = "/servers/";

    private static final String TMP_DATA = System.getProperty("user.home") + "/";
    private static final String EVENT_PATH = TMP_DATA + "event/";
    public static final String TEMP_WORLD_PATH = TMP_DATA + "arenaserver/";

    private static final String WORLDS_FOLDER = "/worlds";
    public static final String WORLDS_BASE_PATH = WORLDS_FOLDER + "/userworlds";
    public static final String BUILDER_BASE_PATH = WORLDS_FOLDER + "/builder";

    public static final String WORLDS_STORAGE_BASE_PATH = "/mnt/storage/worlds/userworlds";

    private File directory = null;
    private String worldDir = null;

    private Node node = null;
    private ServerVersion version = ServerVersion.PAPER_21;
    private Portrange portrange = BAU_PORTS;
    private Function<Integer, String> serverNameProvider = port -> node.getName() + port;
    private BooleanSupplier startCondition = () -> true;
    private Runnable worldSetup = () -> {
    };
    private String worldName = null;
    private Runnable worldCleanup = () -> {
    };
    private boolean allowMerge = false;
    private String fightMap = null;
    private String gameMode = null;
    private boolean checkpoint = false;
    private ServerConstructor constructor = (serverName, port, builder, shutdownCallback, failureCallback) -> new Arenaserver(serverName, gameMode, fightMap, allowMerge, port, builder, shutdownCallback);
    private Consumer<Subserver> callback = subserver -> {
    };
    private Object responsible = new Object();

    private final Set<Player> playersToSend = new HashSet<>();
    private final Map<String, String> arguments = new HashMap<>();

    public ServerStarter arena(GameModeConfig<String, String> mode, String map) {
        portrange = ARENA_PORTS;
        serverNameProvider = port -> mode.GameName + (port - portrange.start);
        version = ServerVersion.from(mode);
        allowMerge = true;
        fightMap = map;
        gameMode = mode.configFile.getName().replace(".yml", "");
        directory = new File(SERVER_PATH, mode.Server.Folder);
        arguments.put("config", mode.configFile.getName());
        tempWorld(SERVER_PATH + mode.Server.Folder + "/arenas/" + map);
        startCondition = () -> {
            if (playersToSend.stream().anyMatch(player -> Subserver.isArena(Subserver.getSubserver(player)))) {
                playersToSend.forEach(player -> Chatter.of(player).system("FIGHT_IN_ARENA"));
                return false;
            }

            return true;
        };
        return this;
    }

    public ServerStarter event(EventFight eventFight) {
        arena(ArenaMode.getByInternal(eventFight.getSpielmodus()), eventFight.getMap());
        node = VelocityCore.local;
        worldDir = EVENT_PATH + "/" + serverToWorldName(Event.byId(eventFight.getEventID()).getEventName()) + "/" + eventFight.getStartTime().toLocalDateTime().format(DateTimeFormatter.ISO_LOCAL_DATE);
        worldCleanup = () -> {
        };
        arguments.put("fightID", String.valueOf(eventFight.getFightID()));
        fightMap = eventFight.getMap();
        gameMode = eventFight.getSpielmodus();

        String serverName = Team.byId(eventFight.getTeamBlue()).getTeamKuerzel() + "_vs_" + Team.byId(eventFight.getTeamRed()).getTeamKuerzel();
        serverNameProvider = port -> serverName;
        worldName = serverToWorldName(serverName + eventFight.getStartTime().toLocalDateTime().format(DateTimeFormatter.ISO_TIME));
        return this;
    }

    public ServerStarter test(GameModeConfig<String, String> mode, String map, Player owner) {
        arena(mode, map);
        buildWithTemp(owner);
        portrange = BAU_PORTS;
        arguments.put("fightID", String.valueOf(-1));
        return send(owner);
    }

    public ServerStarter blueLeader(Player player) {
        arguments.put("blueLeader", player.getUniqueId().toString());
        return send(player);
    }

    public ServerStarter redLeader(Player player) {
        arguments.put("redLeader", player.getUniqueId().toString());
        return send(player);
    }

    public ServerStarter check(int schemID) {
        arguments.put("checkSchemID", String.valueOf(schemID));
        return this;
    }

    public ServerStarter prepare(int schemID) {
        arguments.put("prepareSchemID", String.valueOf(schemID));
        return this;
    }

    public ServerStarter replay(int replayID) {
        arguments.put("replay", String.valueOf(replayID));
        return this;
    }

    public ServerStarter build(ServerVersion version, UUID owner) {
        this.version = version;
        directory = version.getServerDirectory("Bau");
        worldDir = version.getWorldFolder(WORLDS_BASE_PATH);
        worldName = String.valueOf(SteamwarUser.get(owner).getId());
        checkpoint = true;

        build(owner);

        worldSetup = () -> {
            File world = new File(worldDir, worldName);
            if (!world.exists()) {
                File storage = new File(version.getWorldFolder(WORLDS_STORAGE_BASE_PATH), worldName);

                if (storage.exists()) {
                    node.execute("mv", storage.getPath(), world.getPath());
                } else {
                    copyWorld(node, new File(directory, "Bauwelt").getPath(), world.getPath());
                }
            }
        };

        // Send players to existing server
        startCondition = () -> {
            Bauserver subserver = Bauserver.get(owner);
            if (subserver != null) {
                for (Player p : playersToSend) {
                    SubserverSystem.sendPlayer(subserver, p);
                }
                return false;
            }
            boolean atLeastOneSupervisor = playersToSend.stream().anyMatch(player -> {
                if (player.getUniqueId().equals(owner)) return true;
                BauweltMember bauweltMember = BauweltMember.getBauMember(owner, player.getUniqueId());
                return bauweltMember.isSupervisor();
            });
            if (!atLeastOneSupervisor) {
                for (Player p : playersToSend) {
                    Chatter.of(p).system("BAU_START_NOT_ALLOWED");
                }
            }
            return atLeastOneSupervisor;
        };

        return this;
    }

    private void tempWorld(String template) {
        worldDir = TEMP_WORLD_PATH;
        worldSetup = () -> {
            mkdirWorldDir(node, worldDir);
            copyWorld(node, template, new File(worldDir, worldName).getPath());
        };
        worldCleanup = () -> SubserverSystem.deleteFolder(node, new File(worldDir, worldName).getPath());
    }

    private void buildWithTemp(Player owner) {
        build(owner.getUniqueId());

        // Stop existing build server
        startCondition = () -> {
            if (startingBau(owner)) return false;

            Bauserver subserver = Bauserver.get(owner.getUniqueId());
            if (subserver != null && subserver.isStarted()) subserver.sleep();

            return !startingBau(owner);
        };
    }

    private void build(UUID owner) {
        constructor = (serverName, port, builder, shutdownCallback, failureCallback) -> new Bauserver(serverName, owner, port, builder, shutdownCallback, failureCallback);
        serverNameProvider = port -> bauServerName(SteamwarUser.get(owner));
    }

    public ServerStarter builder(ServerVersion version, String map, File generator) {
        this.version = version;
        directory = version.getServerDirectory("Builder");
        worldDir = version.getWorldFolder(BUILDER_BASE_PATH);
        worldName = map;
        serverNameProvider = port -> "*" + map;
        checkpoint = true;
        constructor = (serverName, port, builder, shutdownCallback, failureCallback) -> new Builderserver(serverName, worldName, port, builder, shutdownCallback, failureCallback);

        // Send players to existing server
        startCondition = () -> {
            Builderserver subserver = Builderserver.get(worldName);
            if (subserver != null) {
                for (Player p : playersToSend) {
                    SubserverSystem.sendPlayer(subserver, p);
                }
                return false;
            }
            return true;
        };

        if (generator != null) {
            worldSetup = () -> {
                File leveldat = new File(new File(worldDir, worldName), "level.dat");
                try {
                    Files.copy(generator.toPath(), leveldat.toPath());
                } catch (IOException e) {
                    throw new SecurityException(e);
                }
            };
        }

        return this;
    }

    public ServerStarter send(Player player) {
        playersToSend.add(player);

        if (!(responsible instanceof Player)) {
            responsible = player;
        }

        return this;
    }

    public ServerStarter callback(Consumer<Subserver> callback) {
        this.callback = callback;
        return this;
    }

    public void start() {
        VelocityCore.schedule(() -> {
            synchronized (responsible) {
                if (!startCondition.getAsBoolean()) return;

                int port = portrange.freePort();
                String serverName = serverNameProvider.apply(port);
                arguments.put("serverName", serverName);

                if (node == null) {
                    node = Node.getNode();
                    if (node == null) {
                        for (Player p : playersToSend) {
                            Chatter.of(p).system("SERVER_START_OVERLOAD");
                        }

                        return;
                    }
                }
                if (worldName == null) {
                    worldName = serverToWorldName(serverName);
                }

                worldSetup.run();
                arguments.put("logPath", worldName);

                File checkpointDir = new File("/tmp/" + System.getProperty("user.name") + ".checkpoints/" + directory.getName() + "/" + worldName);
                if (checkpoint) {
                    arguments.put("checkpoint", checkpointDir.getPath());
                }

                ((VelocityViaConfig) Via.getConfig()).getVelocityServerProtocols().put(serverName, version.getProtocolVersion().getProtocol());
                if (checkpoint && checkpointDir.exists()) {
                    try (DataOutputStream out = new DataOutputStream(Files.newOutputStream(new File(checkpointDir, "port").toPath()))) {
                        out.writeInt(port);
                    } catch (IOException e) {
                        throw new SecurityException(e);
                    }

                    postStart(constructor.construct(serverName, port, node.prepareExecution(
                            "criu", "restore", "-D", checkpointDir.getPath(), "--auto-dedup", "--shell-job", "-v"
                    ), worldCleanup, e -> regularStart(serverName, port)));
                } else {
                    regularStart(serverName, port);
                }
            }
        }).schedule();
    }

    private void regularStart(String serverName, int port) {
        postStart(constructor.construct(serverName, port, node.startServer(
                version, directory, worldDir, worldName, port, arguments.entrySet().stream().map(entry -> entry.getKey() + "=" + entry.getValue()).toArray(String[]::new)
        ), worldCleanup, null));
    }

    private void postStart(Subserver subserver) {
        for (Player p : playersToSend) {
            SubserverSystem.sendPlayer(subserver, p);
        }

        callback.accept(subserver);
    }

    private static boolean startingBau(Player p) {
        Bauserver subserver = Bauserver.get(p.getUniqueId());
        if (subserver != null && !subserver.isStarted()) {
            Chatter.of(p).system("BAU_START_ALREADY");
            return true;
        }
        return false;
    }

    public static String bauServerName(SteamwarUser user) {
        return user.getUserName() + "s Bau";
    }

    public static String serverToWorldName(String serverName) {
        return serverName.replace(' ', '_').replace("[", "").replace("]", "").replace(".", "");
    }

    public static void mkdirWorldDir(Node node, String targetDir) {
        node.execute("mkdir", "p", targetDir);
    }

    public static void copyWorld(Node node, String template, String target) {
        node.execute("cp", "-r", template, target);
    }

    private interface ServerConstructor {
        Subserver construct(String serverName, int port, ProcessBuilder builder, Runnable shutdownCallback, Consumer<Exception> failureCallback);
    }

    private static class Portrange {

        private final int start;
        private final int end;
        private int current;

        private Portrange(int start, int end) {
            this.start = start;
            this.end = end;
            current = start;
        }

        private void increment() {
            current++;
            if (current == end) current = start;
        }

        private synchronized int freePort() {
            Set<Integer> usedPorts;

            synchronized (Subserver.getServerList()) {
                usedPorts = Subserver.getServerList().stream().map(server -> server.getServer().getAddress().getPort()).collect(Collectors.toSet());
            }

            while (usedPorts.contains(current)) {
                increment();
            }

            int result = current;
            increment();
            return result;
        }
    }

}
