package net.viper.status.modules.tablist;

import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.config.ServerInfo;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.event.PlayerDisconnectEvent;
import net.md_5.bungee.api.event.PostLoginEvent;
import net.md_5.bungee.api.event.ServerSwitchEvent;
import net.md_5.bungee.api.plugin.Listener;
import net.md_5.bungee.api.plugin.Plugin;
import net.md_5.bungee.api.scheduler.ScheduledTask;
import net.md_5.bungee.event.EventHandler;
import net.md_5.bungee.protocol.packet.PlayerListItem;
import net.md_5.bungee.protocol.packet.PlayerListItem.Item;
import net.md_5.bungee.protocol.packet.PlayerListItemUpdate;
import net.viper.status.module.Module;

import java.io.*;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.*;
import java.awt.Color;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

public class TablistModule implements Module, Listener {

    private static final String CONFIG_FILE    = "tablist.properties";

    // Leerer Skin (grauer Kopf) f\u00fcr Platzhalter-Slots
    private static final net.md_5.bungee.protocol.data.Property[] EMPTY_SKIN = {
        new net.md_5.bungee.protocol.data.Property(
            "textures",
            "ewogICJ0aW1lc3RhbXAiIDogMTY0NDcwNTExNjQ2OCwKICAicHJvZmlsZUlkIiA6ICJmZDQ3Y2I4YjgzNjQ0YmY3YWIyYmUxODZkYjI1ZmMwZCIsCiAgInByb2ZpbGVOYW1lIiA6ICJDVUNGTDEyIiwKICAic2lnbmF0dXJlUmVxdWlyZWQiIDogdHJ1ZSwKICAidGV4dHVyZXMiIDogewogICAgIlNLSU4iIDogewogICAgICAidXJsIiA6ICJodHRwOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlL2ZmOWJiOWU1NjEyNWM4MjI3Yjk0YmJkYTlmNmUwZjg2MjkzMWMyMjkyNTViYThmMTIwNWQxM2M0NGMxYmI1NjEiCiAgICB9CiAgfQp9",
            "D24yzbg+aBETxe5e+acQR8xJwBkhf8+CdkNYi1ufu3NgXk6YK67dIij8o3QtMx/y3rR6xupRq7bKHUGGgkw+joCC/mtG6yDdLbD32s//VAhA+VVDbIQq/CJrJ8oYarerElTjOF08zxQCw8n97cfI10gkoZvdTDouRfTfQYIIo6vvG9kTGyAJv7mIriTvxE/nwP3m6WlwRmtKWOqDhiMRNoWwo9btCp5JTZR9HVFaZdsNQvh6gUmjBqHoKtr/xWOVveEhQ5mc8WZh0dAiiC3Astfr0VIx7HW1+xNu+Z7xvRMgbZ+SbKuRwotW2KHCN+BDymTbiQ3GBljjXDjwFao0sBHQ24DjafWQcuEEWNsDnhDHtmG3tKdvGQbZ1bYhh97EjRYKXG+eZKMrFGG4jr9oCg0JD3JMBc88Z0mJWyKzPF9B+klFocmrFBF/UgkQnzkNShfkpC6RjUfCymrnAFAoV6XBcznbKQzyKKAMeNE3LPFZ3iS2Tygbrqo2Sjmq9zGpjva04RxWHJ1oeKzROQkge0z96AOO7ChTFTXqnNnAjdkfW2TjK7pSIwS0vMGsUgm1C/amzMpZdJuI0FXFEzz1jhFi5cdwHXSQY1gVpa4VTLNQvu1xgcnbOVJaV0Ty+AebI2s6CLt6OcpI3QKY+KPlITuwj5HydMiQvfYldhiHPjc="
        )
    };

    private static final int ROWS = 20;
    private int rows = ROWS, columns = 6, total = 120, tabSizeMax = 0;
    private int configuredTabSize = 0;
    private int configuredColumns = 0;
    private UUID[] fakeUuids;

    private final ConcurrentHashMap<UUID, net.md_5.bungee.protocol.data.Property[]> skinCache = new ConcurrentHashMap<>();
    private final Map<String, String> serverSymbols = new LinkedHashMap<>();

    private String columnHeaderMode = "none";
    private String playerDisplayMode = "server";

    private boolean enabled = true;
    private int updateInterval = 5;
    private String layoutMode = "compact";

    private String headerLine1 = "&8&m" + rep('\u2501', 53);
    private String headerLine2 = "                    &6&lViper Network";
    private String headerLine3 = "&8&m" + rep('\u2501', 53);
    private String footerLine1 = "&8&m" + rep('\u2501', 53);
    private String footerLine2 = " &7Discord: &ediscord.viper-network.de  &8|  &7Shop: &eviper-network.de/shop";
    private String footerLine3 = "&8&m" + rep('\u2501', 53);

    private String compactHeader1 = "&6&lViper Network &8• &2Hallo, &a%player%&7! &6Sch\u00f6n dass du da bist!";
    private String compactHeader2 = "";
    private String compactHeader3 = "";
    private boolean compactHeader1Spacer = false;
    private boolean compactHeader2Spacer = false;
    private boolean compactHeader3Spacer = false;
    private String compactHeader4 = "";
    private boolean compactHeader4Spacer = false;

    private String compactFooter1 = "";
    private String compactFooter2 = "&7Zeit: &f%time%  &8|  &7Spieler: &f%online%  &8|  &7Ping: &f%ping%ms";
    private String compactFooter3 = "&7Kontostand: &a$%balance%  &8|  &7Server: &f%server%  &8|  &7Welt: &f%world%";
    private String compactFooter4 = "";
    private String compactFooter5 = "";
    private String compactFooter6 = "";
    private boolean compactFooter1Spacer = false;
    private boolean compactFooter2Spacer = false;
    private boolean compactFooter3Spacer = false;
    private boolean compactFooter4Spacer = false;
    private boolean compactFooter5Spacer = false;
    private boolean compactFooter6Spacer = false;

    private String colorSrvHeader = "&6&l";
    private boolean showFooterServerList = true;
    private String timeFormat = "HH:mm:ss / h:mm a";
    private String timeZone = "Europe/Berlin";
    private SimpleDateFormat sdf;
    private List<String> serverOrder = new ArrayList<>();
    private Set<String> hiddenServers = new HashSet<>();
    private List<String> rankOrder = new ArrayList<>();

    private static class InfoEntry {
        String label, type, value; boolean enabled;
        InfoEntry(String l, String t, String v, boolean e) { label=l; type=t; value=v; enabled=e; }
    }
    private List<InfoEntry> infoEntries = new ArrayList<>();

    private Plugin plugin;
    private ScheduledTask updateTask;
    private Method sendPacketQueuedMethod;
    private java.lang.reflect.Field tabListHandlerField;

    @Override public String getName() { return "TablistModule"; }

    @Override
    public void onEnable(Plugin plugin) {
        this.plugin = plugin;
        plugin.getLogger().info("[TablistModule] Starte...");
        ensureConfigExists();
        loadConfig();
        plugin.getLogger().info("[TablistModule] Config geladen. Layout=" + layoutMode + " enabled=" + enabled);
        if (!enabled) { plugin.getLogger().info("[TablistModule] Deaktiviert."); return; }
        try {
            initGridSize();
        } catch (Exception e) {
            plugin.getLogger().warning("[TablistModule] initGridSize Fehler: " + e.getMessage());
            int fbSize = configuredTabSize > 0 ? configuredTabSize : 120;
            int maxCols = fbSize / ROWS;
            tabSizeMax = fbSize; rows = ROWS;
            columns = configuredColumns > 0 ? configuredColumns : Math.max(6, maxCols);
            total = ROWS * columns;
        }
        initUuids();
        try {
            Class<?> uc = Class.forName("net.md_5.bungee.UserConnection");
            sendPacketQueuedMethod = uc.getMethod("sendPacketQueued", net.md_5.bungee.protocol.DefinedPacket.class);
            sendPacketQueuedMethod.setAccessible(true);
            // BungeeCords internen tabListHandler-Field finden und nullbar machen
            try {
                tabListHandlerField = uc.getDeclaredField("tabListHandler");
                tabListHandlerField.setAccessible(true);
            } catch (Exception ignored) {}
            plugin.getLogger().info("[TablistModule] sendPacketQueued gefunden.");
        } catch (Exception e) {
            plugin.getLogger().severe("[TablistModule] sendPacketQueued NICHT gefunden: " + e.getMessage());
            return;
        }
        ProxyServer.getInstance().getPluginManager().registerListener(plugin, this);
        updateTask = ProxyServer.getInstance().getScheduler().schedule(plugin, this::updateAll, 2L, Math.max(1, updateInterval), TimeUnit.SECONDS);
        ProxyServer.getInstance().getScheduler().schedule(plugin, () -> {
            plugin.getLogger().info("[TablistModule] Alle BungeeCord-Server: " + new ArrayList<>(ProxyServer.getInstance().getServers().keySet()));
            plugin.getLogger().info("[TablistModule] Tablist-Spalten: " + getServerOrder());
            recalculateGrid();
        }, 3L, TimeUnit.SECONDS);
        plugin.getLogger().info("[TablistModule] Aktiviert. Grid=" + columns + "x" + rows + " layout=" + layoutMode
            + " column_header=" + columnHeaderMode + " symbols=" + serverSymbols.size());
    }

