package net.viper.status.modules.afk;

import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.CommandSender;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.Title;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.event.ChatEvent;
import net.md_5.bungee.api.event.PlayerDisconnectEvent;
import net.md_5.bungee.api.plugin.Command;
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.viper.status.StatusAPI;
import net.viper.status.module.Module;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;

/**
 * AfkModule – /afk Befehl + automatische AFK-Erkennung nach Inaktivit\u00e4t.
 *
 * Unterst\u00fctzt %gradient:FARBE1:FARBE2:...:TEXT% in allen Title-Eintr\u00e4gen,
 * genau wie Scoreboard und Tablist.
 *
 * Config: afk.properties (wird beim ersten Start automatisch erstellt)
 *
 * Title-Format: "Title-Zeile|Subtitle-Zeile"  (Subtitle optional)
 * Beispiel mit Gradient:
 *   afk.title.set.1=%gradient:&b:&f:&b:&l [AFK] %|&8Bewege dich um zur\u00fcckzukehren
 */
public class AfkModule implements Module, Listener {

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

    private Plugin plugin;
    private ScheduledTask idleCheckTask;
    private Command afkCommand;
    private static AfkModule INSTANCE;
    private java.lang.reflect.Method sendPkt;

    public static final ConcurrentHashMap<UUID, Long> lastActivity = new ConcurrentHashMap<>();

    // Laufender Title-Repeat-Task pro AFK-Spieler
    private final ConcurrentHashMap<UUID, ScheduledTask> titleTasks = new ConcurrentHashMap<>();

    // Welcher Title-Eintrag diesem Spieler gerade angezeigt wird (bleibt gleich solange AFK)
    private final ConcurrentHashMap<UUID, String[]> activeTitleEntry = new ConcurrentHashMap<>();

    // Config
    private boolean enabled      = true;
    private boolean idleEnabled  = true;
    private int     idleSeconds  = 300;
    private String  bypassPerm   = "statusapi.afk.bypass";
    private int     titleFadeIn  = 10;
    private int     titleStay    = 60;
    private int     titleFadeOut = 20;

    // Titel-Paar: set-Nachricht + passende unset-Nachricht
    private static class TitlePair {
        final String[] set;   // { title, subtitle }
        final String[] unset; // { title, subtitle }
        TitlePair(String[] set, String[] unset) { this.set = set; this.unset = unset; }
    }

    private final List<TitlePair> titlePairs = new ArrayList<>();

    // Welches Paar diesem Spieler gerade zugewiesen ist (bleibt f\u00fcr die gesamte AFK-Zeit gleich)
    private final ConcurrentHashMap<UUID, TitlePair> activePair = new ConcurrentHashMap<>();

    private final Random random = new Random();

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

    @Override
    public void onEnable(Plugin plugin) {
        this.plugin = plugin;
        INSTANCE = this;
        try {
            Class<?> uc = Class.forName("net.md_5.bungee.UserConnection");
            sendPkt = uc.getMethod("sendPacketQueued", net.md_5.bungee.protocol.DefinedPacket.class);
            sendPkt.setAccessible(true);
        } catch (Exception e) {
            plugin.getLogger().severe("[AfkModule] sendPacketQueued nicht gefunden: " + e.getMessage());
        }
        ensureConfigExists();
        loadConfig();
        if (!enabled) { plugin.getLogger().info("[AfkModule] Deaktiviert."); return; }

        ProxyServer.getInstance().getPluginManager().registerListener(plugin, this);
        // Plugin-Message-Channel registrieren (BungeeCord → Spigot)
        ProxyServer.getInstance().registerChannel("statusapi:afk");
        afkCommand = new Command("afk") {
            @Override
            public void execute(CommandSender sender, String[] args) {
                if (!(sender instanceof ProxiedPlayer)) {
                    sender.sendMessage(plain("&cNur f\u00fcr Spieler."));
                    return;
                }
                toggleAfk((ProxiedPlayer) sender);
            }
        };
        ProxyServer.getInstance().getPluginManager().registerCommand(plugin, afkCommand);

        if (idleEnabled) {
            idleCheckTask = ProxyServer.getInstance().getScheduler().schedule(
                plugin, this::checkIdle, 10L, 10L, TimeUnit.SECONDS);
        }
        plugin.getLogger().info("[AfkModule] Aktiviert. idle=" + idleEnabled
            + " idleSeconds=" + idleSeconds
            + " pairs=" + titlePairs.size());
    }

