/*
 *   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.persistent;

import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.proxy.server.RegisteredServer;
import com.velocitypowered.api.proxy.server.ServerInfo;
import lombok.Getter;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;

import java.io.*;
import java.net.InetSocketAddress;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.logging.Level;
import java.util.logging.Logger;

public class Subserver {

    public static boolean isArena(Subserver subserver) {
        return subserver instanceof Arenaserver;
    }

    public static boolean isBuild(Subserver subserver) {
        return subserver instanceof Bauserver;
    }

    public static boolean isBuilder(Subserver subserver) {
        return subserver instanceof Builderserver;
    }

    private static final Component PREFIX = Component
            .text("Steam").color(NamedTextColor.YELLOW)
            .append(Component.text("War» ").color(NamedTextColor.DARK_GRAY));

    private static final Logger logger = Persistent.getInstance().getLogger();

    @Getter
    private static final List<Subserver> serverList = new LinkedList<>();
    private static final Map<ServerInfo, Subserver> infoToServer = new HashMap<>();

    public static Subserver getSubserver(Player p) {
        synchronized (serverList) {
            for (int i = serverList.size() - 1; i >= 0; i--) {
                if (serverList.get(i).onServer(p))
                    return serverList.get(i);
            }
        }
        return null;
    }

    public static Subserver getSubserver(ServerInfo server) {
        synchronized (serverList) {
            return infoToServer.get(server);
        }
    }

    static void shutdown() {
        while (!serverList.isEmpty()) {
            Subserver server = serverList.get(0);
            server.stop();
        }
    }

    private final String serverName;
    private final boolean checkpoint;
    private final Runnable shutdownCallback;
    private final Consumer<Exception> failureCallback;
    private final Process process;
    private final PrintWriter writer;
    @Getter
    private final ServerInfo server;
    @Getter
    private RegisteredServer registeredServer;
    private final Thread thread;
    @Getter
    private boolean started;

    private final List<Player> cachedPlayers = new LinkedList<>();
    @Getter
    private final Map<Player, String> tablistNames = new HashMap<>();

    protected Subserver(String serverName, int port, ProcessBuilder processBuilder, Runnable shutdownCallback, Consumer<Exception> failureCallback) {
        this.started = false;
        this.serverName = serverName;
        this.shutdownCallback = shutdownCallback;
        this.failureCallback = failureCallback == null ? this::fatalError : failureCallback;
        this.checkpoint = processBuilder.command().contains("criu");
        this.thread = new Thread(this::run, "Subserver " + serverName);

        try {
            this.process = processBuilder.start();
        } catch (IOException e) {
            throw new SecurityException("Server could not be started", e);
        }

        InetSocketAddress address = new InetSocketAddress("127.0.0.1", port);
        this.server = new ServerInfo(serverName, address);
        this.writer = new PrintWriter(process.getOutputStream(), true);

        register();

        this.thread.start();
    }

    public void sendPlayer(Player p) {
        if (!started) {
            p.sendActionBar(generateBar(0));
            cachedPlayers.add(p);
        } else {
            p.createConnectionRequest(registeredServer).connect();
        }
    }

    public void execute(String command) {
        writer.println(command);
    }

    public void stop() {
        try {
            long pid = process.pid();
            if (checkpoint)
                pid = process.children().findAny().map(ProcessHandle::pid).orElse(pid);

            Runtime.getRuntime().exec(new String[]{"kill", "-SIGUSR1", Long.toString(pid)});
        } catch (IOException e) {
            logger.log(Level.SEVERE, "Failed to send SIGUSR1 to subserver.", e);
        }

        try {
            if (!process.waitFor(1, TimeUnit.MINUTES)) {
                logger.log(Level.SEVERE, () -> serverName + " did not stop correctly, forcibly stopping!");
                process.destroyForcibly();
            }

            if(thread.isAlive())
                thread.join();
        } catch (InterruptedException e) {
            logger.log(Level.SEVERE, "Subserver stop interrupted!", e);
            Thread.currentThread().interrupt();
        }
    }

    private boolean onServer(Player p) {
        return cachedPlayers.contains(p) || (registeredServer != null && registeredServer.getPlayersConnected().contains(p));
    }

    private void fatalError(Exception e) {
        logger.log(Level.SEVERE, e, () -> serverName + " did not run correctly!");

        for (Player cached : cachedPlayers)
            cached.sendMessage(PREFIX.append(Component.text("Unexpected error during server startup.").color(NamedTextColor.RED)));
        if (registeredServer != null) {
            for (Player player : registeredServer.getPlayersConnected())
                player.sendMessage(PREFIX.append(Component.text("Lost connection to server.").color(NamedTextColor.RED)));
        }
    }

    private void start(InputStream stream, Predicate<String> test) throws IOException {
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream))) {
            String line = "";
            while (!started && (line = reader.readLine()) != null) {
                started = test.test(line);
            }

            if (line == null)
                throw new IOException(serverName + " did not start correctly!");
        }
    }

    protected void register() {
        if (Persistent.getInstance().getProxy().getServer(serverName).isPresent()) {
            SecurityException e = new SecurityException("Server already registered: " + serverName);
            stop();
            failureCallback.accept(e);
            throw e;
        }

        synchronized (serverList) {
            registeredServer = Persistent.getInstance().getProxy().registerServer(server);
            serverList.add(this);
            infoToServer.put(server, this);
        }
    }

    protected void unregister() {
        synchronized (serverList) {
            infoToServer.remove(server);
            serverList.remove(this);
            Persistent.getInstance().getProxy().unregisterServer(server);
            registeredServer = null;
        }
    }

    private void run() {
        Exception ex = null;
        try {
            if (checkpoint) {
                start(process.getErrorStream(), line -> line.contains("Restore finished successfully."));
                start(process.getInputStream(), line -> line.contains("Checkpoint restored"));
                Thread.sleep(100);
            } else {
                sendProgress(0);
                start(process.getInputStream(), line -> {
                    if (line.contains("Loading libraries, please wait"))
                        sendProgress(2);
                    else if (line.contains("Starting Minecraft server on"))
                        sendProgress(5);
                    else if (line.contains("Preparing start region"))
                        sendProgress(8);
                    return line.contains("Done (");
                });
            }

            if (!started)
                return;

            sendProgress(10);
            for (Player cachedPlayer : cachedPlayers) {
                sendPlayer(cachedPlayer);
            }
            cachedPlayers.clear();

            process.waitFor();
        } catch (IOException e) {
            ex = e;
        } catch (InterruptedException e) {
            ex = e;
            Thread.currentThread().interrupt();
        } finally {
            unregister();
            shutdownCallback.run();
            if (ex != null)
                failureCallback.accept(ex);
        }
    }

    private Component generateBar(int progress) {
        return Component.text("⬛".repeat(Math.max(0, progress))).color(NamedTextColor.YELLOW)
                .append(Component.text("⬛".repeat(Math.max(0, 10 - progress))).color(NamedTextColor.DARK_GRAY));
    }

    private void sendProgress(int progress) {
        Component tc = generateBar(progress);
        for (Player cached : cachedPlayers)
            cached.sendActionBar(tc);
    }
}
