/*
 *   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 java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.logging.Level;

public abstract class Node {

    //-Xquickstart Langzeitperformance testen!
    private static final List<String> OPENJ9_ARGS = Arrays.asList(
            "-XX:+EnableCRIUSupport", "-XX:-CRIURestoreNonPortableMode",
            "-Xgc:excessiveGCratio=80", "-Xdisableexplicitgc", "-Xnoclassgc", "-Xmos128M", "-Xmns48M", "-XX:+ExitOnOutOfMemoryError", // initial heap half values of memory observed by 1.19 spectate server
            "-Xsyslog:none", "-Xtrace:none", "-Xverify:none", "-Xdump:system:none", "-Xdump:jit:none", "-Xdump:snap:none", "-Xdump:heap:opts=hprof",
            "-XX:+EnableDynamicAgentLoading", "-Dlog4j.configurationFile=log4j2.xml"
    );
    private static final Set<String> JAVA_8 = new HashSet<>();

    static {
        JAVA_8.add("paper-1.8.8.jar");
        JAVA_8.add("paper-1.10.2.jar");
        JAVA_8.add("spigot-1.8.8.jar");
        JAVA_8.add("spigot-1.9.4.jar");
        JAVA_8.add("spigot-1.10.2.jar");
    }

    private static final long MIN_FREE_MEM = 4 * 1024 * 1024L; // 4 GiB

    private static final List<Node> nodes = new ArrayList<>();

    public static Node getNode() {
        for (Node node : nodes) {
            if (node.belowLoadLimit) return node;
        }
        return null;
    }

    public static void forEach(Consumer<Node> consumer) {
        nodes.forEach(consumer);
    }

    public abstract ProcessBuilder startServer(String serverJar, File directory, String worldDir, String levelName, int port, String... dParams);

    protected abstract ProcessBuilder prepareExecution(String... command);

    protected abstract void calcLoadLimit();

    protected final String hostname;
    protected volatile boolean belowLoadLimit = true;

    protected Node(String hostname) {
        this.hostname = hostname;
        nodes.add(this);
        VelocityCore.schedule(this::calcLoadLimit).repeat(2, TimeUnit.SECONDS).schedule();
    }

    public void execute(String... command) {
        try {
            prepareExecution(command).start().waitFor();
        } catch (IOException e) {
            throw new SecurityException("Could not execute command", e);
        } catch (InterruptedException e) {
            VelocityCore.getLogger().log(Level.SEVERE, "Interrupted during execution", e);
            Thread.currentThread().interrupt();
        }
    }

    public boolean belowLoadLimit() {
        return belowLoadLimit;
    }

    public String getName() {
        return hostname;
    }

    protected void constructServerstart(File directory, List<String> cmd, String serverJar, String worldDir, String levelName, int port, String... dParams) {
        if (JAVA_8.contains(serverJar)) {
            cmd.add("/usr/lib/jvm/openj9-8/bin/java");
        } else {
            cmd.add("/usr/lib/jvm/openj9-21/bin/java");
        }

        for (String param : dParams) {
            cmd.add("-D" + param);
        }
        cmd.add("-Xshareclasses:nonfatal,name=" + directory.getName());
        cmd.add("-Xmx768M");
        cmd.addAll(OPENJ9_ARGS);
        if (!JAVA_8.contains(serverJar)) {
            cmd.add("--add-opens");
            cmd.add("java.base/jdk.internal.misc=ALL-UNNAMED");
            cmd.add("-XX:-CRIUSecProvider");
        }
        cmd.add("-jar");
        cmd.add("/jars/" + serverJar);
        cmd.add("--log-strip-color");
        cmd.add("--world-dir");
        cmd.add(worldDir);
        cmd.add("--level-name");
        cmd.add(levelName);
        cmd.add("--port");
        cmd.add(String.valueOf(port));
        cmd.add("nogui");
    }

    protected void calcLoadLimit(BufferedReader meminfo) throws IOException {
        String line = meminfo.readLine();
        while (!line.startsWith("MemAvailable")) {
            line = meminfo.readLine();
        }

        long availableMem = Long.parseLong(line.replaceAll(" +", " ").split(" ")[1]);
        belowLoadLimit = availableMem >= MIN_FREE_MEM;
    }

    public static class LocalNode extends Node {
        private static final File MEMINFO = new File("/proc/meminfo");

        public LocalNode() {
            super("sw");
        }

        @Override
        public ProcessBuilder startServer(String serverJar, File directory, String worldDir, String levelName, int port, String... dParams) {
            List<String> cmd = new ArrayList<>();
            constructServerstart(directory, cmd, serverJar, worldDir, levelName, port, dParams);
            ProcessBuilder builder = new ProcessBuilder(cmd);
            builder.directory(directory);
            return builder;
        }

        @Override
        protected void calcLoadLimit() {
            try (BufferedReader meminfo = new BufferedReader(new InputStreamReader(Files.newInputStream(MEMINFO.toPath())))) {
                calcLoadLimit(meminfo);
            } catch (IOException e) {
                VelocityCore.getLogger().log(Level.SEVERE, "Could not read local load", e);
                belowLoadLimit = false;
            }
        }

        @Override
        protected ProcessBuilder prepareExecution(String... command) {
            return new ProcessBuilder(command);
        }
    }

    public static class RemoteNode extends Node {

        public RemoteNode(String hostname) {
            super(hostname);
            VelocityCore.getLogger().log(Level.INFO, "Added node {0}", hostname);
        }

        @Override
        public ProcessBuilder startServer(String serverJar, File directory, String worldDir, String levelName, int port, String... dParams) {
            List<String> cmd = new ArrayList<>();
            cmd.add("ssh");
            cmd.add("-L");
            cmd.add(port + ":localhost:" + port);
            cmd.add(hostname);
            cmd.add("cd");
            cmd.add(directory.getPath());
            cmd.add(";");
            constructServerstart(directory, cmd, serverJar, worldDir, levelName, port, dParams);
            return new ProcessBuilder(cmd);
        }

        @Override
        protected ProcessBuilder prepareExecution(String... command) {
            List<String> cmd = new ArrayList<>();
            cmd.add("ssh");
            cmd.add(hostname);
            cmd.addAll(Arrays.asList(command));
            return new ProcessBuilder(cmd);
        }

        @Override
        protected void calcLoadLimit() {
            try {
                Process process = prepareExecution("cat /proc/meminfo").start();
                if (!process.waitFor(1, TimeUnit.SECONDS)) {
                    throw new IOException(hostname + " timeout");
                }
                try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
                    calcLoadLimit(reader);
                }
            } catch (IOException e) {
                if (belowLoadLimit) {
                    VelocityCore.getLogger().log(Level.SEVERE, "Could read remote load", e);
                }
                belowLoadLimit = false;
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                belowLoadLimit = false;
            }
        }
    }
}