    @Override
    public void onDisable(Plugin plugin) {
        if (idleCheckTask != null) { idleCheckTask.cancel(); idleCheckTask = null; }
        titleTasks.values().forEach(ScheduledTask::cancel);
        titleTasks.clear();
        activeTitleEntry.clear();
        activePair.clear();

        // Die Spigot-Seite (StatusAPIBridge) fuehrt eine eigene afkState-Map und
        // setzt daraus den [AFK]-Nametag. Wird hier nur lokal geleert, bleibt der
        // Prefix nach einem Reload ueber dem Kopf haengen, weil die Bridge nie ein
        // Update auf false bekommt. Darum vor dem Leeren explizit zuruecksetzen.
        for (UUID id : new ArrayList<>(StatusAPI.playerAfk.keySet())) {
            ProxiedPlayer p = ProxyServer.getInstance().getPlayer(id);
            if (p != null) { try { clearTitle(p); } catch (Exception ignored) {} }
            try { broadcastAfkState(id, false); } catch (Exception ignored) {}
        }
        StatusAPI.playerAfk.clear();
        lastActivity.clear();
        INSTANCE = null;

        // Listener und Command wieder abmelden, damit eine neue Instanz (Reload)
        // sauber registrieren kann und keine Doppel-Registrierung entsteht.
        try { ProxyServer.getInstance().getPluginManager().unregisterListener(this); } catch (Exception ignored) {}
        if (afkCommand != null) {
            try { ProxyServer.getInstance().getPluginManager().unregisterCommand(afkCommand); } catch (Exception ignored) {}
            afkCommand = null;
        }
        try { ProxyServer.getInstance().unregisterChannel("statusapi:afk"); } catch (Exception ignored) {}
    }

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

    @EventHandler
    public void onChat(ChatEvent e) {
        if (!(e.getSender() instanceof ProxiedPlayer)) return;
        ProxiedPlayer p = (ProxiedPlayer) e.getSender();
        recordActivity(p.getUniqueId());
        String msg = e.getMessage();
        if (msg == null) return;
        if (!msg.toLowerCase(Locale.ROOT).startsWith("/afk") && isAfk(p.getUniqueId()))
            setAfk(p, false);
    }

    @EventHandler
    public void onDisconnect(PlayerDisconnectEvent e) {
        UUID id = e.getPlayer().getUniqueId();
        StatusAPI.playerAfk.remove(id);
        lastActivity.remove(id);
        stopTitleTask(id);
        activePair.remove(id);
    }

    // ── Public API ───────────────────────────────────────────────────────────

    /**
     * Setzt den AFK-Status aus einer externen Quelle (POST /player/afk der Bridge).
     *
     * Laeuft ueber denselben Weg wie /afk und die Idle-Erkennung, sonst waere der
     * Status zwar gesetzt, aber ohne Title und ohne Broadcast an die Spigot-Server –
     * der [AFK]-Nametag wuerde dann nicht mitziehen.
     */
    public static void setBridgeAfk(UUID uuid, boolean afk) {
        AfkModule inst = INSTANCE;
        ProxiedPlayer p = ProxyServer.getInstance().getPlayer(uuid);
        if (inst != null && p != null) {
            inst.setAfk(p, afk);
            return;
        }
        // Spieler nicht auf dem Proxy erreichbar: nur den Status nachfuehren.
        if (afk) {
            StatusAPI.playerAfk.put(uuid, true);
        } else {
            StatusAPI.playerAfk.remove(uuid);
            lastActivity.put(uuid, System.currentTimeMillis());
        }
    }

    /**
     * Wird von StatusAPI aufgerufen wenn die Bridge eine Koordinaten\u00e4nderung meldet.
     * Hebt AFK sofort auf und zeigt den Unset-Title an.
     */
    public static void unAfkByMovement(UUID uuid) {
        AfkModule inst = INSTANCE;
        if (inst == null) return;
        if (!Boolean.TRUE.equals(StatusAPI.playerAfk.get(uuid))) return;
        ProxiedPlayer p = ProxyServer.getInstance().getPlayer(uuid);
        if (p == null) return;
        inst.setAfk(p, false);
    }

    public static void recordActivity(UUID uuid) {
        lastActivity.put(uuid, System.currentTimeMillis());
    }

    // ── Interne Logik ────────────────────────────────────────────────────────

    private boolean isAfk(UUID uuid) {
        return Boolean.TRUE.equals(StatusAPI.playerAfk.get(uuid));
    }

    private void toggleAfk(ProxiedPlayer p) {
        setAfk(p, !isAfk(p.getUniqueId()));
    }