    @Override
    public void onDisable(Plugin plugin) {
        if (updateTask != null) { updateTask.cancel(); updateTask = null; }
        for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
            try { removeFakeSlots(p); p.setTabHeader(new TextComponent(""), new TextComponent("")); }
            catch (Exception ignored) {}
        }
    }

    private void initGridSize() {
        // configuredTabSize aus loadConfig() hat immer Vorrang.
        if (configuredTabSize > 0) {
            tabSizeMax = configuredTabSize;
        } else {
            tabSizeMax = 400; // Sicherer Fallback
        }
        rows = ROWS;

        int serverCount = getServerOrder().size();

        // SIMPEL & SICHER: configuredColumns direkt nehmen, kein komplexes Berechnen
        if (configuredColumns > 0) {
            columns = configuredColumns;
        } else {
            // Kein Wert in config → automatisch: 1 Spalte pro Server, mindestens 6
            columns = Math.max(6, serverCount);
        }

        total = ROWS * columns;
        plugin.getLogger().info("[TablistModule] Grid: " + columns + " Spalten × " + ROWS + " = " + total + " Slots | " + serverCount + " Server | tabSizeMax=" + tabSizeMax);
    }

    private void initUuids() {
        fakeUuids = new UUID[total];
        for (int i = 0; i < total; i++) fakeUuids[i] = new UUID(0xFFFEDEAD00000000L, (long) i);
    }

    // ── Events ─────────────────────────────────────────────────────────────────

    @EventHandler
    public void onLogin(PostLoginEvent e) {
        if (!enabled) return;
        ProxiedPlayer p = e.getPlayer();
        net.md_5.bungee.protocol.data.Property[] skin = fetchSkin(p);
        if (skin != null && skin.length > 0) skinCache.put(p.getUniqueId(), skin);
        disableBungeeTabHandler(p);
        // BungeeCord resettet tabListHandler nach PostLoginEvent intern nochmals.
        // Deshalb: 0.5s, 1s, 2s nacheinander \u00fcberschreiben.
        long[] delays = {500L, 1000L, 2000L};
        for (long delayMs : delays) {
            ProxyServer.getInstance().getScheduler().schedule(plugin, () -> {
                disableBungeeTabHandler(p);
                updateTablist(p);
            }, delayMs, TimeUnit.MILLISECONDS);
        }
        ProxyServer.getInstance().getScheduler().schedule(plugin, this::updateAll, 3L, TimeUnit.SECONDS);
    }

    @EventHandler
    public void onSwitch(ServerSwitchEvent e) {
        if (!enabled) return;
        ProxiedPlayer switched = e.getPlayer();

        net.md_5.bungee.protocol.data.Property[] skin = fetchSkin(switched);
        if (skin != null && skin.length > 0) skinCache.put(switched.getUniqueId(), skin);
        // Sofort deaktivieren
        disableBungeeTabHandler(switched);

        // BungeeCord setzt den tabListHandler nach ServerSwitch intern mehrfach zur\u00fcck.
        // Deshalb: 0.5s, 1s, 2s, 3s nacheinander \u00fcberschreiben + tablist neu senden.
        long[] delays = {500L, 1000L, 2000L, 3000L};
        for (long delayMs : delays) {
            ProxyServer.getInstance().getScheduler().schedule(plugin, () -> {
                disableBungeeTabHandler(switched);
                net.md_5.bungee.protocol.data.Property[] freshSkin = fetchSkin(switched);
                if (freshSkin != null && freshSkin.length > 0) skinCache.put(switched.getUniqueId(), freshSkin);
                for (ProxiedPlayer viewer : ProxyServer.getInstance().getPlayers()) {
                    try { removeFakeSlots(viewer); } catch (Exception ignored) {}
                }
                updateAll();
            }, delayMs, TimeUnit.MILLISECONDS);
        }
    }

    @EventHandler
    public void onDisconnect(PlayerDisconnectEvent e) {
        if (!enabled) return;
        skinCache.remove(e.getPlayer().getUniqueId());
        net.viper.status.StatusAPI.playerAfk.remove(e.getPlayer().getUniqueId());
        ProxyServer.getInstance().getScheduler().schedule(plugin, () -> {
            for (ProxiedPlayer viewer : ProxyServer.getInstance().getPlayers()) {
                try { removeFakeSlots(viewer); } catch (Exception ignored) {}
            }
            ProxyServer.getInstance().getScheduler().schedule(plugin,
                this::updateAll, 1L, TimeUnit.SECONDS);
        }, 1L, TimeUnit.SECONDS);
    }

    // ── Core ───────────────────────────────────────────────────────────────────

    private void updateAll() {
        recalculateGrid();
        for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
            if (!skinCache.containsKey(p.getUniqueId())) {
                net.md_5.bungee.protocol.data.Property[] skin = fetchSkin(p);
                if (skin != null && skin.length > 0) skinCache.put(p.getUniqueId(), skin);
            }
        }
        for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) updateTablist(p);
    }

    private void recalculateGrid() {
        // tabSizeMax synchronisieren
        if (configuredTabSize > 0) tabSizeMax = configuredTabSize;

        int serverCount = getServerOrder().size();
        int newColumns = configuredColumns > 0 ? configuredColumns : Math.max(6, serverCount);
        int newTotal = ROWS * newColumns;

        if (newColumns == columns && newTotal == total && fakeUuids != null && fakeUuids.length == newTotal) return;
        for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
            try { removeFakeSlots(p); } catch (Exception ignored) {}
        }
        rows = ROWS;
        columns = newColumns;
        total = newTotal;
        initUuids();
        plugin.getLogger().info("[TablistModule] Grid: " + columns + "x" + rows + "=" + total + " (" + serverCount + " Server)");
    }

    private void updateTablist(ProxiedPlayer viewer) {
        if (viewer == null || !viewer.isConnected()) return;
        try {
            // DEBUG: zeige aktuelle Grid-Werte im Chat (wird nach erstem Fix entfernt)
            String srv = viewer.getServer() != null ? capitalize(viewer.getServer().getInfo().getName()) : "\u2014";
            String world = net.viper.status.StatusAPI.playerWorlds.getOrDefault(viewer.getUniqueId(), "world");
            String rank = getRank(viewer);
            String time = sdf.format(new Date());
            String balance = getBalance(viewer);
            int online = ProxyServer.getInstance().getOnlineCount();

            String header, footer;
            if ("compact".equalsIgnoreCase(layoutMode)) {
                header = buildCompactHeader(viewer, srv, world, rank, time, balance, online);
                footer = buildCompactFooter(viewer, srv, world, rank, time, balance, online);
            } else {
                header = c(headerLine1) + "\n" + c(headerLine2) + "\n" + c(headerLine3);
                footer = c(footerLine1) + "\n" + c(footerLine2) + "\n" + c(footerLine3);
            }
            // fromLegacyText parst §x§R§R§G§G§B§B Hex-Sequenzen korrekt
            net.md_5.bungee.api.chat.BaseComponent[] hComps = net.md_5.bungee.api.chat.TextComponent.fromLegacyText(header);
            net.md_5.bungee.api.chat.BaseComponent[] fComps = net.md_5.bungee.api.chat.TextComponent.fromLegacyText(footer);
            viewer.setTabHeader(
                new net.md_5.bungee.api.chat.TextComponent(hComps),
                new net.md_5.bungee.api.chat.TextComponent(fComps));
            // Erst Slots senden (ADD_PLAYER), DANN echte Spieler verstecken (UPDATE_LISTED=false).
            // Umgekehrte Reihenfolge f\u00fchrt zu "Ignoring player info update for unknown player"
            // weil der Client UPDATE_LISTED f\u00fcr unbekannte UUIDs ignoriert.
            sendSlots(viewer, buildItems(viewer));
            hideRealPlayers(viewer);
        } catch (Exception ex) {
            plugin.getLogger().warning("[TablistModule] " + viewer.getName() + ": " + ex.getMessage());
        }
    }

    // ── Header / Footer ────────────────────────────────────────────────────────

    private String buildCompactHeader(ProxiedPlayer viewer, String srv, String world, String rank, String time, String balance, int online) {
        StringBuilder sb = new StringBuilder();
        appendLine(sb, compactHeader1, compactHeader1Spacer, viewer, srv, world, rank, time, balance, online);
        appendLine(sb, compactHeader2, compactHeader2Spacer, viewer, srv, world, rank, time, balance, online);
        appendLine(sb, compactHeader3, compactHeader3Spacer, viewer, srv, world, rank, time, balance, online);
        appendLine(sb, compactHeader4, compactHeader4Spacer, viewer, srv, world, rank, time, balance, online);
        return sb.toString();
    }

    private String buildCompactFooter(ProxiedPlayer viewer, String srv, String world, String rank, String time, String balance, int online) {
        StringBuilder sb = new StringBuilder();
        appendLine(sb, compactFooter1, compactFooter1Spacer, viewer, srv, world, rank, time, balance, online);
        List<String> servers = getServerOrder();
        if (showFooterServerList && !servers.isEmpty()) {
            StringBuilder sLine = new StringBuilder();
            for (String sName : servers) {
                ServerInfo si = ProxyServer.getInstance().getServerInfo(sName);
                int cnt = si != null ? si.getPlayers().size() : 0;
                if (sLine.length() > 0) sLine.append("  &8|  ");
                sLine.append(c(colorSrvHeader)).append(capitalize(sName)).append(" &8\u25cf &7").append(cnt);
            }
            if (sb.length() > 0) sb.append("\n");
            sb.append(c(sLine.toString()));
        }
        appendLine(sb, compactFooter2, compactFooter2Spacer, viewer, srv, world, rank, time, balance, online);
        appendLine(sb, compactFooter3, compactFooter3Spacer, viewer, srv, world, rank, time, balance, online);
        appendLine(sb, compactFooter4, compactFooter4Spacer, viewer, srv, world, rank, time, balance, online);
        appendLine(sb, compactFooter5, compactFooter5Spacer, viewer, srv, world, rank, time, balance, online);
        appendLine(sb, compactFooter6, compactFooter6Spacer, viewer, srv, world, rank, time, balance, online);
        return sb.toString();
    }

    private void appendLine(StringBuilder sb, String line, boolean spacer, ProxiedPlayer viewer, String srv, String world, String rank, String time, String balance, int online) {
        boolean empty = line == null || line.trim().isEmpty();
        if (empty && !spacer) return;
        if (sb.length() > 0) sb.append("\n");
        sb.append(empty ? "\u00A0" : c(replacePlaceholders(line, viewer, srv, world, rank, time, balance, online)));
    }

    // ── Items ──────────────────────────────────────────────────────────────────

    private Item[] buildItems(ProxiedPlayer viewer) {
        String[] texts = new String[total];
        net.md_5.bungee.protocol.data.Property[][] skins = new net.md_5.bungee.protocol.data.Property[total][];
        int[] pings = new int[total];
        for (int i = 0; i < total; i++) { texts[i] = " "; skins[i] = EMPTY_SKIN; pings[i] = 0; }

        boolean compact = "compact".equalsIgnoreCase(layoutMode);
        // Ob der Spalten-Header einen Slot belegt:
        // "full" = explizit aktiviert, "none"/"small" = fr\u00fcher deaktiviert.
        // FIX: Im Server-Modus immer den Servernamen in Zeile 0 schreiben,
        // sonst wei\u00df der Spieler nicht welche Spalte welcher Server ist.
        boolean useSlotHeader = !"none".equalsIgnoreCase(columnHeaderMode);

        // Info-Spalte (nur classic)
        if (!compact) {
            int base = 0, row = 0;
            String srv = viewer.getServer() != null ? capitalize(viewer.getServer().getInfo().getName()) : "\u2014";
            String world = net.viper.status.StatusAPI.playerWorlds.getOrDefault(viewer.getUniqueId(), "world");
            String rank = getRank(viewer); String time = sdf.format(new Date());
            String balance = getBalance(viewer); int online = ProxyServer.getInstance().getOnlineCount();
            for (InfoEntry entry : infoEntries) {
                if (!entry.enabled || row + 1 >= rows) continue;
                if (entry.label != null && !entry.label.isEmpty())
                    row = set(texts, base, row, c(replacePlaceholders(entry.label, viewer, srv, world, rank, time, balance, online)));
                String val;
                switch (entry.type) {
                    case "name":    val = "&f" + viewer.getName(); break;
                    case "rank":    val = "&f" + rank; break;
                    case "server":  val = "&f" + srv; break;
                    case "world":   val = "&f" + world; break;
                    case "time":    val = "&f[" + time + "]"; break;
                    case "balance": val = "&f" + balance; break;
                    case "online":  val = "&f" + online; break;
                    default:        val = replacePlaceholders(entry.value, viewer, srv, world, rank, time, balance, online); break;
                }
                row = set(texts, base, row, c(val));
                row = set(texts, base, row, " ");
            }
        }

        if ("custom".equalsIgnoreCase(playerDisplayMode)) {
            // ── Custom-Modus: alle Spieler zusammen, nach Rang sortiert ──────────
            // Minecraft Tab-Grid ist spaltenweise aufgebaut (Spalte 1 = Slots 0-19, Spalte 2 = Slots 20-39)
            // "Links nach rechts" = Zeile 0 \u00fcber alle Spalten, dann Zeile 1 usw.
            // Spieler 0 → Spalte 0 Zeile 0, Spieler 1 → Spalte 1 Zeile 0, Spieler 2 → Spalte 2 Zeile 0
            // Spieler 3 → Spalte 0 Zeile 1, Spieler 4 → Spalte 1 Zeile 1 usw.
            List<ProxiedPlayer> allPlayers = new ArrayList<>(ProxyServer.getInstance().getPlayers());
            allPlayers = sortPlayersByRank(allPlayers);
            int startCol   = compact ? 0 : 1;
            int usedCols   = columns - startCol;
            int maxSlots   = usedCols * rows;
            int playerIdx  = 0;
            // Zeile f\u00fcr Zeile iterieren, innerhalb jeder Zeile alle Spalten
            outer:
            for (int row = 0; row < rows; row++) {
                for (int col = startCol; col < columns; col++) {
                    if (playerIdx >= allPlayers.size()) break outer;
                    ProxiedPlayer p = allPlayers.get(playerIdx++);
                    int base = col * rows;
                    String prefix  = getLuckPermsPrefix(p);
                    boolean isAfk  = Boolean.TRUE.equals(net.viper.status.StatusAPI.playerAfk.get(p.getUniqueId()));
                    String symbol  = getServerSymbol(p);
                    String nameStr = p.getName() + (symbol.isEmpty() ? "" : " " + symbol);
                    if (isAfk) {
                        set(texts, base, row, c("&7[AFK] &r" + nameStr));
                    } else {
                        set(texts, base, row, prefix.isEmpty()
                            ? c("&7" + nameStr)
                            : c(prefix + "&r " + nameStr));
                    }
                    net.md_5.bungee.protocol.data.Property[] skin = skinCache.get(p.getUniqueId());
                    skins[base + row] = (skin != null && skin.length > 0) ? skin : EMPTY_SKIN;
                    pings[base + row] = p.getPing() < 0 ? 1 : p.getPing();
                }
            }
        } else {
            // ── Server-Modus: pro Spalte ein Server (default) ──────────────────
            List<String> servers = getServerOrder();
            int startCol = compact ? 0 : 1;
            for (int col = startCol; col < columns && (col - startCol) < servers.size(); col++) {
                int base = col * rows;
                int row  = 0;
                String sName = servers.get(col - startCol);
                if (useSlotHeader) {
                    row = set(texts, base, row, c(colorSrvHeader + capitalize(sName)));
                }
                ServerInfo si = ProxyServer.getInstance().getServerInfo(sName);
                if (si != null) {
                    for (ProxiedPlayer p : sortPlayersByRank(new ArrayList<>(si.getPlayers()))) {
                        if (row >= rows) break;
                        String prefix  = getLuckPermsPrefix(p);
                        boolean isAfk  = Boolean.TRUE.equals(net.viper.status.StatusAPI.playerAfk.get(p.getUniqueId()));
                        String symbol  = getServerSymbol(p);
                        String nameStr = p.getName() + (symbol.isEmpty() ? "" : " " + symbol);
                        if (isAfk) {
                            set(texts, base, row, c("&7[AFK] &r" + nameStr));
                        } else {
                            set(texts, base, row, prefix.isEmpty()
                                ? c("&7" + nameStr)
                                : c(prefix + "&r " + nameStr));
                        }
                        net.md_5.bungee.protocol.data.Property[] skin = skinCache.get(p.getUniqueId());
                        skins[base + row] = (skin != null && skin.length > 0) ? skin : EMPTY_SKIN;
                        pings[base + row] = p.getPing() < 0 ? 1 : p.getPing();
                        row++;
                    }
                }
            }
        }

        Item[] items = new Item[total];
        for (int i = 0; i < total; i++) {
            Item item = new Item();
            item.setUuid(fakeUuids[i]);
            item.setUsername(fakeName(i));
            item.setProperties(skins[i]);
            item.setGamemode(0);
            item.setPing(pings[i]);
            item.setListed(true);
            String dn = texts[i] == null || texts[i].isEmpty() ? " " : texts[i];
            item.setDisplayName(new net.md_5.bungee.api.chat.TextComponent(
                net.md_5.bungee.api.chat.TextComponent.fromLegacyText(dn)));
            items[i] = item;
        }
        return items;
    }

    // ── Pakete ─────────────────────────────────────────────────────────────────

    @SuppressWarnings("unchecked")
    private void hideRealPlayers(ProxiedPlayer viewer) {
        if (sendPacketQueuedMethod == null) return;
        try {
            // Echte Spieler: nur listed=false setzen (KEIN Remove, sonst geht der Skin verloren!)
            // Der Client kennt sie bereits per ADD_PLAYER von BungeeCord → UPDATE_LISTED reicht.
            Collection<ProxiedPlayer> online = ProxyServer.getInstance().getPlayers();
            if (!online.isEmpty()) {
                PlayerListItemUpdate playerPkt = new PlayerListItemUpdate();
                playerPkt.setActions(EnumSet.of(PlayerListItemUpdate.Action.UPDATE_LISTED));
                Item[] playerItems = new Item[online.size()];
                int i = 0;
                for (ProxiedPlayer p : online) {
                    Item it = new Item(); it.setUuid(p.getUniqueId()); it.setListed(false);
                    playerItems[i++] = it;
                }
                playerPkt.setItems(playerItems);
                sendPacketQueuedMethod.invoke(viewer, playerPkt);
            }

            // Server-UUIDs (BungeeCord schreibt pro Server 2 Eintr\u00e4ge in die Tablist):
            // Diese per PlayerListItemRemove entfernen – das funktioniert auch f\u00fcr
            // UUIDs die der Client noch nicht kennt, ohne Skin-Schaden.
            List<UUID> serverUuids = new ArrayList<>();
            for (String srvName : ProxyServer.getInstance().getServers().keySet()) {
                try {
                    serverUuids.add(UUID.nameUUIDFromBytes(("OfflinePlayer:" + srvName).getBytes(StandardCharsets.UTF_8)));
                    serverUuids.add(UUID.nameUUIDFromBytes(srvName.getBytes(StandardCharsets.UTF_8)));
                } catch (Exception ignored) {}
            }
            if (!serverUuids.isEmpty()) {
                try {
                    Class<?> removeClass = Class.forName("net.md_5.bungee.protocol.packet.PlayerListItemRemove");
                    Object removePkt = removeClass.getDeclaredConstructor().newInstance();
                    removeClass.getMethod("setUuids", UUID[].class).invoke(removePkt,
                        (Object) serverUuids.toArray(new UUID[0]));
                    sendPacketQueuedMethod.invoke(viewer, removePkt);
                } catch (Exception ignored) {
                    // Fallback: UPDATE_LISTED=false (Warnungen im Client-Log sind harmlos)
                    PlayerListItemUpdate srvPkt = new PlayerListItemUpdate();
                    srvPkt.setActions(EnumSet.of(PlayerListItemUpdate.Action.UPDATE_LISTED));
                    Item[] srvItems = new Item[serverUuids.size()];
                    for (int j = 0; j < serverUuids.size(); j++) {
                        Item it = new Item(); it.setUuid(serverUuids.get(j)); it.setListed(false);
                        srvItems[j] = it;
                    }
                    srvPkt.setItems(srvItems);
                    sendPacketQueuedMethod.invoke(viewer, srvPkt);
                }
            }
        } catch (Exception e) { plugin.getLogger().warning("[TablistModule] hideRealPlayers: " + e.getMessage()); }
    }

    @SuppressWarnings("unchecked")
    private void sendSlots(ProxiedPlayer viewer, Item[] items) {
        if (sendPacketQueuedMethod == null) return;
        try {
            // Paket 1: ADD_PLAYER + UPDATE_DISPLAY_NAME + UPDATE_LISTED + UPDATE_LATENCY
            PlayerListItemUpdate pkt = new PlayerListItemUpdate();
            pkt.setActions(EnumSet.of(
                    PlayerListItemUpdate.Action.ADD_PLAYER,
                    PlayerListItemUpdate.Action.UPDATE_DISPLAY_NAME,
                    PlayerListItemUpdate.Action.UPDATE_LISTED,
                    PlayerListItemUpdate.Action.UPDATE_LATENCY));
            pkt.setItems(items);
            sendPacketQueuedMethod.invoke(viewer, pkt);

            // Paket 2: Explizit UPDATE_LISTED=true f\u00fcr alle Fake-Slots nochmal senden.
            // BungeeCord sendet nach ServerSwitch ein eigenes Tab-Paket das einige Slots
            // auf listed=false setzt – dieses zweite Paket \u00fcberschreibt das wieder.
            PlayerListItemUpdate listedPkt = new PlayerListItemUpdate();
            listedPkt.setActions(EnumSet.of(PlayerListItemUpdate.Action.UPDATE_LISTED));
            listedPkt.setItems(items); // items haben alle listed=true gesetzt
            sendPacketQueuedMethod.invoke(viewer, listedPkt);
        } catch (Exception e) { plugin.getLogger().warning("[TablistModule] sendSlots: " + e.getMessage()); }
    }

    private void removeFakeSlots(ProxiedPlayer viewer) {
        if (sendPacketQueuedMethod == null || fakeUuids == null) return;
        try {
            Class<?> cls = Class.forName("net.md_5.bungee.protocol.packet.PlayerListItemRemove");
            Object pkt = cls.getDeclaredConstructor().newInstance();
            cls.getMethod("setUuids", UUID[].class).invoke(pkt, (Object) fakeUuids.clone());
            sendPacketQueuedMethod.invoke(viewer, pkt);
        } catch (Exception e) {
            try {
                PlayerListItem rem = new PlayerListItem();
                rem.setAction(PlayerListItem.Action.REMOVE_PLAYER);
                Item[] items = new Item[total];
                for (int i = 0; i < total; i++) { Item it = new Item(); it.setUuid(fakeUuids[i]); items[i] = it; }
                rem.setItems(items);
                sendPacketQueuedMethod.invoke(viewer, rem);
            } catch (Exception ignored) {}
        }
    }

    // ── Helpers ────────────────────────────────────────────────────────────────

    /**
     * Ersetzt BungeeCords internen tabListHandler durch einen No-Op Proxy.
     * null setzen crasht BungeeCord (NPE in DownstreamBridge).
     * Ein leerer Proxy ignoriert alle Aufrufe lautlos.
     */
    private void disableBungeeTabHandler(ProxiedPlayer player) {
        if (tabListHandlerField == null) return;
        try {
            Class<?> tabListClass = Class.forName("net.md_5.bungee.tab.TabList");
            // IMMER neu setzen – BungeeCord resettet den tabListHandler nach jedem
            // ServerSwitch intern, daher reicht ein einmaliges Setzen nicht aus.
            Object noopProxy = java.lang.reflect.Proxy.newProxyInstance(
                tabListClass.getClassLoader(),
                new Class<?>[]{ tabListClass },
                (proxy, method, args) -> {
                    Class<?> ret = method.getReturnType();
                    if (ret == boolean.class || ret == Boolean.class) return false;
                    if (ret == int.class || ret == Integer.class) return 0;
                    if (ret == long.class || ret == Long.class) return 0L;
                    return null;
                }
            );
            tabListHandlerField.set(player, noopProxy);
        } catch (Exception ignored) {}
    }

    /**
     * ── ULTIMATE: Gibt das konfigurierte Server-Symbol f\u00fcr den Spieler zur\u00fcck.
     * Leer wenn kein Symbol f\u00fcr den aktuellen Server definiert ist.
     */
    private String getServerSymbol(ProxiedPlayer player) {
        if (serverSymbols.isEmpty() || player.getServer() == null) return "";
        String srvKey = player.getServer().getInfo().getName().toLowerCase();
        String raw = serverSymbols.get(srvKey);
        if (raw == null || raw.isEmpty()) return "";
        return c(raw); // Farb-Codes und Hex-Farben aufl\u00f6sen
    }

    private net.md_5.bungee.protocol.data.Property[] fetchSkin(ProxiedPlayer player) {
        try {
            Object pending = player.getPendingConnection();
            net.md_5.bungee.connection.LoginResult profile =
                (net.md_5.bungee.connection.LoginResult) pending.getClass().getMethod("getLoginProfile").invoke(pending);
            if (profile != null && profile.getProperties() != null && profile.getProperties().length > 0)
                return profile.getProperties();
        } catch (Exception ignored) {}
        return new net.md_5.bungee.protocol.data.Property[0];
    }

    private List<String> getServerOrder() {
        List<String> list;
        if (!serverOrder.isEmpty()) {
            list = new ArrayList<>(serverOrder);
            list.removeIf(s -> hiddenServers.contains(s.toLowerCase()));
        } else {
            list = new ArrayList<>();
            final String[] lobbyKey = {null};
            for (String key : ProxyServer.getInstance().getServers().keySet())
                if (key.equalsIgnoreCase("lobby")) { lobbyKey[0] = key; break; }
            if (lobbyKey[0] != null) list.add(lobbyKey[0]);
            ProxyServer.getInstance().getServers().keySet().stream()
                .filter(s -> lobbyKey[0] == null || !s.equalsIgnoreCase(lobbyKey[0]))
                .filter(s -> !hiddenServers.contains(s.toLowerCase()))
                .sorted(String.CASE_INSENSITIVE_ORDER).forEach(list::add);
        }
        return list;
    }

    private List<ProxiedPlayer> sortPlayersByRank(List<ProxiedPlayer> players) {
        if (rankOrder.isEmpty()) return players;
        players.sort((a, b) -> { int ia = getRankIndex(a), ib = getRankIndex(b);
            return ia != ib ? Integer.compare(ia, ib) : a.getName().compareToIgnoreCase(b.getName()); });
        return players;
    }

    private int getRankIndex(ProxiedPlayer player) {
        try {
            Class<?> prov = Class.forName("net.luckperms.api.LuckPermsProvider");
            Object api = prov.getMethod("get").invoke(null);
            Object um = api.getClass().getMethod("getUserManager").invoke(api);
            Object usr = um.getClass().getMethod("getUser", UUID.class).invoke(um, player.getUniqueId());
            if (usr != null) {
                Object pg = usr.getClass().getMethod("getPrimaryGroup").invoke(usr);
                if (pg != null) { String g = pg.toString().toLowerCase();
                    for (int i = 0; i < rankOrder.size(); i++) if (rankOrder.get(i).equalsIgnoreCase(g)) return i; }
            }
        } catch (Exception ignored) {}
        return rankOrder.size();
    }

    private String getRank(ProxiedPlayer player) {
        try {
            Class<?> prov = Class.forName("net.luckperms.api.LuckPermsProvider");
            Object api = prov.getMethod("get").invoke(null);
            Object um = api.getClass().getMethod("getUserManager").invoke(api);
            Object usr = um.getClass().getMethod("getUser", UUID.class).invoke(um, player.getUniqueId());
            if (usr != null) {
                Class<?> qo = Class.forName("net.luckperms.api.query.QueryOptions");
                Object opts = qo.getMethod("defaultContextualOptions").invoke(null);
                Object cache = usr.getClass().getMethod("getCachedData").invoke(usr);
                Object meta = cache.getClass().getMethod("getMetaData", qo).invoke(cache, opts);
                Object pfx = meta.getClass().getMethod("getPrefix").invoke(meta);
                if (pfx != null && !pfx.toString().isEmpty()) return pfx.toString();
                Object pg = usr.getClass().getMethod("getPrimaryGroup").invoke(usr);
                if (pg != null && !pg.toString().isEmpty()) return "[" + pg.toString().toUpperCase() + "]";
            }
        } catch (Exception ignored) {}
        return "NONE";
    }

    private String getLuckPermsPrefix(ProxiedPlayer player) {
        try {
            Class<?> prov = Class.forName("net.luckperms.api.LuckPermsProvider");
            Object api = prov.getMethod("get").invoke(null);
            Object um = api.getClass().getMethod("getUserManager").invoke(api);
            Object usr = um.getClass().getMethod("getUser", UUID.class).invoke(um, player.getUniqueId());
            if (usr != null) {
                Class<?> qo = Class.forName("net.luckperms.api.query.QueryOptions");
                Object opts = qo.getMethod("defaultContextualOptions").invoke(null);
                Object cache = usr.getClass().getMethod("getCachedData").invoke(usr);
                Object meta = cache.getClass().getMethod("getMetaData", qo).invoke(cache, opts);
                Object pfx = meta.getClass().getMethod("getPrefix").invoke(meta);
                // ── HEX-Farben auch im Prefix aufl\u00f6sen ───────────────────────
                if (pfx != null) return c(pfx.toString());
            }
        } catch (Exception ignored) {}
        return "";
    }

    private String getBalance(ProxiedPlayer player) {
        try {
            Map<?,?> balances = (Map<?,?>) net.viper.status.StatusAPI.class.getField("playerBalances").get(null);
            Object val = balances.get(player.getUniqueId());
            if (val != null) return String.format("%,.2f", ((Number) val).doubleValue());
        } catch (Exception ignored) {}
        return "0.00";
    }

    private String replacePlaceholders(String text, ProxiedPlayer viewer, String srv, String world, String rank, String time, String balance, int online) {
        if (text == null) return "";
        String result = resolvePapiPlaceholders(text, viewer.getUniqueId());
        result = result.replace("%player%", viewer.getName()).replace("%rank%", rank)
            .replace("%server%", srv).replace("%world%", world).replace("%time%", time)
            .replace("%balance%", balance).replace("%ping%", String.valueOf(viewer.getPing()))
            .replace("%online%", String.valueOf(online));
        return result;
    }

    private static String resolvePapiPlaceholders(String text, UUID uuid) {
        if (text == null || !text.contains("%")) return text;
        Map<String, String> papiMap = net.viper.status.StatusAPI.playerPapi.get(uuid);
        if (papiMap == null || papiMap.isEmpty()) return text;
        StringBuilder sb = new StringBuilder();
        int i = 0;
        while (i < text.length()) {
            int start = text.indexOf('%', i);
            if (start < 0) { sb.append(text.substring(i)); break; }
            int end = text.indexOf('%', start + 1);
            if (end < 0) { sb.append(text.substring(i)); break; }
            String token = text.substring(start + 1, end);
            if (papiMap.containsKey(token)) {
                sb.append(text, i, start);
                sb.append(papiMap.get(token));
                i = end + 1;
            } else {
                sb.append(text, i, end + 1);
                i = end + 1;
            }
        }
        return sb.toString();
    }

    private int set(String[] arr, int base, int row, String text) {
        if (base + row < total) arr[base + row] = text == null ? " " : text; return row + 1;
    }

    // ── Farb-Aufl\u00f6sung ─────────────────────────────────────────────────────────

    // ══════════════════════════════════════════════════════════════════════════
    // Farb-Parser: Birdflop-kompatibel
    // Unterst\u00fctzte Formate (alle gleichzeitig nutzbar):
    //
    //  &#RRGGBB          → Pro-Zeichen Hex (Birdflop Standard-Output)
    //  {#RRGGBB}         → Bracket-Format
    //  <#RRGGBB>         → MiniMessage Kurzform
    //  <color:#RRGGBB>   → MiniMessage color-Tag
    //  <gradient:#C1:#C2:Text> → Farbverlauf (beliebig viele Farb-Stopps)
    //  <shadow:#C:Text>  → Text in Schattenfarbe
    //  <b> <i> <u> <st> <obf> → Formatierungen
    //  &l &o &n &m &k &r      → Standard-Formatierungen
    // ══════════════════════════════════════════════════════════════════════════

    private static String c(String s) {
        if (s == null) return " ";
        s = parseMiniMessage(s);    // MiniMessage-Tags (<gradient:>, <shadow:>, <#>, <color:>, <b> usw.)
        s = parseHexAmpersand(s);   // &#RRGGBB und {#RRGGBB}
        return net.md_5.bungee.api.ChatColor.translateAlternateColorCodes('&', s);
    }

    private static String stripColors(String s) {
        return s == null ? "" : net.md_5.bungee.api.ChatColor.stripColor(c(s));
    }

    // ── MiniMessage Haupt-Dispatcher ─────────────────────────────────────────

    private static String parseMiniMessage(String text) {
        if (text == null || !text.contains("<")) return text == null ? "" : text;
        // gradient-Tags als erstes, weil sie anderen Text enthalten k\u00f6nnen
        text = parseGradientTags(text);
        // shadow-Tags
        text = parseShadowTags(text);
        // Einfache Tags: <color:#>, <#>, <b>, <i>, <u>, <st>, <obf>, </...>
        text = parseSimpleTags(text);
        return text;
    }

    // ── <gradient:#C1:#C2:...:TEXT> ──────────────────────────────────────────

    private static String parseGradientTags(String text) {
        if (!text.contains("<gradient:")) return text;
        StringBuilder result = new StringBuilder();
        int i = 0;
        while (i < text.length()) {
            int start = text.indexOf("<gradient:", i);
            if (start < 0) { result.append(text, i, text.length()); break; }
            result.append(text, i, start);
            // Schlie\u00dfendes > suchen (mit Tiefenz\u00e4hler f\u00fcr verschachtelte <...>)
            int end = findClosingAngle(text, start + 1);
            if (end < 0) { result.append(text, i, text.length()); break; }
            String inner = text.substring(start + 1, end); // "gradient:#C1:#C2:TEXT"
            result.append(applyGradientTag(inner));
            i = end + 1;
        }
        return result.toString();
    }

    /**
     * Parst "gradient:#C1:#C2:#C3:TEXT" → eingef\u00e4rbten Text.
     * TEXT darf selbst wieder §-Codes oder &-Codes enthalten (z.B. &l f\u00fcr Bold).
     */
    private static String applyGradientTag(String inner) {
        // inner = "gradient:COLOR:COLOR:...:TEXT"
        // Farben beginnen mit # oder mit & gefolgt von einem Hex-Code
        java.util.List<String> colors = new java.util.ArrayList<>();
        // Trenne am ersten Doppelpunkt nach "gradient"
        int firstColon = inner.indexOf(':'); // nach "gradient"
        if (firstColon < 0) return inner;
        String rest = inner.substring(firstColon + 1);

        // Lese Farb-Stopps (jeder Teil beginnt mit #)
        // TEXT ist alles ab dem ersten Teil der NICHT mit # beginnt
        StringBuilder textSb = new StringBuilder();
        boolean inText = false;
        String[] parts = rest.split(":", -1);
        for (int p = 0; p < parts.length; p++) {
            String part = parts[p];
            if (!inText && part.startsWith("#") && part.length() == 7) {
                colors.add(part);
            } else {
                // Ab hier Text (inkl. Doppelpunkte wieder zusammensetzen)
                inText = true;
                if (textSb.length() > 0) textSb.append(":");
                textSb.append(part);
            }
        }
        if (colors.size() < 2) return textSb.toString();

        // Shadow-Tags im Text zuerst aufl\u00f6sen (k\u00f6nnen im Gradient-Text stecken)
        String rawText = parseShadowTags(textSb.toString());
        return applyGradient(rawText, colors);
    }

    private static String applyGradient(String text, java.util.List<String> colorStops) {
        if (text == null || text.isEmpty()) return text;
        // §-Codes und &-Codes aus Text herausfiltern f\u00fcr L\u00e4ngenberechnung
        String plain = text
            .replaceAll("\u00A7[0-9a-fk-orx]", "")
            .replaceAll("&[0-9a-fA-Fk-orK-OR]", "")
            .replaceAll("\u00A7x(\u00A7[0-9a-fA-F]){6}", ""); // §x§R§R§G§G§B§B
        int len = plain.length();
        if (len == 0) return text;
        if (len == 1) return resolveColorToSection(colorStops.get(0)) + text;

        int[][] rgbStops = new int[colorStops.size()][3];
        for (int s = 0; s < colorStops.size(); s++) rgbStops[s] = hexToRgb(colorStops.get(s));

        StringBuilder result = new StringBuilder();
        int charIdx = 0;
        int ci = 0;
        while (ci < text.length()) {
            char ch = text.charAt(ci);

            // §x§R§R§G§G§B§B durchreichen (bereits aufgel\u00f6ste Hex-Farbe z.B. von shadow)
            if (ch == '\u00A7' && ci + 1 < text.length() && text.charAt(ci + 1) == 'x') {
                // Lese die 12 folgenden Zeichen (§x + 6x §digit)
                if (ci + 13 < text.length() + 1) {
                    result.append(text, ci, Math.min(ci + 14, text.length()));
                    ci = Math.min(ci + 14, text.length());
                } else {
                    result.append(ch); ci++;
                }
                continue;
            }
            // §-Formatcode durchreichen
            if (ch == '\u00A7' && ci + 1 < text.length()) {
                result.append(ch).append(text.charAt(ci + 1)); ci += 2; continue;
            }
            // &-Formatcode durchreichen
            if (ch == '&' && ci + 1 < text.length() && "&0123456789abcdefABCDEFklmnorKLMNOR".indexOf(text.charAt(ci+1)) >= 0) {
                result.append(ch).append(text.charAt(ci + 1)); ci += 2; continue;
            }

            // Normales Zeichen → Farbe interpolieren
            float t = len <= 1 ? 0f : (float) charIdx / (len - 1);
            int segments = colorStops.size() - 1;
            float scaled = t * segments;
            int seg = Math.min((int) scaled, segments - 1);
            float segT = scaled - seg;
            int[] c1 = rgbStops[seg], c2 = rgbStops[seg + 1];
            int r = clamp((int)(c1[0] + (c2[0] - c1[0]) * segT));
            int g = clamp((int)(c1[1] + (c2[1] - c1[1]) * segT));
            int b = clamp((int)(c1[2] + (c2[2] - c1[2]) * segT));
            String hex = String.format("%02X%02X%02X", r, g, b);
            appendHexSection(result, hex);
            result.append(ch);
            charIdx++;
            ci++;
        }
        return result.toString();
    }

    // ── <shadow:#RRGGBB:TEXT> ─────────────────────────────────────────────────

    private static String parseShadowTags(String text) {
        if (text == null || !text.contains("<shadow:")) return text == null ? "" : text;
        StringBuilder result = new StringBuilder();
        int i = 0;
        while (i < text.length()) {
            int start = text.indexOf("<shadow:", i);
            if (start < 0) { result.append(text, i, text.length()); break; }
            result.append(text, i, start);
            int end = findClosingAngle(text, start + 1);
            if (end < 0) { result.append(text, i, text.length()); break; }
            String inner = text.substring(start + 1, end); // "shadow:#RRGGBB:TEXT"
            // Format: shadow:COLOR:TEXT
            int firstColon = inner.indexOf(':');
            int secondColon = firstColon >= 0 ? inner.indexOf(':', firstColon + 1) : -1;
            if (firstColon < 0 || secondColon < 0) { result.append(text, i, end + 1); i = end + 1; continue; }
            String colorPart = inner.substring(firstColon + 1, secondColon).trim();
            String content   = inner.substring(secondColon + 1);
            result.append(resolveColorToSection(colorPart)).append(content);
            i = end + 1;
        }
        return result.toString();
    }

    // ── Einfache MiniMessage-Tags ─────────────────────────────────────────────

    private static String parseSimpleTags(String text) {
        if (text == null || !text.contains("<")) return text == null ? "" : text;
        // Ersetzungstabelle
        text = text.replace("<b>", "&l").replace("</b>", "&r");
        text = text.replace("<i>", "&o").replace("</i>", "&r");
        text = text.replace("<u>", "&n").replace("</u>", "&r");
        text = text.replace("<st>", "&m").replace("</st>", "&r");
        text = text.replace("<obf>", "&k").replace("</obf>", "&r");
        text = text.replace("<reset>", "&r").replace("</reset>", "");
        // Closing-Tags entfernen (werden nach Verarbeitung nicht mehr ben\u00f6tigt)
        text = text.replaceAll("</gradient>", "");
        text = text.replaceAll("</shadow>", "");
        text = text.replaceAll("</color>", "");
        // <color:#RRGGBB> und <#RRGGBB>
        StringBuilder result = new StringBuilder();
        int i = 0;
        while (i < text.length()) {
            char ch = text.charAt(i);
            if (ch != '<') { result.append(ch); i++; continue; }
            // <color:#RRGGBB>
            if (text.startsWith("<color:#", i)) {
                int end = text.indexOf('>', i);
                if (end > 0) {
                    String hex = text.substring(i + 7, end).trim();
                    if (hex.startsWith("#") && hex.length() == 7 && hex.substring(1).matches("[0-9a-fA-F]{6}")) {
                        appendHexSection(result, hex.substring(1));
                        i = end + 1; continue;
                    }
                }
            }
            // <#RRGGBB>
            if (text.startsWith("<#", i) && i + 9 <= text.length()) {
                int end = text.indexOf('>', i);
                if (end == i + 8) {
                    String hex = text.substring(i + 2, end);
                    if (hex.matches("[0-9a-fA-F]{6}")) {
                        appendHexSection(result, hex);
                        i = end + 1; continue;
                    }
                }
            }
            result.append(ch); i++;
        }
        return result.toString();
    }

    // ── &#RRGGBB und {#RRGGBB} ───────────────────────────────────────────────

    private static String parseHexAmpersand(String text) {
        if (text == null) return "";
        if (!text.contains("&#") && !text.contains("{#")) return text;
        StringBuilder sb = new StringBuilder();
        int i = 0;
        while (i < text.length()) {
            // &#RRGGBB
            if (i + 7 < text.length() + 1 && i + 8 <= text.length()
                    && text.charAt(i) == '&' && text.charAt(i+1) == '#') {
                String hex = text.substring(i+2, i+8);
                if (hex.matches("[0-9a-fA-F]{6}")) {
                    appendHexSection(sb, hex); i += 8; continue;
                }
            }
            // {#RRGGBB}
            if (i + 8 < text.length() && text.charAt(i) == '{' && text.charAt(i+1) == '#') {
                int end = text.indexOf('}', i+2);
                if (end == i + 8) {
                    String hex = text.substring(i+2, i+8);
                    if (hex.matches("[0-9a-fA-F]{6}")) {
                        appendHexSection(sb, hex); i += 9; continue;
                    }
                }
            }
            sb.append(text.charAt(i++));
        }
        return sb.toString();
    }

    // ── Hilfsmethoden ─────────────────────────────────────────────────────────

    private static void appendHexSection(StringBuilder sb, String hex) {
        sb.append('\u00A7').append('x');
        for (char ch : hex.toCharArray()) sb.append('\u00A7').append(ch);
    }

    private static String resolveColorToSection(String color) {
        if (color == null) return "";
        color = color.trim();
        if (color.startsWith("#") && color.length() == 7
                && color.substring(1).matches("[0-9a-fA-F]{6}")) {
            StringBuilder sb = new StringBuilder();
            appendHexSection(sb, color.substring(1));
            return sb.toString();
        }
        if (color.startsWith("&") && color.length() == 2) return "\u00A7" + color.charAt(1);
        return color;
    }

    private static int[] hexToRgb(String color) {
        String hex = color == null ? "" : color.trim();
        if (hex.startsWith("#")) hex = hex.substring(1);
        if (hex.length() != 6) return new int[]{255, 255, 255};
        try {
            return new int[]{
                Integer.parseInt(hex.substring(0,2), 16),
                Integer.parseInt(hex.substring(2,4), 16),
                Integer.parseInt(hex.substring(4,6), 16)
            };
        } catch (Exception e) { return new int[]{255,255,255}; }
    }

    private static int clamp(int v) { return Math.max(0, Math.min(255, v)); }

    /**
     * Findet das schlie\u00dfende '>' f\u00fcr ein Tag das bei fromIndex beginnt.
     * Ber\u00fccksichtigt verschachtelte <...>.
     */
    private static int findClosingAngle(String text, int fromIndex) {
        int depth = 0;
        for (int i = fromIndex; i < text.length(); i++) {
            char ch = text.charAt(i);
            if (ch == '<') depth++;
            else if (ch == '>') { if (depth == 0) return i; depth--; }
        }
        return -1;
    }


    private static String fakeName(int i)     { return String.format("~vt%03d", i); }
    private static String capitalize(String s){ return s==null||s.isEmpty()?s:Character.toUpperCase(s.charAt(0))+s.substring(1); }
    private static String rep(char ch, int n) { StringBuilder sb=new StringBuilder(n); for(int i=0;i<n;i++) sb.append(ch); return sb.toString(); }
    private int parseInt(String s, int fb)    { try{return Integer.parseInt(s==null?"":s.trim());}catch(Exception e){return fb;} }

    // ── Config ─────────────────────────────────────────────────────────────────

    private void ensureConfigExists() {
        File f = new File(plugin.getDataFolder(), CONFIG_FILE);
        if (f.exists()) return;
        if (!plugin.getDataFolder().exists()) plugin.getDataFolder().mkdirs();
        String sep = rep('\u2501', 53);
        String content =
            "# TablistModule Konfiguration\n" +
            "tablist.enabled=true\n" +
            "tablist.tab_size=160\n" +
            "tablist.update_interval=5\n" +
            "# Layout-Modus: classic oder compact\n" +
            "tablist.layout=compact\n" +
            "tablist.server_order=\n" +
            "tablist.hidden_servers=\n" +
            "tablist.rank_order=owner,mod,primo,vip,scout,bewohner\n\n" +
            "# column_header: full = gro\u00dfer Spalten-Header (alte Markierung)\n" +
            "#                none = kein Header, Zeile 0 ist f\u00fcr Spieler frei (MuckiDEE-Wunsch)\n" +
            "#                small = wie none, aber Server-Namen erscheinen im Tab-Footer\n" +
            "tablist.column_header=none\n" +
            "# player_display: server = Server-basiert (default) | custom = alle zusammen nach Rang sortiert\n" +
            "tablist.player_display=server\n\n" +
            "tablist.header.line1=&8&m" + sep + "\n" +
            "tablist.header.line2=                    &6&lViper Network\n" +
            "tablist.header.line3=&8&m" + sep + "\n\n" +
            "tablist.footer.line1=&8&m" + sep + "\n" +
            "tablist.footer.line2= &7Discord: &ediscord.viper-network.de  &8|  &7Shop: &eviper-network.de/shop\n" +
            "tablist.footer.line3=&8&m" + sep + "\n\n" +
            "# ── Compact Layout ──────────────────────────────────────────────────\n" +
            "# Platzhalter: %player% %rank% %server% %world% %time% %balance% %ping% %online%\n" +
            "# spacer=true: leere Zeile = Abstand | spacer=false: leere Zeile = \u00fcberspringen\n" +
            "tablist.compact.header.line1=&6&lViper Network &8• &2Hallo, &a%player%&7!\n" +
            "tablist.compact.header.line2=&dCitybuild &8• &aSurvival &8• &eMinigames\n" +
            "tablist.compact.header.line2.spacer=false\n" +
            "tablist.compact.header.line3=\n" +
            "tablist.compact.header.line3.spacer=false\n\n" +
            "tablist.compact.footer.line1=\n" +
            "tablist.compact.footer.line1.spacer=false\n" +
            "tablist.compact.footer.line2=&7Zeit: &f%time%  &8|  &7Spieler: &f%online%  &8|  &7Ping: &f%ping%ms\n" +
            "tablist.compact.footer.line2.spacer=false\n" +
            "tablist.compact.footer.line3=&7Kontostand: &a$%balance%  &8|  &7Server: &f%server%  &8|  &7Welt: &f%world%\n" +
            "tablist.compact.footer.line3.spacer=false\n" +
            "tablist.compact.footer.line4=\n" +
            "tablist.compact.footer.line4.spacer=false\n" +
            "tablist.compact.footer.line5=\n" +
            "tablist.compact.footer.line5.spacer=false\n" +
            "tablist.compact.footer.line6=\n" +
            "tablist.compact.footer.line6.spacer=false\n\n" +
            "tablist.color.server_header=&6&l\n" +
            "# Server-Liste im Footer anzeigen (true/false)\n" +
            "tablist.compact.footer.serverlist=true\n" +
            "tablist.time_format=HH:mm:ss / h:mm a\n" +
            "tablist.timezone=Europe/Berlin\n\n" +
            "# ── Info-Spalte (nur classic) ────────────────────────────────────────\n" +
            "tablist.info.order=website,name,rank,server,world,time,teamspeak\n\n" +
            "tablist.info.website.enabled=true\n" +
            "tablist.info.website.label=&b&lWebsite:\n" +
            "tablist.info.website.type=website\n" +
            "tablist.info.website.value=&fviper-network.de\n\n" +
            "tablist.info.name.enabled=true\n" +
            "tablist.info.name.label=&b&lName:\n" +
            "tablist.info.name.type=name\n\n" +
            "tablist.info.rank.enabled=true\n" +
            "tablist.info.rank.label=&b&lRank:\n" +
            "tablist.info.rank.type=rank\n\n" +
            "tablist.info.server.enabled=true\n" +
            "tablist.info.server.label=&b&lServer:\n" +
            "tablist.info.server.type=server\n\n" +
            "tablist.info.world.enabled=true\n" +
            "tablist.info.world.label=&b&lWorld:\n" +
            "tablist.info.world.type=world\n\n" +
            "tablist.info.time.enabled=true\n" +
            "tablist.info.time.label=&b&lTime:\n" +
            "tablist.info.time.type=time\n\n" +
            "tablist.info.teamspeak.enabled=true\n" +
            "tablist.info.teamspeak.label=&b&lTeamspeak:\n" +
            "tablist.info.teamspeak.type=teamspeak\n" +
            "tablist.info.teamspeak.value=&fts.viper-network.de\n" +
            "\n# ── Server-Symbole ───────────────────────────────────────────────────\n" +
            "# Format: tablist.symbol.<servername>=&FarbCode Symbol\n" +
            "# Farben: & + Code (z.B. &6 = Gold) oder &#RRGGBB / {#RRGGBB} / <#RRGGBB>\n" +
            "# Emojis und Unicode-Symbole werden unterst\u00fctzt.\n" +
            "# Der Symbol-Text erscheint hinter dem Spielernamen in der Tablist.\n" +
            "tablist.symbol.lobby=&f\uD83C\uDFE0\n" +
            "tablist.symbol.sv1=&6\u26CF\uFE0F\n" +
            "# tablist.symbol.farmwelt=&a\uD83C\uDF3F\n" +
            "# tablist.symbol.spielerwelt=&e\u2728\n" +
            "# tablist.symbol.game=&d\uD83C\uDFAE\n";
        try (OutputStream out = new FileOutputStream(f)) { out.write(content.getBytes(StandardCharsets.UTF_8)); }
        catch (Exception e) { plugin.getLogger().warning("[TablistModule] Config: " + e.getMessage()); }
    }

    private void loadConfig() {
        File file = new File(plugin.getDataFolder(), CONFIG_FILE);
        Map<String, String> map = new LinkedHashMap<>();
        if (file.exists()) {
            try (BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8))) {
                String line;
                while ((line = br.readLine()) != null) {
                    line = line.trim();
                    if (line.isEmpty() || line.startsWith("#")) continue;
                    int eq = line.indexOf('=');
                    if (eq < 1) continue;
                    map.put(line.substring(0, eq).trim(), line.substring(eq + 1));
                }
            } catch (Exception e) { plugin.getLogger().warning("[TablistModule] Ladefehler: " + e.getMessage()); }
        }
        java.util.function.BiFunction<String,String,String> get = (k,d) -> map.getOrDefault(k,d);

        configuredTabSize = parseInt(get.apply("tablist.tab_size", "0"), 0);
        configuredColumns = parseInt(get.apply("tablist.columns", "0"), 0);
        enabled        = Boolean.parseBoolean(get.apply("tablist.enabled", "true"));
        updateInterval = parseInt(get.apply("tablist.update_interval", "5"), 5);
        layoutMode     = get.apply("tablist.layout", "compact").trim().toLowerCase();
        // ── UPGRADE: column_header Modus ──────────────────────────────────────
        columnHeaderMode    = get.apply("tablist.column_header", "none").trim().toLowerCase();
        playerDisplayMode   = get.apply("tablist.player_display", "server").trim().toLowerCase();

        headerLine1 = get.apply("tablist.header.line1", headerLine1);
        headerLine2 = get.apply("tablist.header.line2", headerLine2);
        headerLine3 = get.apply("tablist.header.line3", headerLine3);
        footerLine1 = get.apply("tablist.footer.line1", footerLine1);
        footerLine2 = get.apply("tablist.footer.line2", footerLine2);
        footerLine3 = get.apply("tablist.footer.line3", footerLine3);
        compactHeader1 = get.apply("tablist.compact.header.line1", compactHeader1);
        compactHeader2 = get.apply("tablist.compact.header.line2", compactHeader2);
        compactHeader3 = get.apply("tablist.compact.header.line3", compactHeader3);
        compactHeader1Spacer = Boolean.parseBoolean(get.apply("tablist.compact.header.line1.spacer", "false"));
        compactHeader2Spacer = Boolean.parseBoolean(get.apply("tablist.compact.header.line2.spacer", "false"));
        compactHeader3Spacer = Boolean.parseBoolean(get.apply("tablist.compact.header.line3.spacer", "false"));
        compactHeader4 = get.apply("tablist.compact.header.line4", compactHeader4);
        compactHeader4Spacer = Boolean.parseBoolean(get.apply("tablist.compact.header.line4.spacer", "false"));
        compactFooter1 = get.apply("tablist.compact.footer.line1", compactFooter1);
        compactFooter2 = get.apply("tablist.compact.footer.line2", compactFooter2);
        compactFooter3 = get.apply("tablist.compact.footer.line3", compactFooter3);
        compactFooter4 = get.apply("tablist.compact.footer.line4", compactFooter4);
        compactFooter5 = get.apply("tablist.compact.footer.line5", compactFooter5);
        compactFooter6 = get.apply("tablist.compact.footer.line6", compactFooter6);
        compactFooter1Spacer = Boolean.parseBoolean(get.apply("tablist.compact.footer.line1.spacer", "false"));
        compactFooter2Spacer = Boolean.parseBoolean(get.apply("tablist.compact.footer.line2.spacer", "false"));
        compactFooter3Spacer = Boolean.parseBoolean(get.apply("tablist.compact.footer.line3.spacer", "false"));
        compactFooter4Spacer = Boolean.parseBoolean(get.apply("tablist.compact.footer.line4.spacer", "false"));
        compactFooter5Spacer = Boolean.parseBoolean(get.apply("tablist.compact.footer.line5.spacer", "false"));
        compactFooter6Spacer = Boolean.parseBoolean(get.apply("tablist.compact.footer.line6.spacer", "false"));
        colorSrvHeader = get.apply("tablist.color.server_header", colorSrvHeader);
        showFooterServerList = Boolean.parseBoolean(get.apply("tablist.compact.footer.serverlist", "true"));
        timeFormat     = get.apply("tablist.time_format", timeFormat);
        timeZone       = get.apply("tablist.timezone", timeZone);
        try { sdf = new SimpleDateFormat(timeFormat); sdf.setTimeZone(java.util.TimeZone.getTimeZone(timeZone)); }
        catch (Exception e) { sdf = new SimpleDateFormat("HH:mm:ss / h:mm a"); }

        rankOrder.clear();
        String rankRaw = get.apply("tablist.rank_order", "").trim();
        if (!rankRaw.isEmpty()) for (String s : rankRaw.split(",")) { String t=s.trim(); if(!t.isEmpty()) rankOrder.add(t.toLowerCase()); }

        serverOrder.clear();
        String raw = get.apply("tablist.server_order", "").trim();
        if (!raw.isEmpty()) for (String s : raw.split(",")) { String t=s.trim(); if(!t.isEmpty()) serverOrder.add(t); }

        hiddenServers.clear();
        String hRaw = get.apply("tablist.hidden_servers", "").trim();
        if (!hRaw.isEmpty()) for (String s : hRaw.split(",")) { String t=s.trim().toLowerCase(); if(!t.isEmpty()) hiddenServers.add(t); }

        infoEntries.clear();
        String orderRaw = get.apply("tablist.info.order", "website,name,rank,server,world,time,teamspeak").trim();
        for (String id : orderRaw.split(",")) {
            id = id.trim(); if (id.isEmpty()) continue;
            boolean en = Boolean.parseBoolean(get.apply("tablist.info." + id + ".enabled", "true"));
            String label = get.apply("tablist.info." + id + ".label", "");
            String type  = get.apply("tablist.info." + id + ".type", "custom");
            String value = get.apply("tablist.info." + id + ".value", "");
            infoEntries.add(new InfoEntry(label, type, value, en));
        }
        if (infoEntries.isEmpty()) {
            infoEntries.add(new InfoEntry("&b&lWebsite:", "website", "&fviper-network.de", true));
            infoEntries.add(new InfoEntry("&b&lName:", "name", "", true));
            infoEntries.add(new InfoEntry("&b&lRank:", "rank", "", true));
            infoEntries.add(new InfoEntry("&b&lServer:", "server", "", true));
            infoEntries.add(new InfoEntry("&b&lWorld:", "world", "", true));
            infoEntries.add(new InfoEntry("&b&lTime:", "time", "", true));
            infoEntries.add(new InfoEntry("&b&lTeamspeak:", "teamspeak", "&fts.viper-network.de", true));
        }

        // ── Server-Symbole aus tablist.properties ─────────────────────────────
        // Format: tablist.symbol.<servername>=&FarbCode Symbol
        // Beispiel: tablist.symbol.lobby=&f🏠
        //           tablist.symbol.sv1=&6⛏️
        serverSymbols.clear();
        for (Map.Entry<String, String> entry : map.entrySet()) {
            String key = entry.getKey();
            if (key.startsWith("tablist.symbol.")) {
                String srvName = key.substring("tablist.symbol.".length()).trim().toLowerCase();
                String symbol  = entry.getValue().trim();
                if (!srvName.isEmpty() && !symbol.isEmpty()) {
                    serverSymbols.put(srvName, symbol);
                    plugin.getLogger().info("[TablistModule] Symbol: " + srvName + " → " + symbol);
                }
            }
        }
    }

}