package net.viper.status;

import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.config.ListenerInfo;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.chat.ClickEvent;
import net.md_5.bungee.api.chat.HoverEvent;
import net.md_5.bungee.api.chat.hover.content.Text;
import net.md_5.bungee.api.chat.ClickEvent;
import net.md_5.bungee.api.chat.HoverEvent;
import net.md_5.bungee.api.chat.hover.content.Text;
import net.md_5.bungee.api.event.PlayerDisconnectEvent;
import net.md_5.bungee.api.event.PostLoginEvent;
import net.md_5.bungee.api.plugin.Listener;
import net.md_5.bungee.api.plugin.Plugin;
import net.md_5.bungee.event.EventHandler;
import net.viper.status.module.ModuleManager;
import net.viper.status.modules.economy.EconomyModule;
import net.viper.status.modules.tablist.TablistModule;
import net.viper.status.modules.scoreboard.ScoreboardModule;
import net.viper.status.modules.antibot.AntiBotModule;
import net.viper.status.modules.network.NetworkInfoModule;
import net.viper.status.modules.network.MultiAccountGuard;
import net.viper.status.modules.AutoMessage.AutoMessageModule;
import net.viper.status.modules.customcommands.CustomCommandModule;
import net.viper.status.modules.serverswitcher.ServerSwitcherModule;
import net.viper.status.stats.PlayerStats;
import net.viper.status.stats.StatsModule;
import net.viper.status.modules.verify.VerifyModule;
import net.viper.status.modules.commandblocker.CommandBlockerModule;
import net.viper.status.modules.broadcast.BroadcastModule;
import net.viper.status.modules.chat.ChatModule;
import net.viper.status.modules.vanish.VanishModule;
import net.viper.status.modules.help.HelpModule;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.StringReader;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import net.md_5.bungee.api.scheduler.ScheduledTask;

/**
 * StatusAPI - zentraler BungeeCord HTTP-Status- und Broadcast-Endpunkt
 */
public class StatusAPI extends Plugin implements Runnable, Listener {

    // Welt pro Spieler (UUID -> Weltname), wird von StatusAPIBridge gepusht
    public static final ConcurrentHashMap<UUID, String> playerWorlds = new ConcurrentHashMap<>();

    // Kontostand pro Spieler (UUID -> Balance), wird von StatusAPIBridge gepusht
    public static final ConcurrentHashMap<UUID, Double> playerBalances = new ConcurrentHashMap<>();

    // PlaceholderAPI-Werte pro Spieler (UUID -> (placeholder -> aufgel\u00f6ster Wert))
    public static final ConcurrentHashMap<UUID, Map<String, String>> playerPapi = new ConcurrentHashMap<>();

    // AFK-Status pro Spieler (UUID -> true wenn AFK), wird von StatusAPIBridge gepusht
    public static final ConcurrentHashMap<UUID, Boolean> playerAfk = new ConcurrentHashMap<>();

    /** Alle %token%-Tokens aus den Config-Dateien – als JSON-Array f\u00fcr GET /papi/tokens */
    public static volatile String papiTokensJson = "[]";

    // Debug-Modus (aus verify.properties)
    public static boolean DEBUG = false;

    /**
     * Login-Logger: UUID, Name und IP bei jedem Join in player-logins.log schreiben.
     * Vor dem Kompilieren auf true setzen um den Logger zu aktivieren.
     */
    private static final boolean LOGIN_LOGGER_ENABLED = false;

    /** Gibt eine Info-Meldung nur im Debug-Modus aus */
    public static void debugLog(Plugin plugin, String message) {
        if (DEBUG) plugin.getLogger().info(message);
    }

    private volatile Thread thread;
    private volatile ServerSocket serverSocket;
    private volatile boolean shuttingDown = false;
    private int port = 9191;
    private ScheduledTask httpWatchdogTask;
    private ExecutorService requestExecutor;
    private final AtomicLong lastHttpRequestAt = new AtomicLong(0L);

    private ModuleManager moduleManager;
    private UpdateChecker updateChecker;
    private Properties verifyProperties;

    @Override
    public void onEnable() {

        if (!getDataFolder().exists()) {
            getDataFolder().mkdirs();
        }

        mergeVerifyConfig();

        // Port aus verify.properties lesen
        String portStr = verifyProperties != null ? verifyProperties.getProperty("statusapi.port", "9191") : "9191";
        try {
            port = Integer.parseInt(portStr);
        } catch (NumberFormatException e) {
            getLogger().warning("Ung\u00fcltiger Port in verify.properties, nutze Standard-Port 9191.");
            port = 9191;
        }

        // Debug-Modus
        DEBUG = verifyProperties != null && Boolean.parseBoolean(verifyProperties.getProperty("debug", "false"));

        moduleManager = new ModuleManager();

        // Module in korrekter Reihenfolge registrieren
        // VanishModule MUSS vor ChatModule registriert werden (VanishProvider-Abh\u00e4ngigkeit)
        moduleManager.registerModule(new net.viper.status.modules.afk.AfkModule());
        moduleManager.registerModule(new StatsModule());
        moduleManager.registerModule(new HelpModule());
        moduleManager.registerModule(new VerifyModule());
        moduleManager.registerModule(new BroadcastModule());
        moduleManager.registerModule(new CommandBlockerModule());
        moduleManager.registerModule(new VanishModule());
        moduleManager.registerModule(new ChatModule());
        moduleManager.registerModule(new AntiBotModule());
        moduleManager.registerModule(new NetworkInfoModule());
        moduleManager.registerModule(new MultiAccountGuard());
        moduleManager.registerModule(new AutoMessageModule());
        moduleManager.registerModule(new CustomCommandModule());
        moduleManager.registerModule(new ServerSwitcherModule());
        moduleManager.registerModule(new EconomyModule());
        moduleManager.registerModule(new TablistModule());
        moduleManager.registerModule(new ScoreboardModule());

        try {
            Class<?> forumBridge = Class.forName("net.viper.status.modules.forum.ForumBridgeModule");
            Object forumBridgeInstance = forumBridge.getDeclaredConstructor().newInstance();
            moduleManager.registerModule((net.viper.status.module.Module) forumBridgeInstance);
        } catch (Exception e) {
            getLogger().warning("ForumBridgeModule konnte nicht geladen werden: " + e.getMessage());
        }

        moduleManager.enableAll(this);

        // PAPI-Tokens sofort scannen + nochmal nach 5s als Fallback (falls Configs erst beim Enable erstellt)
        scanAndPublishPapiTokens();
        ProxyServer.getInstance().getScheduler().schedule(this, this::scanAndPublishPapiTokens, 5, TimeUnit.SECONDS);

        // /statusapi reload Befehl registrieren
        ProxyServer.getInstance().getPluginManager().registerCommand(this, new StatusAPICommand(this));

        // PlayerLoginLogger: schreibt UUID, Name und IP bei jedem Join in player-logins.log
        // Aktivieren: LOGIN_LOGGER_ENABLED = true setzen und neu kompilieren
        if (LOGIN_LOGGER_ENABLED) {
            Properties loginProps = loadNetworkGuardProperties();
            String tzRaw = loginProps.getProperty("login.log.timezone", "UTC").trim();
            java.time.ZoneId loginZone;
            try {
                loginZone = java.time.ZoneId.of(tzRaw);
            } catch (java.time.zone.ZoneRulesException e) {
                getLogger().warning("[PlayerLoginLogger] Unbekannte Zeitzone '" + tzRaw + "' – fallback auf UTC.");
                loginZone = java.time.ZoneId.of("UTC");
            }
            PlayerLoginLogger loginLogger = new PlayerLoginLogger(this, loginZone);
            ProxyServer.getInstance().getPluginManager().registerListener(this, loginLogger);
            getLogger().info("[PlayerLoginLogger] Login-Logging aktiv -> " + getDataFolder() + "/player-logins.log (Zeitzone: " + loginZone + ")");
        }

        // Memory-Leak-Fix: playerWorlds/playerPapi beim Disconnect bereinigen
        ProxyServer.getInstance().getPluginManager().registerListener(this, this);

        // FIX: ScoreboardModule mit NetworkInfoModule verbinden (TPS-Fallback)
        try {
            net.viper.status.modules.scoreboard.ScoreboardModule sbMod =
                (net.viper.status.modules.scoreboard.ScoreboardModule) moduleManager.getModule("ScoreboardModule");
            net.viper.status.modules.network.NetworkInfoModule nimMod =
                (net.viper.status.modules.network.NetworkInfoModule) moduleManager.getModule("NetworkInfoModule");
            if (sbMod != null && nimMod != null) {
                sbMod.setNetworkInfoModule(nimMod);
                getLogger().info("[StatusAPI] ScoreboardModule → NetworkInfoModule TPS-Fallback verbunden.");
            }
        } catch (Exception e) {
            getLogger().warning("[StatusAPI] TPS-Fallback konnte nicht verbunden werden: " + e.getMessage());
        }

        // WebServer starten
        shuttingDown = false;
        requestExecutor = Executors.newFixedThreadPool(4, r -> {
            Thread t = new Thread(r, "StatusAPI-HTTP-Worker");
            t.setDaemon(true);
            return t;
        });
        startHttpServerThread();
        httpWatchdogTask = ProxyServer.getInstance().getScheduler().schedule(
                this, this::ensureHttpServerAlive, 15, 15, TimeUnit.SECONDS);

        // Update-Checker
        String currentVersion = getDescription() != null ? getDescription().getVersion() : "0.0.0";
        updateChecker = new UpdateChecker(this, currentVersion, 6);
        checkAndMaybeUpdate();
        ProxyServer.getInstance().getScheduler().schedule(this, this::checkAndMaybeUpdate, 6, 6, TimeUnit.HOURS);
    }