    private void setAfk(ProxiedPlayer p, boolean afk) {
        UUID id = p.getUniqueId();
        // Zustandswechsel atomar: setAfk wird sowohl vom Scheduler (checkIdle) als
        // auch von den HTTP-Threads der Bridge (Bewegung, Compass) aufgerufen. Ein
        // getrenntes isAfk()-Vorabpruefen liess beide Threads durchlaufen – Folge
        // waren doppelte Title-Tasks und doppelte Broadcasts.
        if (afk) {
            if (StatusAPI.playerAfk.putIfAbsent(id, Boolean.TRUE) != null) return;
            startTitleTask(p);
        } else {
            if (StatusAPI.playerAfk.remove(id) == null) return;
            lastActivity.put(id, System.currentTimeMillis());
            TitlePair pair = activePair.remove(id);
            stopTitleTask(id);
            clearTitle(p);
            if (pair != null) sendTitleEntry(p, pair.unset);
        }
        // AFK-Status an alle Spigot-Server broadcasten damit die Bridge
        // den Nametag-Prefix sofort aktualisieren kann.
        broadcastAfkState(id, afk);
    }

    /**
     * Schickt eine Plugin-Message (Channel "statusapi:afk") an alle Spigot-Server,
     * auf denen der betroffene Spieler ODER irgendein anderer Spieler eingeloggt ist.
     * Format: "UUID:true" / "UUID:false"
     */
    private void broadcastAfkState(UUID uuid, boolean afk) {
        byte[] payload = (uuid.toString() + ":" + afk).getBytes(java.nio.charset.StandardCharsets.UTF_8);
        Set<net.md_5.bungee.api.config.ServerInfo> reached = new java.util.HashSet<>();
        // Zuerst: Server des betroffenen Spielers direkt ansprechen
        ProxiedPlayer self = ProxyServer.getInstance().getPlayer(uuid);
        if (self != null && self.isConnected() && self.getServer() != null) {
            net.md_5.bungee.api.config.ServerInfo srv = self.getServer().getInfo();
            if (srv != null) { srv.sendData("statusapi:afk", payload); reached.add(srv); }
        }
        // Dann: alle anderen Online-Spieler – deren Server brauchen das Update
        // damit der Prefix über dem AFK-Spieler-Kopf auch bei Anderen stimmt.
        for (ProxiedPlayer other : ProxyServer.getInstance().getPlayers()) {
            if (!other.isConnected() || other.getServer() == null) continue;
            net.md_5.bungee.api.config.ServerInfo srv = other.getServer().getInfo();
            if (srv != null && !reached.contains(srv)) {
                srv.sendData("statusapi:afk", payload);
                reached.add(srv);
            }
        }
    }

    private void checkIdle() {
        long now = System.currentTimeMillis();
        long thresholdMs = idleSeconds * 1000L;
        for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
            try {
                if (!p.isConnected()) continue;
                UUID id = p.getUniqueId();
                if (p.hasPermission(bypassPerm) || isAfk(id)) continue;
                Long last = lastActivity.get(id);
                if (last == null) { lastActivity.put(id, now); continue; }
                if (now - last >= thresholdMs) setAfk(p, true);
            } catch (Exception ex) {
                plugin.getLogger().log(Level.FINE, "[AfkModule] Fehler in checkIdle fuer Spieler.", ex);
            }
        }
    }

    /**
     * W\u00e4hlt einen zuf\u00e4lligen AFK-Title und wiederholt ihn dauerhaft,
     * solange der Spieler AFK ist – kein Ausblenden m\u00f6glich.
     */
    private void startTitleTask(ProxiedPlayer p) {
        if (titlePairs.isEmpty()) return;
        UUID id = p.getUniqueId();
        stopTitleTask(id);

        // Zuf\u00e4lliges Paar w\u00e4hlen – bleibt f\u00fcr die gesamte AFK-Zeit gleich
        TitlePair pair = titlePairs.get(random.nextInt(titlePairs.size()));
        activePair.put(id, pair);
        activeTitleEntry.put(id, pair.set);
        if (!sendTitleEntry(p, pair.set, true)) {  // firstSend=true → TitleTimes mitsenden
            stopTitleTask(id);
            return;
        }

        // Kurz vor Ablauf der Standzeit erneut senden, damit der Title nie ausblendet.
        // Fruehere Rechnung (titleStay - 20) * 50 lief bei kleinem afk.title.stay ins
        // Leere: unter 30 Ticks war das Intervall laenger als die Standzeit selbst und
        // der Title verschwand zwischen zwei Ticks.
        long stayMs = Math.max(1L, titleStay) * 50L;
        long intervalMs = Math.max(500L, stayMs - 1000L);
        ScheduledTask task = ProxyServer.getInstance().getScheduler().schedule(plugin, () -> {
            try {
                ProxiedPlayer online = ProxyServer.getInstance().getPlayer(id);
                if (online == null || !online.isConnected() || !isAfk(id)) {
                    stopTitleTask(id);
                    return;
                }
                String[] current = activeTitleEntry.get(id);
                if (current == null) return;
                // firstSend=false → kein TitleTimes → kein Blinken beim Repeat
                if (!sendTitleEntry(online, current, false)) {
                    stopTitleTask(id);
                    return;
                }
                // Der Spieler kann sich waehrend des Sendens bewegt haben – setAfk(false)
                // laeuft auf dem HTTP-Thread der Bridge. Dann klebt jetzt wieder der
                // AFK-Title auf dem Bildschirm, obwohl der Spieler zurueck ist.
                if (!isAfk(id)) {
                    clearTitle(online);
                    sendTitleEntry(online, pair.unset);
                    stopTitleTask(id);
                }
            } catch (Exception ex) {
                plugin.getLogger().log(Level.FINE, "[AfkModule] Fehler im Title-Repeat-Task.", ex);
                stopTitleTask(id);
            }
        }, intervalMs, intervalMs, TimeUnit.MILLISECONDS);
        titleTasks.put(id, task);
    }

    private void stopTitleTask(UUID id) {
        ScheduledTask old = titleTasks.remove(id);
        if (old != null) old.cancel();
        activeTitleEntry.remove(id);
        // BUGFIX: activePair hier NICHT entfernen wenn der Spieler noch AFK ist –
        // setAfk(false) liest activePair noch aus (für den Unset-Title) und entfernt es selbst.
        // Wenn der Spieler aber NICHT mehr AFK ist (z.B. Exception im Task), muss activePair
        // hier bereinigt werden, sonst Memory Leak.
        if (!Boolean.TRUE.equals(StatusAPI.playerAfk.get(id))) {
            activePair.remove(id);
        }
    }

    /** Entfernt den Title sofort vom Bildschirm via ClearTitles-Packet. */
    private void clearTitle(ProxiedPlayer p) {
        try {
            net.md_5.bungee.protocol.packet.ClearTitles clear = new net.md_5.bungee.protocol.packet.ClearTitles();
            if (sendPkt != null) sendPkt.invoke(p, clear);
        } catch (Exception ignored) {}
    }

    /**
     * Sendet Title + Subtitle als raw Packets (wie ScoreboardModule) –
     * dadurch werden Hex-Farben korrekt \u00fcbertragen, ohne durch TextComponent.fromArray() zu laufen.
     */
    /**
     * Sendet Title + Subtitle an den Spieler.
     * @param firstSend true = TitleTimes-Packet mitsenden (erster Aufruf).
     *                  false = nur Title/Subtitle refreshen, KEIN TitleTimes →
     *                  verhindert das Blinken (fade-in/-out) bei jedem Repeat-Tick.
     */
    private boolean sendTitleEntry(ProxiedPlayer p, String[] entry, boolean firstSend) {
        if (p == null || !p.isConnected() || entry == null || entry.length == 0) return false;

        String titleRaw = ChatColor.translateAlternateColorCodes('&', applyGradients(
            entry[0] == null ? "" : entry[0]
        ));
        String subtitleRaw = entry.length > 1
            ? ChatColor.translateAlternateColorCodes('&', applyGradients(entry[1] == null ? "" : entry[1]))
            : "";

        try {
            if (sendPkt == null) throw new IllegalStateException("sendPkt not initialized");

            // TitleTimes NUR beim ersten Senden schicken.
            // Wird das Packet bei jedem Repeat-Tick gesendet, startet der Client
            // jedes Mal eine neue fade-in-Animation → sichtbares Blinken.
            if (firstSend) {
                net.md_5.bungee.protocol.packet.TitleTimes times = new net.md_5.bungee.protocol.packet.TitleTimes();
                times.setFadeIn(titleFadeIn);
                times.setStay(titleStay);
                times.setFadeOut(titleFadeOut);
                sendPkt.invoke(p, times);
            }

            // Title-Packet
            net.md_5.bungee.protocol.packet.Title titlePkt = new net.md_5.bungee.protocol.packet.Title();
            titlePkt.setAction(net.md_5.bungee.protocol.packet.Title.Action.TITLE);
            titlePkt.setText(mergeComponents(buildComponents(titleRaw)));
            sendPkt.invoke(p, titlePkt);

            // Subtitle-Packet
            net.md_5.bungee.protocol.packet.Subtitle subPkt = new net.md_5.bungee.protocol.packet.Subtitle();
            subPkt.setText(mergeComponents(buildComponents(subtitleRaw)));
            sendPkt.invoke(p, subPkt);

            return true;

        } catch (Exception e) {
            // Fallback auf Title-API
            try {
                Title title = ProxyServer.getInstance().createTitle();
                title.title(buildComponents(titleRaw));
                title.subTitle(buildComponents(subtitleRaw));
                if (firstSend) {
                    title.fadeIn(titleFadeIn);
                    title.stay(titleStay);
                    title.fadeOut(titleFadeOut);
                } else {
                    // Kein Fade → bleibt stabil stehen ohne Blink-Effekt
                    title.fadeIn(0);
                    title.stay(titleStay);
                    title.fadeOut(0);
                }
                p.sendTitle(title);
                return true;
            } catch (Exception ignored) {
                return false;
            }
        }
    }

    /** Overload für Aufrufe ohne firstSend-Flag (z.B. unset-Title beim Zurückkommen). */
    private boolean sendTitleEntry(ProxiedPlayer p, String[] entry) {
        return sendTitleEntry(p, entry, true);
    }

    /** Fasst BaseComponent[] in eine TextComponent zusammen (f\u00fcr Title/Subtitle setText). */
    private static net.md_5.bungee.api.chat.BaseComponent mergeComponents(BaseComponent[] parts) {
        TextComponent root = new TextComponent("");
        for (BaseComponent bc : parts) root.addExtra(bc);
        return root;
    }

    // ── Gradient-Verarbeitung ─────────────────────────────────────────────────
    // Identische Implementierung wie im ScoreboardModule – verarbeitet
    // %gradient:FARBE1:FARBE2:...:TEXT%  mit beliebig vielen Farb-Stopps.
    // Farben: #RRGGBB, &#RRGGBB oder &b, &f, &c usw.
    // Formatcodes (&l, &o, &n, &m) im Text bleiben erhalten.

    private String applyGradients(String input) {
        if (input == null || !input.contains("%gradient:")) return input;
        StringBuilder result = new StringBuilder();
        int i = 0;
        while (i < input.length()) {
            int start = input.indexOf("%gradient:", i);
            if (start < 0) { result.append(input.substring(i)); break; }
            result.append(input, i, start);
            int end = input.indexOf("%", start + 10);
            if (end < 0) { result.append(input.substring(start)); break; }
            String inner = input.substring(start + 10, end);
            List<int[]> stops = new ArrayList<>();
            int colonIdx = 0;
            while (colonIdx < inner.length()) {
                int nextColon = inner.indexOf(':', colonIdx);
                if (nextColon < 0) break;
                String candidate = inner.substring(colonIdx, nextColon);
                int[] rgb = parseGradientColor(candidate);
                if (rgb != null) { stops.add(rgb); colonIdx = nextColon + 1; }
                else break;
            }
            if (stops.size() < 2) { result.append(input, start, end + 1); i = end + 1; continue; }
            String text  = inner.substring(colonIdx);
            String plain = ChatColor.stripColor(ChatColor.translateAlternateColorCodes('&', text));
            boolean bold      = text.contains("&l") || text.contains("\u00A7l");
            boolean italic    = text.contains("&o") || text.contains("\u00A7o");
            boolean underline = text.contains("&n") || text.contains("\u00A7n");
            boolean strike    = text.contains("&m") || text.contains("\u00A7m");
            String fmt = (bold ? "\u00A7l" : "") + (italic ? "\u00A7o" : "")
                       + (underline ? "\u00A7n" : "") + (strike ? "\u00A7m" : "");
            int visLen = 0;
            for (char ch : plain.toCharArray()) if (ch != ' ') visLen++;
            if (visLen == 0) visLen = 1;
            int charIdx = 0;
            int[] lastRgb = stops.get(0); // Farbe f\u00fcr f\u00fchrende Leerzeichen
            for (char ch : plain.toCharArray()) {
                if (ch == ' ') {
                    // Leerzeichen mit der letzten aktiven Gradient-Farbe einf\u00e4rben
                    result.append('\u00A7').append('x');
                    result.append('\u00A7').append(String.format("%02X", lastRgb[0]).charAt(0));
                    result.append('\u00A7').append(String.format("%02X", lastRgb[0]).charAt(1));
                    result.append('\u00A7').append(String.format("%02X", lastRgb[1]).charAt(0));
                    result.append('\u00A7').append(String.format("%02X", lastRgb[1]).charAt(1));
                    result.append('\u00A7').append(String.format("%02X", lastRgb[2]).charAt(0));
                    result.append('\u00A7').append(String.format("%02X", lastRgb[2]).charAt(1));
                    result.append(fmt).append(ch);
                    continue;
                }
                float pos = visLen <= 1 ? 0f : (float) charIdx / (visLen - 1);
                lastRgb = interpolateGradient(stops, pos);
                result.append('\u00A7').append('x');
                result.append('\u00A7').append(String.format("%02X", lastRgb[0]).charAt(0));
                result.append('\u00A7').append(String.format("%02X", lastRgb[0]).charAt(1));
                result.append('\u00A7').append(String.format("%02X", lastRgb[1]).charAt(0));
                result.append('\u00A7').append(String.format("%02X", lastRgb[1]).charAt(1));
                result.append('\u00A7').append(String.format("%02X", lastRgb[2]).charAt(0));
                result.append('\u00A7').append(String.format("%02X", lastRgb[2]).charAt(1));
                result.append(fmt).append(ch);
                charIdx++;
            }
            i = end + 1;
        }
        return result.toString();
    }

    private int[] parseGradientColor(String s) {
        s = s.trim();
        if (s.startsWith("&#")) s = s.substring(1);
        if (s.startsWith("#") && s.length() == 7) {
            try {
                return new int[]{
                    Integer.parseInt(s.substring(1,3),16),
                    Integer.parseInt(s.substring(3,5),16),
                    Integer.parseInt(s.substring(5,7),16)
                };
            } catch (Exception ignored) {}
        }
        if (s.startsWith("&") && s.length() == 2) return mcColorToRgb(s.charAt(1));
        return null;
    }

    private int[] interpolateGradient(List<int[]> stops, float pos) {
        if (stops.size() == 1) return stops.get(0);
        float scaled = pos * (stops.size() - 1);
        int i0 = Math.min((int) scaled, stops.size() - 2);
        float t = scaled - i0;
        return new int[]{
            (int)(stops.get(i0)[0] * (1-t) + stops.get(i0+1)[0] * t),
            (int)(stops.get(i0)[1] * (1-t) + stops.get(i0+1)[1] * t),
            (int)(stops.get(i0)[2] * (1-t) + stops.get(i0+1)[2] * t)
        };
    }

    private static int[] mcColorToRgb(char code) {
        switch (Character.toLowerCase(code)) {
            case '0': return new int[]{  0,   0,   0};
            case '1': return new int[]{  0,   0, 170};
            case '2': return new int[]{  0, 170,   0};
            case '3': return new int[]{  0, 170, 170};
            case '4': return new int[]{170,   0,   0};
            case '5': return new int[]{170,   0, 170};
            case '6': return new int[]{255, 170,   0};
            case '7': return new int[]{170, 170, 170};
            case '8': return new int[]{ 85,  85,  85};
            case '9': return new int[]{ 85,  85, 255};
            case 'a': return new int[]{ 85, 255,  85};
            case 'b': return new int[]{ 85, 255, 255};
            case 'c': return new int[]{255,  85,  85};
            case 'd': return new int[]{255,  85, 255};
            case 'e': return new int[]{255, 255,  85};
            case 'f': return new int[]{255, 255, 255};
            default:  return null;
        }
    }

    // ── BaseComponent-Builder ─────────────────────────────────────────────────
    // Wandelt einen vorverarbeiteten String (§x§R§R§G§G§B§B + §-Codes) in
    // ein BaseComponent[]-Array um – identisch zu ScoreboardModule.buildComponents().

    private static BaseComponent[] buildComponents(String text) {
        if (text == null || text.isEmpty())
            return new BaseComponent[]{ new TextComponent("") };

        List<BaseComponent> parts = new ArrayList<>();
        net.md_5.bungee.api.ChatColor currentColor = net.md_5.bungee.api.ChatColor.WHITE;
        boolean bold = false, italic = false, underline = false, strike = false, magic = false;
        int i = 0;
        StringBuilder buf = new StringBuilder();

        while (i < text.length()) {
            char c = text.charAt(i);
            // §x§R§R§G§G§B§B – RGB Hex (14 Zeichen)
            if (c == '§' && i + 13 < text.length() && text.charAt(i+1) == 'x') {
                if (buf.length() > 0) {
                    parts.add(makeComp(buf.toString(), currentColor, bold, italic, underline, strike, magic));
                    buf.setLength(0);
                }
                try {
                    String hex = "" + text.charAt(i+3) + text.charAt(i+5)
                               + text.charAt(i+7) + text.charAt(i+9)
                               + text.charAt(i+11) + text.charAt(i+13);
                    currentColor = net.md_5.bungee.api.ChatColor.of("#" + hex);
                } catch (Exception ignored) {}
                i += 14;
                continue;
            }
            // §X – Farb-/Formatcodes
            if (c == '§' && i + 1 < text.length()) {
                char code = Character.toLowerCase(text.charAt(i+1));
                if (buf.length() > 0) {
                    parts.add(makeComp(buf.toString(), currentColor, bold, italic, underline, strike, magic));
                    buf.setLength(0);
                }
                switch (code) {
                    case 'r': currentColor = net.md_5.bungee.api.ChatColor.WHITE;
                              bold=false; italic=false; underline=false; strike=false; magic=false; break;
                    case 'l': bold=true;      break;
                    case 'o': italic=true;    break;
                    case 'n': underline=true; break;
                    case 'm': strike=true;    break;
                    case 'k': magic=true;     break;
                    default:
                        net.md_5.bungee.api.ChatColor col = net.md_5.bungee.api.ChatColor.getByChar(code);
                        if (col != null) { currentColor=col; bold=false; italic=false; underline=false; strike=false; magic=false; }
                }
                i += 2;
                continue;
            }
            buf.append(c);
            i++;
        }
        if (buf.length() > 0)
            parts.add(makeComp(buf.toString(), currentColor, bold, italic, underline, strike, magic));
        if (parts.isEmpty())
            return new BaseComponent[]{ new TextComponent("") };
        return parts.toArray(new BaseComponent[0]);
    }

    private static BaseComponent makeComp(String text,
            net.md_5.bungee.api.ChatColor color,
            boolean bold, boolean italic, boolean underline, boolean strike, boolean magic) {
        TextComponent tc = new TextComponent(text);
        tc.setColor(color);
        tc.setBold(bold);
        tc.setItalic(italic);
        tc.setUnderlined(underline);
        tc.setStrikethrough(strike);
        tc.setObfuscated(magic);
        return tc;
    }

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

    private void loadConfig() {
        java.io.File file = new java.io.File(plugin.getDataFolder(), CONFIG_FILE);
        Map<String, String> map = new LinkedHashMap<>();
        if (file.exists()) {
            try (java.io.BufferedReader br = new java.io.BufferedReader(
                    new java.io.InputStreamReader(
                        new java.io.FileInputStream(file),
                        java.nio.charset.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("[AfkModule] Ladefehler: " + e.getMessage());
            }
        }
        enabled      = Boolean.parseBoolean(map.getOrDefault("afk.enabled",           "true"));
        idleEnabled  = Boolean.parseBoolean(map.getOrDefault("afk.idle_enabled",       "true"));
        idleSeconds  = parseInt(map.getOrDefault("afk.idle_seconds",                   "300"), 300);
        bypassPerm   = map.getOrDefault("afk.permission.bypass", "statusapi.afk.bypass");
        titleFadeIn  = parseInt(map.getOrDefault("afk.title.fade_in",  "10"),  10);
        titleStay    = parseInt(map.getOrDefault("afk.title.stay",    "100"), 100);
        titleFadeOut = parseInt(map.getOrDefault("afk.title.fade_out", "10"),  10);

        titlePairs.clear();
        for (int i = 1; i <= 20; i++) {
            String raw = map.get("afk.title.pair." + i);
            if (raw == null || raw.isEmpty()) continue;
            // Format: setTitle|setSubtitle||unsetTitle|unsetSubtitle
            int sep = raw.indexOf("||");
            if (sep < 0) continue;
            String[] set   = splitTitleLine(raw.substring(0, sep));
            String[] unset = splitTitleLine(raw.substring(sep + 2));
            titlePairs.add(new TitlePair(set, unset));
        }
        if (titlePairs.isEmpty()) {
            titlePairs.add(new TitlePair(
                new String[]{"%gradient:&b:&f:&b:&l [AFK] %", "&8Bewege dich um zur\u00fcckzukehren"},
                new String[]{"&aWillkommen zur\u00fcck!", ""}
            ));
        }
    }

    private String[] splitTitleLine(String raw) {
        int pipe = raw.indexOf('|');
        if (pipe < 0) return new String[]{ raw, "" };
        return new String[]{ raw.substring(0, pipe), raw.substring(pipe + 1) };
    }

    private void ensureConfigExists() {
        java.io.File file = new java.io.File(plugin.getDataFolder(), CONFIG_FILE);
        if (file.exists()) return;
        try (java.io.OutputStreamWriter w = new java.io.OutputStreamWriter(
                new java.io.FileOutputStream(file), java.nio.charset.StandardCharsets.UTF_8)) {
            w.write(
                "# =====================================================\n" +
                "# AfkModule – /afk Befehl & automatische AFK-Erkennung\n" +
                "# =====================================================\n\n" +
                "afk.enabled=true\n\n" +
                "# Automatisch AFK setzen nach X Sekunden ohne Aktivitaet\n" +
                "afk.idle_enabled=true\n" +
                "afk.idle_seconds=300\n\n" +
                "# Berechtigung zum Umgehen des auto-AFK\n" +
                "afk.permission.bypass=statusapi.afk.bypass\n\n" +
                "# ── Title-Anzeigezeiten (in Ticks, 20 Ticks = 1 Sekunde) ──\n" +
                "afk.title.fade_in=10\n" +
                "afk.title.stay=100\n" +
                "afk.title.fade_out=10\n\n" +
                "# ── Nachrichten-Paare ─────────────────────────────────────\n" +
                "# Format: setTitle|setSubtitle||unsetTitle|unsetSubtitle\n" +
                "#   |  trennt Title und Subtitle innerhalb einer Seite\n" +
                "#   || trennt AFK-Nachricht (links) von R\u00fcckkehr-Nachricht (rechts)\n" +
                "# Gradient: %gradient:FARBE1:FARBE2:...:TEXT%\n" +
                "# Farben: &b, &f, &a usw. oder #RRGGBB\n" +
                "# Es wird zufaellig ein Paar gewaehlt (max. 20 Paare).\n" +
                "# Die R\u00fcckkehr-Nachricht passt thematisch zur AFK-Nachricht.\n\n" +
                "afk.title.pair.1=%gradient:&b:&f:&b:&l [AFK] %|&8Wahrscheinlich auf dem Klo...||%gradient:&a:&f:&a:&l Willkommen zur\u00fcck! %|&7War das Klo sauber?\n" +
                "afk.title.pair.2=%gradient:&b:&f:&b:&l [AFK] %|&8Hat den Stecker gezogen||%gradient:&a:&f:&a:&l Wieder eingesteckt! %|&7Der Strom ist zur\u00fcck\n" +
                "afk.title.pair.3=%gradient:&b:&f:&b:&l [AFK] %|&8Schaut seit 10min die Decke an||%gradient:&a:&f:&a:&l Na endlich! %|&7Die Decke hat dich freigegeben\n" +
                "afk.title.pair.4=%gradient:&b:&f:&b:&l [AFK] %|&8Vom K\u00fchlschrank verschluckt worden||%gradient:&a:&f:&a:&l Er lebt! %|&7Der Snack war es wert, oder?\n" +
                "afk.title.pair.5=%gradient:&b:&f:&b:&l [AFK] %|&8Loading... Spieler nicht gefunden||%gradient:&a:&f:&a:&l Error behoben! %|&7Spieler erfolgreich neugestartet\n" +
                "afk.title.pair.6=%gradient:&b:&f:&b:&l [AFK] %|&8Vermutlich beim Snackholen||%gradient:&a:&f:&a:&l Snack erfolgreich geholt! %|&7Weiter gehts!\n" +
                "afk.title.pair.7=%gradient:&b:&f:&b:&l [AFK] %|&8Eingeschlafen. Bitte nicht wecken.||%gradient:&a:&f:&a:&l Aufgewacht! %|&7Der Wecker hat funktioniert\n" +
                "afk.title.pair.8=%gradient:&b:&f:&b:&l [AFK] %|&8Hat die Realit\u00e4t betreten||%gradient:&a:&f:&a:&l Zur\u00fcck aus der Realit\u00e4t! %|&7Und? War's schlimm?\n" +
                "afk.title.pair.9=%gradient:&b:&f:&b:&l [AFK] %|&8Gehirn: AFK. K\u00f6rper: noch da.||%gradient:&a:&f:&a:&l Gehirn wieder eingeschaltet! %|&7Willkommen zur\u00fcck\n" +
                "afk.title.pair.10=%gradient:&b:&f:&b:&l [AFK] %|&8Spricht mit echten Menschen. Seltsam.||%gradient:&a:&f:&a:&l Zur\u00fcck zu den Pixeln! %|&7Echte Menschen sind \u00fcbersch\u00e4tzt\n" +
                "afk.title.pair.11=%gradient:&b:&f:&b:&l [AFK] %|&8Sucht den Einschalter f\u00fcrs Leben||%gradient:&a:&f:&a:&l Einschalter gefunden! %|&7Das Leben l\u00e4uft wieder\n" +
                "afk.title.pair.12=%gradient:&b:&f:&b:&l [AFK] %|&8Mom hat gerufen. RIP.||%gradient:&a:&f:&a:&l Mom ist wieder weg. %|&7Puh. Knapp entkommen.\n" +
                "afk.title.pair.13=%gradient:&b:&f:&b:&l [AFK] %|&8Error 404: Spieler nicht gefunden||%gradient:&a:&f:&a:&l Spieler wieder online! %|&7404 behoben\n" +
                "afk.title.pair.14=%gradient:&b:&f:&b:&l [AFK] %|&8Tut so als w\u00e4re er besch\u00e4ftigt||%gradient:&a:&f:&a:&l Aufgeflogen! %|&7War eh nicht \u00fcberzeugend\n" +
                "afk.title.pair.15=%gradient:&b:&f:&b:&l [AFK] %|&8Kaffeepause. Die wichtigste Pause.||%gradient:&a:&f:&a:&l Koffein erfolgreich zugef\u00fchrt! %|&7Jetzt wieder einsatzbereit\n"
            );
        } catch (Exception e) {
            plugin.getLogger().warning("[AfkModule] Konnte Config nicht erstellen: " + e.getMessage());
        }
    }

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

    private static TextComponent plain(String text) {
        return new TextComponent(ChatColor.translateAlternateColorCodes('&', text));
    }

    private static int parseInt(String s, int def) {
        try { return Integer.parseInt(s.trim()); } catch (Exception e) { return def; }
    }
}