    @Override
    public void onDisable() {
        shuttingDown = true;
        if (moduleManager != null) {
            moduleManager.disableAll(this);
        }
        if (httpWatchdogTask != null) {
            httpWatchdogTask.cancel();
            httpWatchdogTask = null;
        }
        stopHttpServerThread();
        if (requestExecutor != null) {
            requestExecutor.shutdownNow();
            requestExecutor = null;
        }
    }

    private synchronized void startHttpServerThread() {
        if (thread != null && thread.isAlive()) {
            return;
        }
        thread = new Thread(this, "StatusAPI-HTTP-Server");
        thread.setDaemon(true);
        thread.start();
    }

    private synchronized void stopHttpServerThread() {
        Thread localThread = thread;
        if (localThread != null) {
            localThread.interrupt();
        }
        ServerSocket localServerSocket = serverSocket;
        if (localServerSocket != null) {
            try {
                localServerSocket.close();
            } catch (IOException ignored) {
            }
        }
        if (localThread != null) {
            try {
                localThread.join(1500);
            } catch (InterruptedException ignored) {
                Thread.currentThread().interrupt();
            }
        }
        thread = null;
    }

    private void ensureHttpServerAlive() {
        if (shuttingDown) return;
        Thread t = thread;
        if (t == null || !t.isAlive()) {
            getLogger().warning("HTTP-Server-Thread war gestoppt und wird neu gestartet.");
            startHttpServerThread();
        }
    }

    // --- MERGE LOGIK ---
    private void mergeVerifyConfig() {
        try {
            File file = new File(getDataFolder(), "verify.properties");
            verifyProperties = new Properties();
            if (file.exists()) {
                try (java.io.InputStreamReader reader = new java.io.InputStreamReader(
                        new FileInputStream(file), StandardCharsets.UTF_8)) {
                    verifyProperties.load(reader);
                }
            } else {
                getLogger().warning("verify.properties nicht gefunden.");
            }
        } catch (IOException e) {
            getLogger().severe("Fehler beim Laden der verify.properties: " + e.getMessage());
        }
    }

    public Properties getVerifyProperties() {
        synchronized (this) {
            return verifyProperties;
        }
    }

    public ModuleManager getModuleManager() {
        return moduleManager;
    }

    // --- Update-Logik ---
    private void checkAndMaybeUpdate() {
        try {
            updateChecker.checkNow();
            String currentVersion = getDescription() != null ? getDescription().getVersion() : "0.0.0";
            if (updateChecker.isUpdateAvailable(currentVersion)) {
                String newVersion  = updateChecker.getLatestVersion();
                String releaseUrl  = updateChecker.getLatestUrl();
                boolean preRelease = updateChecker.isLatestPreRelease();
                // Konsolen-Log
                getLogger().warning((preRelease ? "[Pre-Release] " : "") + "Update verfuegbar: " + newVersion + " -> " + releaseUrl);
                // Ingame-Meldung an alle Spieler mit statusapi.update.notify
                broadcastUpdateNotice(newVersion, releaseUrl, preRelease);
            }
        } catch (Exception e) {
            getLogger().severe("Fehler beim Update-Check: " + e.getMessage());
        }
    }

    /** Sendet die Update-Meldung an alle online Spieler mit statusapi.update.notify */
    private void broadcastUpdateNotice(String newVersion, String releaseUrl, boolean preRelease) {
        String current = getDescription() != null ? getDescription().getVersion() : "?";
        sendUpdateMessage(ProxyServer.getInstance().getConsole(), newVersion, current, releaseUrl, preRelease);
        for (net.md_5.bungee.api.connection.ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
            if (p.hasPermission("statusapi.update.notify")) {
                sendUpdateMessage(p, newVersion, current, releaseUrl, preRelease);
            }
        }
    }

    private static final String RELEASES_URL = "https://git.viper.ipv64.net/M_Viper/StatusAPI/releases";

    /** Formatiert und schickt die Update-Meldung an einen einzelnen Empfaenger */
    private static void sendUpdateMessage(net.md_5.bungee.api.CommandSender target,
                                          String newVersion, String currentVersion, String releaseUrl,
                                          boolean preRelease) {
        // Einfache Textzeilen
        String typeLabel  = preRelease ? "&e&lPre-Release" : "&a&lRelease";
        String typeNotice = preRelease ? "&eVorsicht: Dies ist ein Pre-Release und kann instabil sein!" : "";
        String[] plainLines = preRelease ? new String[]{
            "&8&m" + repeat("-", 44),
            "&6&l StatusAPI &7\u2013 &e&lPre-Release verf\u00fcgbar!",
            "&7Aktuelle Version: &c" + currentVersion,
            "&7Neue Version:     &e" + newVersion + " &7[Pre-Release]",
            "&eVorsicht: &7Kann instabil sein, bitte testen!",
        } : new String[]{
            "&8&m" + repeat("-", 44),
            "&6&l StatusAPI &7\u2013 Update verf\u00fcgbar!",
            "&7Aktuelle Version: &c" + currentVersion,
            "&7Neue Version:     &a" + newVersion,
        };
        for (String line : plainLines) {
            target.sendMessage(new net.md_5.bungee.api.chat.TextComponent(
                net.md_5.bungee.api.ChatColor.translateAlternateColorCodes('&', line)));
        }

        // Klickbare Link-Zeile (nur fuer ProxiedPlayer, Konsole bekommt Plaintext)
        if (target instanceof net.md_5.bungee.api.connection.ProxiedPlayer) {
            net.md_5.bungee.api.chat.TextComponent prefix = new net.md_5.bungee.api.chat.TextComponent(
                net.md_5.bungee.api.ChatColor.translateAlternateColorCodes('&', "&7Releases:         "));
            net.md_5.bungee.api.chat.TextComponent link = new net.md_5.bungee.api.chat.TextComponent(
                net.md_5.bungee.api.ChatColor.translateAlternateColorCodes('&', "&b&n" + RELEASES_URL));
            link.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, RELEASES_URL));
            link.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT,
                new Text(net.md_5.bungee.api.ChatColor.translateAlternateColorCodes('&',
                    "&7Klicke um die Release-Seite zu \u00f6ffnen"))));
            prefix.addExtra(link);
            target.sendMessage(prefix);
        } else {
            target.sendMessage(new net.md_5.bungee.api.chat.TextComponent(
                net.md_5.bungee.api.ChatColor.translateAlternateColorCodes('&', "&7Releases:         &b" + RELEASES_URL)));
        }

        target.sendMessage(new net.md_5.bungee.api.chat.TextComponent(
            net.md_5.bungee.api.ChatColor.translateAlternateColorCodes('&', "&8&m" + repeat("-", 44))));
    }

        private static String repeat(String s, int n) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < n; i++) sb.append(s);
        return sb.toString();
    }

    // Memory-Leak-Fix: playerWorlds und playerPapi beim Spieler-Logout bereinigen.
    // Diese Maps werden über HTTP von der Bridge befüllt und haben sonst keinen
    // Disconnect-Handler, was zu unbegrenztem Wachstum führt.
    @EventHandler
    public void onPlayerDisconnect(PlayerDisconnectEvent event) {
        UUID id = event.getPlayer().getUniqueId();
        playerWorlds.remove(id);
        playerPapi.remove(id);
    }

    /** Update-Hinweis beim Login für Spieler mit statusapi.update.notify */
    @EventHandler
    public void onPostLogin(PostLoginEvent event) {
        net.md_5.bungee.api.connection.ProxiedPlayer p = event.getPlayer();
        if (!p.hasPermission("statusapi.update.notify")) return;
        String current = getDescription() != null ? getDescription().getVersion() : "0.0.0";
        if (updateChecker != null && updateChecker.isUpdateAvailable(current)) {
            // Kurze Verzögerung damit der Spieler erst spawnt
            ProxyServer.getInstance().getScheduler().schedule(this, () ->
                sendUpdateMessage(p, updateChecker.getLatestVersion(), current, updateChecker.getLatestUrl(), updateChecker.isLatestPreRelease()),
                3, TimeUnit.SECONDS);
        }
    }

    // --- WebServer ---
    @Override
    public void run() {
        while (!shuttingDown && !Thread.currentThread().isInterrupted()) {
            // FIX #1: reuseAddress muss VOR dem bind() gesetzt werden.
            // new ServerSocket(port) bindet sofort → stattdessen unboundenen Socket anlegen.
            ServerSocket localServerSocket = null;
            try {
                localServerSocket = new ServerSocket();
                localServerSocket.setReuseAddress(true);
                localServerSocket.setSoTimeout(1000);
                localServerSocket.bind(new InetSocketAddress(port));
                this.serverSocket = localServerSocket;

                while (!shuttingDown && !Thread.currentThread().isInterrupted()) {
                    try {
                        Socket clientSocket = localServerSocket.accept();
                        submitConnection(clientSocket);
                    } catch (SocketTimeoutException ignored) {
                        // Poll-Schleife f\u00fcr Interrupt/Shutdown
                    } catch (IOException e) {
                        if (!shuttingDown) {
                            getLogger().warning("Fehler beim Akzeptieren einer Verbindung: " + e.getMessage());
                        }
                    } catch (Throwable t) {
                        if (!shuttingDown) {
                            getLogger().severe("Unbehandelter Fehler im HTTP-Accept-Loop: " + t.getMessage());
                        }
                    }
                }
            } catch (IOException e) {
                if (!shuttingDown) {
                    getLogger().severe("Konnte ServerSocket nicht starten: " + e.getMessage());
                    try {
                        Thread.sleep(2000L);
                    } catch (InterruptedException ignored) {
                        Thread.currentThread().interrupt();
                    }
                }
            } finally {
                if (localServerSocket != null) {
                    try { localServerSocket.close(); } catch (IOException ignored) {}
                }
                serverSocket = null;
            }
        }
    }

    private void submitConnection(Socket clientSocket) {
        if (clientSocket == null) return;
        try {
            clientSocket.setSoTimeout(5000);
            clientSocket.setTcpNoDelay(true);
        } catch (Exception ignored) {}

        ExecutorService executor = requestExecutor;
        if (executor == null || executor.isShutdown()) {
            try { clientSocket.close(); } catch (IOException ignored) {}
            return;
        }

        try {
            executor.execute(() -> {
                try {
                    handleConnection(clientSocket);
                } finally {
                    try { clientSocket.close(); } catch (IOException ignored) {}
                }
            });
        } catch (RejectedExecutionException ex) {
            try { clientSocket.close(); } catch (IOException ignored) {}
        }
    }

    private void handleConnection(Socket clientSocket) {
        try (BufferedReader in = new BufferedReader(
                     new InputStreamReader(clientSocket.getInputStream(), StandardCharsets.UTF_8));
             OutputStream out = clientSocket.getOutputStream()) {

            String inputLine = in.readLine();
            if (inputLine == null) return;
            lastHttpRequestAt.set(System.currentTimeMillis());

            String[] reqParts = inputLine.split(" ");
            if (reqParts.length < 2) return;
            String method  = reqParts[0].trim();
            String path    = reqParts[1].trim();
            String pathOnly = path;
            int queryIndex = path.indexOf('?');
            if (queryIndex >= 0) pathOnly = path.substring(0, queryIndex);

            // GET /health
            if ("GET".equalsIgnoreCase(method) && "/health".equalsIgnoreCase(path)) {
                long lastMs = lastHttpRequestAt.get();
                long age = lastMs <= 0L ? -1L : (System.currentTimeMillis() - lastMs);
                sendHttpResponse(out, "{\"success\":true,\"online\":true,\"last_request_age_ms\":" + age + "}", 200);
                return;
            }

            // GET /antibot/security-log
            if ("GET".equalsIgnoreCase(method) && "/antibot/security-log".equalsIgnoreCase(pathOnly)) {
                Map<String, Object> payload = new LinkedHashMap<>();
                payload.put("success", true);
                payload.put("events", loadAntiBotSecurityEvents(250));
                sendHttpResponse(out, buildJsonString(payload), 200);
                return;
            }

            // Headers lesen
            Map<String, String> headers = new HashMap<>();
            String line;
            while ((line = in.readLine()) != null && !line.isEmpty()) {
                int idx = line.indexOf(':');
                if (idx > 0) {
                    headers.put(line.substring(0, idx).trim().toLowerCase(Locale.ROOT),
                                line.substring(idx + 1).trim());
                }
            }

            // GET /network/backendguard/config
            if ("GET".equalsIgnoreCase(method) && path.equalsIgnoreCase("/network/backendguard/config")) {
                Properties guardProps = loadNetworkGuardProperties();
                String requiredApiKey = guardProps.getProperty("backendguard.sync.api_key", "").trim();
                String providedApiKey = headers.getOrDefault("x-api-key", headers.getOrDefault("x-apikey", ""));
                if (!requiredApiKey.isEmpty() && !requiredApiKey.equals(providedApiKey == null ? "" : providedApiKey.trim())) {
                    sendHttpResponse(out, "{\"success\":false,\"error\":\"bad_api_key\"}", 403);
                    return;
                }
                Map<String, Object> payload = new LinkedHashMap<>();
                payload.put("success", true);
                Map<String, Object> guard = new LinkedHashMap<>();
                guard.put("enforcement_enabled",  Boolean.parseBoolean(guardProps.getProperty("backendguard.enforcement_enabled", "true")));
                guard.put("log_blocked_attempts",  Boolean.parseBoolean(guardProps.getProperty("backendguard.log_blocked_attempts", "true")));
                guard.put("kick_message",          guardProps.getProperty("backendguard.kick_message", "&cBitte verbinde dich nur ueber den Proxy."));
                guard.put("allowed_proxy_ips",     parseCommaListProperty(guardProps.getProperty("backendguard.allowed_proxy_ips", "127.0.0.1,::1")));
                guard.put("allowed_proxy_cidrs",   parseCommaListProperty(guardProps.getProperty("backendguard.allowed_proxy_cidrs", "")));
                payload.put("backend_guard", guard);
                sendHttpResponse(out, buildJsonString(payload), 200);
                return;
            }

            // POST /forum/notify
            if ("POST".equalsIgnoreCase(method) && path.equalsIgnoreCase("/forum/notify")) {
                String body = readBody(in, headers);
                String apiKeyHeader = headers.getOrDefault("x-api-key", headers.getOrDefault("x-apikey", ""));
                Object mod = moduleManager.getModule("ForumBridgeModule");
                if (mod != null) {
                    try {
                        java.lang.reflect.Method m = mod.getClass().getMethod("handleNotify", String.class, String.class);
                        String resp = (String) m.invoke(mod, body, apiKeyHeader);
                        sendHttpResponse(out, resp, 200);
                    } catch (Exception e) {
                        e.printStackTrace();
                        sendHttpResponse(out, "{\"success\":false,\"error\":\"internal\"}", 500);
                    }
                } else {
                    sendHttpResponse(out, "{\"success\":false,\"error\":\"no_forum_module\"}", 500);
                }
                return;
            }

            // POST /network/attack
            if ("POST".equalsIgnoreCase(method) && path.equalsIgnoreCase("/network/attack")) {
                String body = readBody(in, headers);
                String apiKeyHeader = headers.getOrDefault("x-api-key", headers.getOrDefault("x-apikey", ""));
                NetworkInfoModule mod = (NetworkInfoModule) moduleManager.getModule("NetworkInfoModule");
                if (mod == null || !mod.isEnabled() || !mod.isAttackNotificationsEnabled()) {
                    sendHttpResponse(out, "{\"success\":false,\"error\":\"network_module_disabled\"}", 403);
                    return;
                }
                if (!mod.isAttackApiKeyValid(apiKeyHeader)) {
                    sendHttpResponse(out, "{\"success\":false,\"error\":\"bad_api_key\"}", 403);
                    return;
                }
                String eventType = extractJsonString(body, "event");
                if (eventType == null || eventType.trim().isEmpty()) eventType = "detected";
                String source = extractJsonString(body, "source");

                Integer cps = null, blockedIps = null;
                Long blockedConnections = null;
                String cpsStr = extractJsonString(body, "connectionsPerSecond");
                if (cpsStr == null || cpsStr.isEmpty()) cpsStr = extractJsonString(body, "cps");
                try { if (cpsStr != null && !cpsStr.isEmpty()) cps = Integer.valueOf(cpsStr.trim()); } catch (Exception ignored) {}
                String blockedIpsStr = extractJsonString(body, "ipAddressesBlocked");
                if (blockedIpsStr == null || blockedIpsStr.isEmpty()) blockedIpsStr = extractJsonString(body, "blockedIps");
                try { if (blockedIpsStr != null && !blockedIpsStr.isEmpty()) blockedIps = Integer.valueOf(blockedIpsStr.trim()); } catch (Exception ignored) {}
                String blockedConnectionsStr = extractJsonString(body, "connectionsBlocked");
                if (blockedConnectionsStr == null || blockedConnectionsStr.isEmpty()) blockedConnectionsStr = extractJsonString(body, "blockedConnections");
                try { if (blockedConnectionsStr != null && !blockedConnectionsStr.isEmpty()) blockedConnections = Long.valueOf(blockedConnectionsStr.trim()); } catch (Exception ignored) {}

                boolean sent = mod.sendAttackNotification(eventType, cps, blockedIps, blockedConnections, source);
                sendHttpResponse(out, sent ? "{\"success\":true}" : "{\"success\":false,\"error\":\"webhook_disabled_or_missing\"}", sent ? 200 : 400);
                return;
            }

            // POST /broadcast/cancel
            if ("POST".equalsIgnoreCase(method) && (path.equalsIgnoreCase("/broadcast/cancel") || path.equalsIgnoreCase("/cancel"))) {
                String body = readBody(in, headers);
                String clientScheduleId = extractJsonString(body, "clientScheduleId");
                if (clientScheduleId == null || clientScheduleId.isEmpty()) {
                    sendHttpResponse(out, "{\"success\":false,\"error\":\"missing_clientScheduleId\"}", 400);
                    return;
                }
                Object mod = moduleManager.getModule("BroadcastModule");
                if (mod instanceof BroadcastModule) {
                    boolean ok = ((BroadcastModule) mod).cancelScheduled(clientScheduleId);
                    sendHttpResponse(out, ok ? "{\"success\":true}" : "{\"success\":false,\"error\":\"not_found\"}", ok ? 200 : 404);
                } else {
                    sendHttpResponse(out, "{\"success\":false,\"error\":\"no_broadcast_module\"}", 500);
                }
                return;
            }

            // POST /broadcast
            if ("POST".equalsIgnoreCase(method) && ("/broadcast".equalsIgnoreCase(path) || "/".equals(path) || path.isEmpty())) {
                String body = readBody(in, headers);
                String apiKeyHeader = headers.getOrDefault("x-api-key", headers.getOrDefault("x-apikey", ""));
                String message        = extractJsonString(body, "message");
                String type           = extractJsonString(body, "type");
                String prefix         = extractJsonString(body, "prefix");
                String prefixColor    = extractJsonString(body, "prefixColor");
                String bracketColor   = extractJsonString(body, "bracketColor");
                String messageColor   = extractJsonString(body, "messageColor");
                String sourceName     = extractJsonString(body, "source");
                String scheduleTimeStr= extractJsonString(body, "scheduleTime");
                String recur          = extractJsonString(body, "recur");
                String clientScheduleId = extractJsonString(body, "clientScheduleId");

                if (sourceName == null || sourceName.isEmpty()) sourceName = "PulseCast";
                if (type == null || type.isEmpty()) type = "global";

                Object mod = moduleManager.getModule("BroadcastModule");
                if (!(mod instanceof BroadcastModule)) {
                    sendHttpResponse(out, "{\"success\":false,\"error\":\"no_broadcast_module\"}", 500);
                    return;
                }
                BroadcastModule bm = (BroadcastModule) mod;

                if (scheduleTimeStr != null && !scheduleTimeStr.trim().isEmpty()) {
                    long scheduleMillis;
                    try {
                        scheduleMillis = Long.parseLong(scheduleTimeStr.trim());
                        if (scheduleMillis < 1_000_000_000_000L) scheduleMillis *= 1000L;
                    } catch (NumberFormatException ignored) {
                        try {
                            long v = (long) Double.parseDouble(scheduleTimeStr.trim());
                            scheduleMillis = v < 1_000_000_000_000L ? v * 1000L : v;
                        } catch (Exception ex) {
                            sendHttpResponse(out, "{\"success\":false,\"error\":\"bad_scheduleTime\"}", 400);
                            return;
                        }
                    }
                    boolean ok = bm.scheduleBroadcast(scheduleMillis, sourceName, message, type, apiKeyHeader,
                            prefix, prefixColor, bracketColor, messageColor,
                            recur == null ? "none" : recur, clientScheduleId);
                    sendHttpResponse(out, ok ? "{\"success\":true}" : "{\"success\":false,\"error\":\"rejected\"}", ok ? 200 : 403);
                    return;
                }

                if (message == null || message.isEmpty()) {
                    sendHttpResponse(out, "{\"success\":false,\"error\":\"missing_message\"}", 400);
                    return;
                }
                boolean ok = bm.handleBroadcast(sourceName, message, type, apiKeyHeader, prefix, prefixColor, bracketColor, messageColor);
                sendHttpResponse(out, ok ? "{\"success\":true}" : "{\"success\":false,\"error\":\"rejected\"}", ok ? 200 : 403);
                return;
            }

            // GET /stats/player?uuid=...  oder  ?name=...
            if ("GET".equalsIgnoreCase(method) && "/stats/player".equalsIgnoreCase(pathOnly)) {
                Map<String, String> qp = parseQueryParams(path);
                StatsModule statsMod = (StatsModule) moduleManager.getModule("StatsModule");
                PlayerStats ps = resolvePlayer(qp.get("uuid"), qp.get("name"), statsMod);
                if (ps == null) {
                    sendHttpResponse(out, "{\"success\":false,\"error\":\"player_not_found\"}", 404);
                    return;
                }
                Map<String, Object> payload = new LinkedHashMap<>();
                payload.put("success", true);
                Map<String, Object> playerMap = new LinkedHashMap<>();
                playerMap.put("uuid",        ps.uuid.toString());
                playerMap.put("name",        ps.name);
                playerMap.put("first_seen",  ps.firstSeen);
                playerMap.put("last_seen",   ps.lastSeen);
                playerMap.put("playtime",    ps.getPlaytimeWithCurrentSession());
                playerMap.put("joins",       ps.joins);
                playerMap.put("kills",       ps.kills);
                playerMap.put("deaths",      ps.deaths);
                playerMap.put("online",      ProxyServer.getInstance().getPlayer(ps.uuid) != null);
                // Balance direkt aus MySQL (server\u00fcbergreifend)
                double playerBalance = playerBalances.getOrDefault(ps.uuid, 0.0);
                Map<String, Object> economy = new LinkedHashMap<>();
                economy.put("balance",            playerBalance);
                economy.put("total_earned",       ps.totalEarned);
                economy.put("total_spent",        ps.totalSpent);
                economy.put("transactions_count", ps.transactionsCount);
                playerMap.put("economy", economy);
                Map<String, Object> punishments = new LinkedHashMap<>();
                punishments.put("bans",                 ps.bansCount);
                punishments.put("mutes",                ps.mutesCount);
                punishments.put("warns",                ps.warnsCount);
                punishments.put("last_punishment_at",   ps.lastPunishmentAt);
                punishments.put("last_punishment_type", ps.lastPunishmentType != null ? ps.lastPunishmentType : "");
                punishments.put("punishment_score",     ps.punishmentScore);
                playerMap.put("punishments", punishments);
                payload.put("player", playerMap);
                sendHttpResponse(out, buildJsonString(payload), 200);
                return;
            }

            // GET /economy/player?uuid=...  oder  ?name=...
            // Kein Cache – UUID und Balance kommen direkt aus der DB
            if ("GET".equalsIgnoreCase(method) && "/economy/player".equalsIgnoreCase(pathOnly)) {
                Map<String, String> qp = parseQueryParams(path);
                // UUID aufl\u00f6sen aus Query-Param
                UUID ecoUuid = null;
                String ecoName = null;
                String uuidParam = qp.get("uuid");
                String nameParam = qp.get("name");
                if (uuidParam != null && !uuidParam.isEmpty()) {
                    try { ecoUuid = UUID.fromString(uuidParam.trim()); } catch (IllegalArgumentException ignored) {}
                }
                if (ecoUuid == null) {
                    sendHttpResponse(out, "{\"success\":false,\"error\":\"player_not_found\"}", 404);
                    return;
                }
                if (ecoName == null) ecoName = uuidParam;
                // Balance aus playerBalances Map lesen (bef\u00fcllt von StatusAPIBridge via NexEco)
                double directBalance = playerBalances.getOrDefault(ecoUuid, 0.0);
                Map<String, Object> payload = new LinkedHashMap<>();
                payload.put("success", true);
                payload.put("uuid",    ecoUuid.toString());
                payload.put("name",    ecoName);
                Map<String, Object> economy = new LinkedHashMap<>();
                economy.put("balance", directBalance);
                payload.put("economy", economy);
                sendHttpResponse(out, buildJsonString(payload), 200);
                return;
            }

            // POST /economy/update
            // Empf\u00e4ngt Balance-Updates von StatusAPIBridge (Vault/NexEco → HTTP)
            // Schreibt NUR in playerBalances f\u00fcr Tablist/Scoreboard – KEINE DB-Schreiboperationen
            if ("POST".equalsIgnoreCase(method) && "/economy/update".equalsIgnoreCase(pathOnly)) {
                String body = readBody(in, headers);
                // UUID aufl\u00f6sen
                UUID ecoUpdUuid = null;
                String uuidBody = extractJsonString(body, "uuid");
                if (uuidBody != null && !uuidBody.isEmpty()) {
                    try { ecoUpdUuid = UUID.fromString(uuidBody.trim()); } catch (IllegalArgumentException ignored) {}
                }
                if (ecoUpdUuid == null) {
                    sendHttpResponse(out, "{\"success\":false,\"error\":\"player_not_found\"}", 404);
                    return;
                }
                // Balance NUR in playerBalances Map speichern (f\u00fcr Tablist/Scoreboard)
                // Die echte DB-Verwaltung macht ausschlie\u00dflich NexEco
                String balStr = extractJsonString(body, "balance");
                if (balStr != null && !balStr.isEmpty()) {
                    try {
                        double newBal = Double.parseDouble(balStr);
                        playerBalances.put(ecoUpdUuid, newBal);
                    } catch (NumberFormatException ignored) {}
                }
                // Stats-Felder (total_earned etc.) im Cache aktualisieren, falls vorhanden
                StatsModule statsModEco = (StatsModule) moduleManager.getModule("StatsModule");
                if (statsModEco != null) {
                    PlayerStats psEco = statsModEco.getManager().getIfPresent(ecoUpdUuid);
                    if (psEco != null) {
                        String earnStr  = extractJsonString(body, "total_earned");
                        if (earnStr == null) earnStr = extractJsonString(body, "totalEarned");
                        String spentStr = extractJsonString(body, "total_spent");
                        if (spentStr == null) spentStr = extractJsonString(body, "totalSpent");
                        String txStr    = extractJsonString(body, "transactions_count");
                        if (txStr == null) txStr = extractJsonString(body, "transactionsCount");
                        synchronized (psEco) {
                            try { if (balStr   != null && !balStr.isEmpty())   psEco.balance          = Double.parseDouble(balStr);   } catch (Exception ignored) {}
                            try { if (earnStr  != null && !earnStr.isEmpty())  psEco.totalEarned       = Double.parseDouble(earnStr);  } catch (Exception ignored) {}
                            try { if (spentStr != null && !spentStr.isEmpty()) psEco.totalSpent        = Double.parseDouble(spentStr); } catch (Exception ignored) {}
                            try { if (txStr    != null && !txStr.isEmpty())    psEco.transactionsCount = Integer.parseInt(txStr);      } catch (Exception ignored) {}
                        }
                    }
                }
                sendHttpResponse(out, "{\"success\":true}", 200);
                return;
            }

            // POST /punishment/update
            if ("POST".equalsIgnoreCase(method) && "/punishment/update".equalsIgnoreCase(pathOnly)) {
                String body = readBody(in, headers);
                StatsModule statsModPun = (StatsModule) moduleManager.getModule("StatsModule");
                PlayerStats psPun = resolvePlayer(extractJsonString(body, "uuid"), extractJsonString(body, "name"), statsModPun);
                if (psPun == null) {
                    sendHttpResponse(out, "{\"success\":false,\"error\":\"player_not_found\"}", 404);
                    return;
                }
                String bansStr   = extractJsonString(body, "bans");
                String mutesStr  = extractJsonString(body, "mutes");
                String warnsStr  = extractJsonString(body, "warns");
                String lastAtStr = extractJsonString(body, "last_punishment_at");
                if (lastAtStr == null) lastAtStr = extractJsonString(body, "lastPunishmentAt");
                String typeStr   = extractJsonString(body, "last_punishment_type");
                if (typeStr == null) typeStr = extractJsonString(body, "lastPunishmentType");
                String scoreStr  = extractJsonString(body, "punishment_score");
                if (scoreStr == null) scoreStr = extractJsonString(body, "punishmentScore");
                // Typ auf erlaubte Werte begrenzen
                String safeType = null;
                if (typeStr != null) {
                    String t = typeStr.trim().toLowerCase(Locale.ROOT);
                    safeType = (t.equals("ban") || t.equals("mute") || t.equals("warn") || t.equals("kick")) ? t : "";
                }
                synchronized (psPun) {
                    try { if (bansStr  != null && !bansStr.isEmpty())  psPun.bansCount       = Integer.parseInt(bansStr);        } catch (Exception ignored) {}
                    try { if (mutesStr != null && !mutesStr.isEmpty()) psPun.mutesCount       = Integer.parseInt(mutesStr);       } catch (Exception ignored) {}
                    try { if (warnsStr != null && !warnsStr.isEmpty()) psPun.warnsCount       = Integer.parseInt(warnsStr);       } catch (Exception ignored) {}
                    try { if (lastAtStr != null && !lastAtStr.isEmpty()) psPun.lastPunishmentAt = Long.parseLong(lastAtStr);      } catch (Exception ignored) {}
                    if (safeType != null) psPun.lastPunishmentType = safeType;
                    try { if (scoreStr != null && !scoreStr.isEmpty()) psPun.punishmentScore  = Integer.parseInt(scoreStr);       } catch (Exception ignored) {}
                }
                sendHttpResponse(out, "{\"success\":true}", 200);
                return;
            }

            // POST /stats/update – Kills und Deaths eines Spielers aktualisieren (von Backend-Server via StatusAPIBridge)
            if ("POST".equalsIgnoreCase(method) && "/stats/update".equalsIgnoreCase(pathOnly)) {
                String body = readBody(in, headers);
                StatsModule statsModUpd = (StatsModule) moduleManager.getModule("StatsModule");
                if (statsModUpd == null) {
                    sendHttpResponse(out, "{\"success\":false,\"error\":\"stats_module_unavailable\"}", 503);
                    return;
                }
                String uuidStr = extractJsonString(body, "uuid");
                String nameStr = extractJsonString(body, "name");
                PlayerStats psUpd = resolvePlayer(uuidStr, nameStr, statsModUpd);
                if (psUpd == null) {
                    // Spieler noch nicht bekannt → ignorieren (er hat sich noch nicht eingeloggt)
                    sendHttpResponse(out, "{\"success\":false,\"error\":\"player_not_found\"}", 404);
                    return;
                }
                String killsStr  = extractJsonString(body, "kills");
                String deathsStr = extractJsonString(body, "deaths");
                // Playtime auch updaten
                String playtimeStr = extractJsonString(body, "playtime");
                synchronized (psUpd) {
                    // H\u00d6CHSTER WERT gewinnt – mehrere Unterserver k\u00f6nnen unterschiedliche Werte haben
                    try { if (killsStr  != null && !killsStr.isEmpty()) {
                        int v = Integer.parseInt(killsStr.trim());
                        if (v > psUpd.kills)  psUpd.kills  = v;
                    }} catch (Exception ignored) {}
                    try { if (deathsStr != null && !deathsStr.isEmpty()) {
                        int v = Integer.parseInt(deathsStr.trim());
                        if (v > psUpd.deaths) psUpd.deaths = v;
                    }} catch (Exception ignored) {}
                    try { if (playtimeStr != null && !playtimeStr.isEmpty()) {
                        long v = Long.parseLong(playtimeStr.trim());
                        if (v > psUpd.totalPlaytime) psUpd.totalPlaytime = v;
                    }} catch (Exception ignored) {}
                }
                sendHttpResponse(out, "{\"success\":true}", 200);
                return;
            }

            // POST /scoreboard/health – Leben eines Spielers aktualisieren (von StatusAPIBridge)
            if ("POST".equalsIgnoreCase(method) && "/scoreboard/health".equalsIgnoreCase(pathOnly)) {
                String body = readBody(in, headers);
                String uuidStr   = extractJsonString(body, "uuid");
                String healthStr = extractJsonString(body, "health");
                if (uuidStr != null && !uuidStr.isEmpty() && healthStr != null && !healthStr.isEmpty()) {
                    try {
                        UUID hUuid = UUID.fromString(uuidStr.trim());
                        double health = Double.parseDouble(healthStr.trim());
                        net.viper.status.modules.scoreboard.ScoreboardModule.playerHealth.put(hUuid, health);
                    } catch (Exception ignored) {}
                }
                sendHttpResponse(out, "{\"success\":true}", 200);
                return;
            }

            // POST /scoreboard/compass – Himmelsrichtung eines Spielers (von StatusAPIBridge)
            if ("POST".equalsIgnoreCase(method) && "/scoreboard/compass".equalsIgnoreCase(pathOnly)) {
                String body = readBody(in, headers);
                String uuidStr    = extractJsonString(body, "uuid");
                String compassStr = extractJsonString(body, "compass");
                if (uuidStr != null && !uuidStr.isEmpty() && compassStr != null && !compassStr.isEmpty()) {
                    try {
                        UUID cUuid = UUID.fromString(uuidStr.trim());
                        // Yaw-\u00c4nderung = Mausbewegung → AFK aufheben
                        String oldCompass = net.viper.status.modules.scoreboard.ScoreboardModule.playerCompass.get(cUuid);
                        if (oldCompass != null && !oldCompass.equals(compassStr.trim())
                                && Boolean.TRUE.equals(playerAfk.get(cUuid))) {
                            net.viper.status.modules.afk.AfkModule.unAfkByMovement(cUuid);
                        }
                        net.viper.status.modules.afk.AfkModule.recordActivity(cUuid);
                        net.viper.status.modules.scoreboard.ScoreboardModule.playerCompass.put(cUuid, compassStr.trim());
                    } catch (Exception ignored) {}
                }
                sendHttpResponse(out, "{\"success\":true}", 200);
                return;
            }

            // POST /scoreboard/tps – TPS des Spieler-Servers (von StatusAPIBridge)
            if ("POST".equalsIgnoreCase(method) && "/scoreboard/tps".equalsIgnoreCase(pathOnly)) {
                String body = readBody(in, headers);
                String uuidStr = extractJsonString(body, "uuid");
                String tpsStr  = extractJsonString(body, "tps");
                if (uuidStr != null && !uuidStr.isEmpty() && tpsStr != null && !tpsStr.isEmpty()) {
                    try {
                        UUID tUuid = UUID.fromString(uuidStr.trim());
                        double tps = Double.parseDouble(tpsStr.trim());
                        net.viper.status.modules.scoreboard.ScoreboardModule.playerTps.put(tUuid, tps);
                    } catch (Exception ignored) {}
                }
                sendHttpResponse(out, "{\"success\":true}", 200);
                return;
            }

            // POST /player/world – Welt eines Spielers aktualisieren (von StatusAPIBridge)
            if ("POST".equalsIgnoreCase(method) && "/player/world".equalsIgnoreCase(pathOnly)) {
                String body = readBody(in, headers);
                String uuidStr  = extractJsonString(body, "uuid");
                String worldStr = extractJsonString(body, "world");
                if (uuidStr != null && !uuidStr.isEmpty() && worldStr != null && !worldStr.isEmpty()) {
                    try {
                        playerWorlds.put(UUID.fromString(uuidStr.trim()), worldStr.trim());
                    } catch (IllegalArgumentException ignored) {}
                }
                sendHttpResponse(out, "{\"success\":true}", 200);
                return;
            }

            // POST /player/afk – AFK-Status eines Spielers setzen (von StatusAPIBridge)
            if ("POST".equalsIgnoreCase(method) && "/player/afk".equalsIgnoreCase(pathOnly)) {
                String body    = readBody(in, headers);
                String uuidStr = extractJsonString(body, "uuid");
                String afkStr  = extractJsonString(body, "afk");
                if (uuidStr != null && !uuidStr.isEmpty() && afkStr != null) {
                    try {
                        UUID uid = UUID.fromString(uuidStr.trim());
                        boolean isAfk = Boolean.parseBoolean(afkStr.trim());
                        net.viper.status.modules.afk.AfkModule.setBridgeAfk(uid, isAfk);
                    } catch (IllegalArgumentException ignored) {}
                }
                sendHttpResponse(out, "{\"success\":true}", 200);
                return;
            }

            // POST /ticket/update – TicketSystem Daten (von StatusAPIBridge)
            if ("POST".equalsIgnoreCase(method) && "/ticket/update".equalsIgnoreCase(pathOnly)) {
                String body    = readBody(in, headers);
                String uuidStr = extractJsonString(body, "uuid");
                if (uuidStr != null && !uuidStr.isEmpty()) {
                    try {
                        UUID uid = UUID.fromString(uuidStr.trim());
                        String myOpen = extractJsonString(body, "my_open");
                        if (myOpen != null)
                            net.viper.status.modules.scoreboard.ScoreboardModule.ticketMyOpen.put(uid, Integer.parseInt(myOpen));
                    } catch (Exception ignored) {}
                }
                try {
                    String totOpen    = extractJsonString(body, "total_open");
                    String totClaimed = extractJsonString(body, "total_claimed");
                    String ratGood    = extractJsonString(body, "rating_good");
                    String ratBad     = extractJsonString(body, "rating_bad");
                    if (totOpen    != null) net.viper.status.modules.scoreboard.ScoreboardModule.ticketTotalOpen.set(Integer.parseInt(totOpen));
                    if (totClaimed != null) net.viper.status.modules.scoreboard.ScoreboardModule.ticketTotalClaimed.set(Integer.parseInt(totClaimed));
                    if (ratGood    != null) net.viper.status.modules.scoreboard.ScoreboardModule.ticketRatingGood.set(Integer.parseInt(ratGood));
                    if (ratBad     != null) net.viper.status.modules.scoreboard.ScoreboardModule.ticketRatingBad.set(Integer.parseInt(ratBad));
                } catch (Exception ignored) {}
                sendHttpResponse(out, "{\"success\":true}", 200);
                return;
            }

            // POST /player/data – Koordinaten, Gamemode, Exp, Food, Speed
            if ("POST".equalsIgnoreCase(method) && "/player/data".equalsIgnoreCase(pathOnly)) {
                String body    = readBody(in, headers);
                String uuidStr = extractJsonString(body, "uuid");
                if (uuidStr != null && !uuidStr.isEmpty()) {
                    try {
                        UUID uid = UUID.fromString(uuidStr.trim());
                        String xS  = extractJsonString(body, "x");
                        String yS  = extractJsonString(body, "y");
                        String zS  = extractJsonString(body, "z");
                        String gm  = extractJsonString(body, "gamemode");
                        String expS= extractJsonString(body, "exp");
                        String fdS = extractJsonString(body, "food");
                        String spS = extractJsonString(body, "speed");
                        String wld = extractJsonString(body, "world");
                        // Bewegungserkennung: Koordinaten mit letzten Werten vergleichen
                        if (xS != null && yS != null && zS != null) {
                            int newX = (int) Double.parseDouble(xS);
                            int newY = (int) Double.parseDouble(yS);
                            int newZ = (int) Double.parseDouble(zS);
                            Integer oldX = net.viper.status.modules.scoreboard.ScoreboardModule.playerX.get(uid);
                            Integer oldY = net.viper.status.modules.scoreboard.ScoreboardModule.playerY.get(uid);
                            Integer oldZ = net.viper.status.modules.scoreboard.ScoreboardModule.playerZ.get(uid);
                            if (oldX != null && (newX != oldX || newY != oldY || newZ != oldZ)) {
                                net.viper.status.modules.afk.AfkModule.recordActivity(uid);
                                // AFK aufheben wenn Spieler sich bewegt hat
                                if (Boolean.TRUE.equals(playerAfk.get(uid))) {
                                    ProxiedPlayer mover = ProxyServer.getInstance().getPlayer(uid);
                                    if (mover != null) net.viper.status.modules.afk.AfkModule.unAfkByMovement(uid);
                                }
                            }
                        }
                        if (xS  != null) net.viper.status.modules.scoreboard.ScoreboardModule.playerX.put(uid, (int)Double.parseDouble(xS));
                        if (yS  != null) net.viper.status.modules.scoreboard.ScoreboardModule.playerY.put(uid, (int)Double.parseDouble(yS));
                        if (zS  != null) net.viper.status.modules.scoreboard.ScoreboardModule.playerZ.put(uid, (int)Double.parseDouble(zS));
                        if (gm  != null) net.viper.status.modules.scoreboard.ScoreboardModule.playerGamemode.put(uid, gm);
                        if (expS!= null) net.viper.status.modules.scoreboard.ScoreboardModule.playerExp.put(uid, (int)Double.parseDouble(expS));
                        if (fdS != null) net.viper.status.modules.scoreboard.ScoreboardModule.playerFood.put(uid, (int)Double.parseDouble(fdS));
                        if (spS != null) net.viper.status.modules.scoreboard.ScoreboardModule.playerSpeed.put(uid, Double.parseDouble(spS));
                        if (wld != null) net.viper.status.modules.scoreboard.ScoreboardModule.playerWorld.put(uid, wld);
                    } catch (Exception ignored) {}
                }
                sendHttpResponse(out, "{\"success\":true}", 200);
                return;
            }

            // GET /papi/tokens – liefert alle erkannten %token%-Placeholder als JSON-Array
            if ("GET".equalsIgnoreCase(method) && "/papi/tokens".equalsIgnoreCase(pathOnly)) {
                sendHttpResponse(out, papiTokensJson, 200);
                return;
            }

            // POST /player/papi – empf\u00e4ngt von StatusAPIBridge aufgel\u00f6ste PAPI-Werte
            if ("POST".equalsIgnoreCase(method) && "/player/papi".equalsIgnoreCase(pathOnly)) {
                String body = readBody(in, headers);
                String uuidStr = extractJsonString(body, "uuid");
                if (uuidStr != null && !uuidStr.isEmpty()) {
                    try {
                        UUID papiUuid = UUID.fromString(uuidStr.trim());
                        Map<String, String> map = playerPapi.computeIfAbsent(papiUuid, k -> new ConcurrentHashMap<>());
                        // "placeholders"-Objekt manuell parsen
                        int start = body.indexOf("\"placeholders\"");
                        if (start >= 0) {
                            int brace = body.indexOf('{', start + 14);
                            if (brace >= 0) {
                                int i = brace + 1;
                                while (i < body.length()) {
                                    while (i < body.length() && Character.isWhitespace(body.charAt(i))) i++;
                                    if (i >= body.length() || body.charAt(i) == '}') break;
                                    if (body.charAt(i) != '"') { i++; continue; }
                                    i++;
                                    StringBuilder key = new StringBuilder();
                                    while (i < body.length() && body.charAt(i) != '"') {
                                        char ch = body.charAt(i++);
                                        if (ch == '\\' && i < body.length()) i++; else key.append(ch);
                                    }
                                    i++;
                                    while (i < body.length() && (body.charAt(i) == ':' || Character.isWhitespace(body.charAt(i)))) i++;
                                    if (i < body.length() && body.charAt(i) == '"') {
                                        i++;
                                        StringBuilder val = new StringBuilder();
                                        boolean esc = false;
                                        while (i < body.length()) {
                                            char ch = body.charAt(i++);
                                            if (esc) { val.append(ch == 'n' ? '\n' : ch == 't' ? '\t' : ch); esc = false; }
                                            else if (ch == '\\') esc = true;
                                            else if (ch == '"') break;
                                            else val.append(ch);
                                        }
                                        if (key.length() > 0) map.put(key.toString(), val.toString());
                                    }
                                    while (i < body.length() && (body.charAt(i) == ',' || Character.isWhitespace(body.charAt(i)))) i++;
                                }
                            }
                        }
                    } catch (Exception ignored) {}
                }
                sendHttpResponse(out, "{\"success\":true}", 200);
                return;
            }

            // GET – Status-Endpunkt
            if (inputLine.startsWith("GET")) {
                Map<String, Object> data = new LinkedHashMap<>();
                data.put("online", true);

                String versionRaw = ProxyServer.getInstance().getVersion();
                String versionClean = (versionRaw != null && versionRaw.contains(":")) ? versionRaw.split(":")[2].trim() : versionRaw;
                data.put("version", versionClean);

                int globalLimit = ProxyServer.getInstance().getConfig().getPlayerLimit();
                if (globalLimit <= 0) {
                    try {
                        Iterator<ListenerInfo> limIt = ProxyServer.getInstance().getConfig().getListeners().iterator();
                        if (limIt.hasNext()) {
                            int listenerMax = limIt.next().getMaxPlayers();
                            if (listenerMax > 0) globalLimit = listenerMax;
                        }
                    } catch (Exception ignored) {}
                }
                data.put("max_players", String.valueOf(globalLimit));

                String motd = "BungeeCord";
                try {
                    Iterator<ListenerInfo> it = ProxyServer.getInstance().getConfig().getListeners().iterator();
                    if (it.hasNext()) motd = it.next().getMotd();
                } catch (Exception ignored) {}
                data.put("motd", motd);

                StatsModule statsModule = (StatsModule) moduleManager.getModule("StatsModule");
                boolean luckPermsEnabled = ProxyServer.getInstance().getPluginManager().getPlugin("LuckPerms") != null;
                List<Map<String, Object>> playersList = new ArrayList<>();

                for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
                    Map<String, Object> playerInfo = new LinkedHashMap<>();
                    playerInfo.put("name", p.getName());
                    try { playerInfo.put("uuid", p.getUniqueId().toString()); } catch (Exception ignored) {}

                    boolean isBedrock = false;
                    String bedrockId = null;
                    try {
                        Class<?> floodgateApi = Class.forName("org.geysermc.floodgate.api.FloodgateApi");
                        Object api = floodgateApi.getMethod("getInstance").invoke(null);
                        isBedrock = (boolean) api.getClass().getMethod("isBedrockPlayer", java.util.UUID.class).invoke(api, p.getUniqueId());
                        if (isBedrock) {
                            bedrockId = (String) api.getClass().getMethod("getBedrockId", java.util.UUID.class).invoke(api, p.getUniqueId());
                        }
                    } catch (Exception ignored) {}
                    // Fallback: Floodgate-UUIDs haben MSB == 0 (00000000-0000-0000-xxxx-xxxxxxxxxxxx)
                    if (!isBedrock) {
                        isBedrock = p.getUniqueId().getMostSignificantBits() == 0L;
                    }
                    playerInfo.put("isBedrock", isBedrock);
                    if (bedrockId != null) playerInfo.put("bedrockId", bedrockId);

                    String prefix = "";
                    if (luckPermsEnabled) {
                        try {
                            Class<?> providerClass = Class.forName("net.luckperms.api.LuckPermsProvider");
                            Object luckPermsApi = providerClass.getMethod("get").invoke(null);
                            Object userManager = luckPermsApi.getClass().getMethod("getUserManager").invoke(luckPermsApi);
                            Object user = userManager.getClass().getMethod("getUser", java.util.UUID.class).invoke(userManager, p.getUniqueId());
                            if (user != null) {
                                Class<?> queryOptionsClass = Class.forName("net.luckperms.api.query.QueryOptions");
                                Object queryOptions = queryOptionsClass.getMethod("defaultContextualOptions").invoke(null);
                                Object cachedData = user.getClass().getMethod("getCachedData").invoke(user);
                                Object metaData = cachedData.getClass().getMethod("getMetaData", queryOptionsClass).invoke(cachedData, queryOptions);
                                Object result = metaData.getClass().getMethod("getPrefix").invoke(metaData);
                                if (result != null) prefix = (String) result;
                            }
                        } catch (Exception ignored) {}
                    }
                    playerInfo.put("prefix", prefix);

                    // Aktueller Sub-Server des Spielers (z.B. "Lobby", "Survival")
                    try {
                        if (p.getServer() != null && p.getServer().getInfo() != null) {
                            playerInfo.put("server", p.getServer().getInfo().getName());
                        }
                    } catch (Exception ignored) {}

                    if (statsModule != null) {
                        PlayerStats ps = statsModule.getManager().getIfPresent(p.getUniqueId());
                        if (ps != null) {
                            playerInfo.put("playtime",   ps.getPlaytimeWithCurrentSession());
                            playerInfo.put("joins",      ps.joins);
                            playerInfo.put("kills",      ps.kills);
                            playerInfo.put("deaths",     ps.deaths);
                            playerInfo.put("first_seen", ps.firstSeen);
                            playerInfo.put("last_seen",  ps.lastSeen);
                            // Balance direkt aus MySQL (server\u00fcbergreifend)
                            double statusBalance = playerBalances.getOrDefault(p.getUniqueId(), 0.0);
                            Map<String, Object> eco = new LinkedHashMap<>();
                            eco.put("balance",            statusBalance);
                            eco.put("total_earned",       ps.totalEarned);
                            eco.put("total_spent",        ps.totalSpent);
                            eco.put("transactions_count", ps.transactionsCount);
                            playerInfo.put("economy", eco);
                            Map<String, Object> pun = new LinkedHashMap<>();
                            pun.put("bans",                 ps.bansCount);
                            pun.put("mutes",                ps.mutesCount);
                            pun.put("warns",                ps.warnsCount);
                            pun.put("last_punishment_at",   ps.lastPunishmentAt);
                            pun.put("last_punishment_type", ps.lastPunishmentType != null ? ps.lastPunishmentType : "");
                            pun.put("punishment_score",     ps.punishmentScore);
                            playerInfo.put("punishments", pun);
                        }
                    }
                    playersList.add(playerInfo);
                }
                data.put("players", playersList);

                NetworkInfoModule networkInfoModule = (NetworkInfoModule) moduleManager.getModule("NetworkInfoModule");
                if (networkInfoModule != null && networkInfoModule.isEnabled()) {
                    data.put("network", networkInfoModule.buildSnapshot());
                }

                AntiBotModule antiBotModule = (AntiBotModule) moduleManager.getModule("AntiBotModule");
                if (antiBotModule != null && antiBotModule.isEnabled()) {
                    data.put("antibot", antiBotModule.buildSnapshot());
                }

                String json = buildJsonString(data);
                byte[] jsonBytes = json.getBytes(StandardCharsets.UTF_8);
                StringBuilder response = new StringBuilder();
                response.append("HTTP/1.1 200 OK\r\n");
                response.append("Content-Type: application/json; charset=UTF-8\r\n");
                response.append("Access-Control-Allow-Origin: *\r\n");
                response.append("Content-Length: ").append(jsonBytes.length).append("\r\n");
                response.append("Connection: close\r\n\r\n");
                out.write(response.toString().getBytes(StandardCharsets.UTF_8));
                out.write(jsonBytes);
                out.flush();
            }

        } catch (Exception e) {
            getLogger().severe("Fehler beim Verarbeiten der Anfrage: " + e.getMessage());
        }
    }

    // --- Hilfsmethoden ---

    /**
     * Liest den HTTP-Body basierend auf Content-Length.
     * Max. 1 MB – schützt vor überdimensionierten Requests (DoS via Content-Length).
     */
    private static final int MAX_BODY_SIZE = 1024 * 1024; // 1 MB
    private String readBody(BufferedReader in, Map<String, String> headers) throws IOException {
        int contentLength = 0;
        if (headers.containsKey("content-length")) {
            try { contentLength = Integer.parseInt(headers.get("content-length")); } catch (NumberFormatException ignored) {}
        }
        if (contentLength <= 0) return "";
        if (contentLength > MAX_BODY_SIZE) {
            getLogger().warning("[StatusAPI] Request abgelehnt: Content-Length " + contentLength + " > " + MAX_BODY_SIZE + " Bytes.");
            return "";
        }
        char[] bodyChars = new char[contentLength];
        int read = 0;
        while (read < contentLength) {
            int r = in.read(bodyChars, read, contentLength - read);
            if (r == -1) break;
            read += r;
        }
        return new String(bodyChars);
    }

    private String extractJsonString(String json, String key) {
        if (json == null || key == null) return null;
        String search = "\"" + key + "\"";
        int idx = json.indexOf(search);
        if (idx < 0) return null;
        int colon = json.indexOf(':', idx + search.length());
        if (colon < 0) return null;
        int i = colon + 1;
        while (i < json.length() && Character.isWhitespace(json.charAt(i))) i++;
        if (i >= json.length()) return null;
        char c = json.charAt(i);
        if (c == '"') {
            i++;
            StringBuilder sb = new StringBuilder();
            boolean escape = false;
            while (i < json.length()) {
                char ch = json.charAt(i++);
                if (escape) {
                    switch (ch) {
                        case 'n':  sb.append('\n'); break;
                        case 'r':  sb.append('\r'); break;
                        case 't':  sb.append('\t'); break;
                        case '"':  sb.append('"');   break;
                        case '\\': sb.append('\\'); break;
                        case '/':  sb.append('/');   break;
                        case 'b':  sb.append('\b'); break;
                        case 'f':  sb.append('\f'); break;
                        case 'u':
                            if (i + 4 <= json.length()) {
                                try {
                                    int cp = Integer.parseInt(json.substring(i, i + 4), 16);
                                    sb.append((char) cp);
                                    i += 4;
                                } catch (NumberFormatException ignored) {
                                    sb.append("\\u");
                                }
                            }
                            break;
                        default: sb.append(ch); break;
                    }
                    escape = false;
                } else {
                    if (ch == '\\') escape = true;
                    else if (ch == '"') break;
                    else sb.append(ch);
                }
            }
            return sb.toString();
        } else {
            StringBuilder sb = new StringBuilder();
            while (i < json.length()) {
                char ch = json.charAt(i);
                if (ch == ',' || ch == '}' || ch == '\n' || ch == '\r') break;
                sb.append(ch);
                i++;
            }
            return sb.toString().trim();
        }
    }

    private Properties loadNetworkGuardProperties() {
        Properties props = new Properties();
        File file = new File(getDataFolder(), "network-guard.properties");
        if (!file.exists()) return props;
        try (FileInputStream fis = new FileInputStream(file)) {
            props.load(new java.io.InputStreamReader(fis, StandardCharsets.UTF_8));
        } catch (Exception e) {
            getLogger().warning("Konnte network-guard.properties nicht laden: " + e.getMessage());
        }
        return props;
    }

    private List<String> parseCommaListProperty(String raw) {
        List<String> out = new ArrayList<>();
        if (raw == null || raw.trim().isEmpty()) return out;
        for (String p : raw.split(",")) {
            String trimmed = p == null ? "" : p.trim();
            if (!trimmed.isEmpty()) out.add(trimmed);
        }
        return out;
    }

    private Map<String, String> parseQueryParams(String path) {
        Map<String, String> params = new HashMap<>();
        int q = path.indexOf('?');
        if (q < 0) return params;
        String query = path.substring(q + 1);
        for (String pair : query.split("&")) {
            if (pair.isEmpty()) continue;
            int eq = pair.indexOf('=');
            if (eq <= 0) continue;
            params.put(pair.substring(0, eq).trim().toLowerCase(Locale.ROOT), pair.substring(eq + 1).trim());
        }
        return params;
    }

    private PlayerStats resolvePlayer(String uuidStr, String name, StatsModule statsModule) {
        if (statsModule == null) return null;
        if (uuidStr != null && !uuidStr.isEmpty()) {
            try {
                return statsModule.getManager().getIfPresent(UUID.fromString(uuidStr.trim()));
            } catch (IllegalArgumentException ignored) {}
        }
        if (name != null && !name.isEmpty()) {
            String lower = name.trim().toLowerCase(Locale.ROOT);
            for (PlayerStats ps : statsModule.getManager().all()) {
                if (ps.name.toLowerCase(Locale.ROOT).equals(lower)) return ps;
            }
        }
        return null;
    }

    private void sendHttpResponse(OutputStream out, String json, int code) throws IOException {
        byte[] jsonBytes = json.getBytes(StandardCharsets.UTF_8);
        String status = code == 200 ? "OK" : (code == 403 ? "Forbidden" : code == 404 ? "Not Found" : code == 400 ? "Bad Request" : "Error");
        StringBuilder response = new StringBuilder();
        response.append("HTTP/1.1 ").append(code).append(" ").append(status).append("\r\n");
        response.append("Content-Type: application/json; charset=UTF-8\r\n");
        response.append("Access-Control-Allow-Origin: *\r\n");
        response.append("Content-Length: ").append(jsonBytes.length).append("\r\n");
        response.append("Connection: close\r\n\r\n");
        out.write(response.toString().getBytes(StandardCharsets.UTF_8));
        out.write(jsonBytes);
        out.flush();
    }

    private String buildJsonString(Map<String, Object> data) {
        StringBuilder sb = new StringBuilder("{");
        boolean first = true;
        for (Map.Entry<String, Object> entry : data.entrySet()) {
            if (!first) sb.append(",");
            first = false;
            sb.append("\"").append(escapeJson(entry.getKey())).append("\":").append(valueToString(entry.getValue()));
        }
        sb.append("}");
        return sb.toString();
    }

    @SuppressWarnings("unchecked")
    private String valueToString(Object value) {
        if (value == null) return "null";
        else if (value instanceof Boolean) return value.toString();
        else if (value instanceof Number) return value.toString();
        else if (value instanceof Map) return buildJsonString((Map<String, Object>) value);
        else if (value instanceof List) {
            StringBuilder sb = new StringBuilder("[");
            List<?> list = (List<?>) value;
            for (int i = 0; i < list.size(); i++) {
                if (i > 0) sb.append(",");
                Object item = list.get(i);
                if (item instanceof Map) sb.append(buildJsonString((Map<String, Object>) item));
                else if (item instanceof Number) sb.append(item.toString());
                else sb.append("\"").append(escapeJson(String.valueOf(item))).append("\"");
            }
            sb.append("]");
            return sb.toString();
        }
        return "\"" + escapeJson(String.valueOf(value)) + "\"";
    }

    private String escapeJson(String s) {
        if (s == null) return "";
        return s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n").replace("\r", "\\r");
    }

    private List<Map<String, Object>> loadAntiBotSecurityEvents(int maxEntries) {
        List<Map<String, Object>> out = new ArrayList<>();
        File logFile = new File(getDataFolder(), "antibot-security.log");
        if (!logFile.exists() || maxEntries <= 0) return out;
        try {
            List<String> lines = Files.readAllLines(logFile.toPath(), StandardCharsets.UTF_8);
            for (int i = lines.size() - 1; i >= 0 && out.size() < maxEntries; i--) {
                String line = lines.get(i);
                if (line == null || line.trim().isEmpty()) continue;
                Map<String, String> parsed = parseSecurityLogLine(line);
                String event = parsed.get("event");
                if (!isAttackSecurityEvent(event)) continue;
                String player = parsed.get("player");
                String uuid   = parsed.get("uuid");
                if (player == null || player.trim().isEmpty() || "-".equals(player) || "unknown".equalsIgnoreCase(player)) continue;
                if (uuid == null || uuid.trim().isEmpty()) uuid = "-";
                Map<String, Object> row = new LinkedHashMap<>();
                row.put("datetime", parsed.getOrDefault("datetime", ""));
                row.put("player",   player);
                row.put("uuid",     uuid);
                row.put("ip",       parsed.getOrDefault("ip", "-"));
                out.add(row);
            }
        } catch (Exception e) {
            getLogger().warning("Konnte antibot-security.log nicht lesen: " + e.getMessage());
        }
        return out;
    }

    private Map<String, String> parseSecurityLogLine(String line) {
        Map<String, String> map = new LinkedHashMap<>();
        if (line == null) return map;
        String[] segments = line.split("\\s*\\|\\s*");
        if (segments.length > 0) map.put("datetime", segments[0].trim());
        for (int i = 1; i < segments.length; i++) {
            String seg = segments[i];
            int idx = seg.indexOf('=');
            if (idx <= 0) continue;
            map.put(seg.substring(0, idx).trim().toLowerCase(Locale.ROOT), seg.substring(idx + 1).trim());
        }
        return map;
    }

    private boolean isAttackSecurityEvent(String event) {
        if (event == null) return false;
        String e = event.trim().toLowerCase(Locale.ROOT);
        return e.contains("ip_rate") || e.contains("vpn") || e.contains("learning_threshold_block") || e.contains("block");
    }

    // ── PAPI-Token-Erkennung ──────────────────────────────────────────────────

    /** Alle Tokens die StatusAPI selbst aufl\u00f6st – werden nicht an PAPI weitergegeben */
    private static final Set<String> NATIVE_TOKENS = new HashSet<>(Arrays.asList(
        "player", "rank", "money", "server", "compass", "health", "hearts", "ping",
        "online", "maxplayers", "tps", "ram", "time", "playtime", "x", "y", "z",
        "world", "gamemode", "exp", "food", "foodsym", "speed", "uptime", "servers",
        "proxymem", "date", "news", "line", "balance",
        "ticket_my_open", "ticket_open", "ticket_claimed",
        "ticket_rating_good", "ticket_rating_bad", "ticket_rating_pct"
    ));

    /**
     * Scannt alle .properties-Dateien im Plugin-Ordner nach %token%-Mustern,
     * filtert nativ unterst\u00fctzte Tokens heraus und ver\u00f6ffentlicht den Rest
     * als JSON-Array unter GET /papi/tokens f\u00fcr StatusAPIBridge.
     */
    public void scanAndPublishPapiTokens() {
        Set<String> tokens = new LinkedHashSet<>();
        File folder = getDataFolder();
        if (folder.exists()) {
            File[] files = folder.listFiles((dir, name) -> name.endsWith(".properties"));
            if (files != null) {
                for (File f : files) {
                    try (BufferedReader br = new BufferedReader(
                            new InputStreamReader(new FileInputStream(f), StandardCharsets.UTF_8))) {
                        String line;
                        while ((line = br.readLine()) != null) {
                            extractAllTokensFromText(line, tokens);
                        }
                    } catch (IOException ignored) {}
                }
            }
        }
        // Ressourcen-Defaults scannen (falls noch keine Dateien im Ordner)
        for (String resource : new String[]{"scoreboard.properties"}) {
            try (java.io.InputStream is = getResourceAsStream(resource)) {
                if (is == null) continue;
                BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8));
                String line;
                while ((line = br.readLine()) != null) {
                    extractAllTokensFromText(line, tokens);
                }
            } catch (IOException ignored) {}
        }
        tokens.removeAll(NATIVE_TOKENS);
        // JSON-Array bauen
        StringBuilder json = new StringBuilder("[");
        boolean first = true;
        for (String token : tokens) {
            if (!first) json.append(",");
            json.append("\"").append(token.replace("\\", "\\\\").replace("\"", "\\\"")).append("\"");
            first = false;
        }
        json.append("]");
        papiTokensJson = json.toString();
        if (!tokens.isEmpty()) {
            getLogger().info("[StatusAPI] " + tokens.size() + " PAPI-Token(s) erkannt: " + tokens);
        }
    }

    private static void extractAllTokensFromText(String text, Set<String> result) {
        if (text == null || text.startsWith("#") || !text.contains("%")) return;
        int eq = text.indexOf('=');
        String value = eq >= 0 ? text.substring(eq + 1) : text;
        int i = 0;
        while (i < value.length()) {
            int start = value.indexOf('%', i);
            if (start < 0) break;
            int end = value.indexOf('%', start + 1);
            if (end < 0) break;
            String token = value.substring(start + 1, end);
            if (!token.isEmpty() && !token.contains(" ") && token.matches("[a-zA-Z0-9_:]+")) {
                result.add(token);
            }
            i = end + 1;
        }
    }

    // ── Reload ────────────────────────────────────────────────────────────────

    /**
     * L\u00e4dt Scoreboard und Tablist neu (Config + Tasks), ohne den HTTP-Server zu ber\u00fchren.
     * Alle anderen Module (Chat, AntiBot, etc.) bleiben unber\u00fchrt.
     */
    public void reloadModules() {
        getLogger().info("[StatusAPI] Reload von Scoreboard, Tablist und AFK...");

        net.viper.status.module.Module sbMod  = moduleManager.getModule("ScoreboardModule");
        net.viper.status.module.Module tabMod = moduleManager.getModule("TablistModule");
        net.viper.status.module.Module afkMod = moduleManager.getModule("AfkModule");

        if (sbMod  != null) sbMod.onDisable(this);
        if (tabMod != null) tabMod.onDisable(this);
        // AfkModule meldet Listener, Command und Plugin-Channel in onDisable sauber ab
        // und setzt laufende AFK-Zustaende auf den Spigot-Servern zurueck.
        if (afkMod != null) afkMod.onDisable(this);

        // Neue Instanzen erstellen und registrieren
        net.viper.status.modules.scoreboard.ScoreboardModule newSb  = new net.viper.status.modules.scoreboard.ScoreboardModule();
        net.viper.status.modules.tablist.TablistModule        newTab = new net.viper.status.modules.tablist.TablistModule();
        net.viper.status.modules.afk.AfkModule                newAfk = new net.viper.status.modules.afk.AfkModule();

        moduleManager.replaceModule("ScoreboardModule", newSb);
        moduleManager.replaceModule("TablistModule",    newTab);
        moduleManager.replaceModule("AfkModule",        newAfk);

        newAfk.onEnable(this);
        newSb.onEnable(this);
        newTab.onEnable(this);

        // TPS-Fallback neu verbinden
        try {
            net.viper.status.modules.network.NetworkInfoModule nim =
                (net.viper.status.modules.network.NetworkInfoModule) moduleManager.getModule("NetworkInfoModule");
            if (nim != null) newSb.setNetworkInfoModule(nim);
        } catch (Exception ignored) {}

        scanAndPublishPapiTokens();
        getLogger().info("[StatusAPI] Reload abgeschlossen.");
    }

    // ── /statusapi Befehl ─────────────────────────────────────────────────────

    private static class StatusAPICommand extends net.md_5.bungee.api.plugin.Command {

        private final StatusAPI plugin;

        StatusAPICommand(StatusAPI plugin) {
            super("statusapi", "statusapi.admin", "sapi");
            this.plugin = plugin;
        }

        @Override
        public void execute(net.md_5.bungee.api.CommandSender sender, String[] args) {
            if (args.length == 0 || args[0].equalsIgnoreCase("help")) {
                boolean isAdmin = sender.hasPermission("statusapi.admin")
                        || !(sender instanceof net.md_5.bungee.api.connection.ProxiedPlayer);
                send(sender, "&8&m──────────────────────────────────────────");
                send(sender, "&6&lStatusAPI &7| Befehle");
                if (isAdmin) {
                    send(sender, "&e/statusapi reload &7– Scoreboard, Tablist & AFK neu laden");
                } else {
                    send(sender, "&7Keine weiteren Unterbefehle verf\u00fcgbar.");
                }
                send(sender, "&8&m──────────────────────────────────────────");
                return;
            }

            if (!sender.hasPermission("statusapi.admin")) {
                send(sender, "&cKeine Berechtigung.");
                return;
            }

            switch (args[0].toLowerCase()) {
                case "reload":
                    send(sender, "&7Lade &6Scoreboard &7und &6Tablist &7neu...");
                    plugin.reloadModules();
                    send(sender, "&aScoreboard &7und &aTablist &7wurden neu geladen.");
                    send(sender, "&7PAPI-Tokens erkannt: &e" + papiTokensJson);
                    break;

                default:
                    send(sender, "&cUnbekannter Unterbefehl. Nutze &e/statusapi help&c.");
                    break;
            }
        }

        private static void send(net.md_5.bungee.api.CommandSender s, String text) {
            s.sendMessage(new net.md_5.bungee.api.chat.TextComponent(
                net.md_5.bungee.api.ChatColor.translateAlternateColorCodes('&', text)));
        }
    }
}