package net.viper.status.modules.antibot;

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.connection.PendingConnection;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.event.PreLoginEvent;
import net.md_5.bungee.api.event.PostLoginEvent;
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.event.EventHandler;
import net.viper.status.StatusAPI;
import net.viper.status.module.Module;
import net.viper.status.modules.network.NetworkInfoModule;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.URL;
import java.nio.file.Files;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Date;
import java.util.Deque;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;

/**
 * Eigenst\u00e4ndiger AntiBot/Attack-Guard.
 *
 * Fixes:
 *   - cleanupExpired() nutzt jetzt removeIf() statt Iteration + remove() (Bug #3)
 *   - applyProfileDefaults() setzt korrekten attackDefaultSource aus Config
 */
public class AntiBotModule implements Module, Listener {

    private static final String CONFIG_FILE_NAME = "network-guard.properties";

    private StatusAPI plugin;

    private boolean enabled = true;
    private String profile = "high-traffic";
    private int maxCps = 120;
    private int attackStartCps = 220;
    private int attackStopCps = 120;
    private int attackCalmSeconds = 20;
    private int ipConnectionsPerMinute = 18;
    private int ipBlockSeconds = 600;
    private String kickMessage = "Zu viele Verbindungen von deiner IP. Bitte warte kurz.";

    private boolean vpnCheckEnabled = false;
    private boolean vpnBlockProxy = true;
    private boolean vpnBlockHosting = true;
    private int vpnCacheMinutes = 30;
    private int vpnTimeoutMs = 2500;
    private boolean securityLogEnabled = true;
    private String securityLogFileName = "antibot-security.log";
    private File securityLogFile;
    private final Object securityLogLock = new Object();

    private boolean learningModeEnabled = true;
    private int learningScoreThreshold = 100;
    private int learningDecayPerSecond = 2;
    private int learningStateWindowSeconds = 120;
    private int learningRapidWindowMs = 1500;
    private int learningRapidPoints = 12;
    private int learningIpRateExceededPoints = 30;
    private int learningVpnProxyPoints = 40;
    private int learningVpnHostingPoints = 30;
    private int learningAttackModePoints = 12;
    private int learningHighCpsPoints = 10;
    private int learningRecentEventLimit = 30;

    private final AtomicInteger currentSecondConnections = new AtomicInteger(0);
    private volatile long currentSecond = System.currentTimeMillis() / 1000L;
    private volatile int lastCps = 0;
    private final AtomicInteger peakCps = new AtomicInteger(0);

    private volatile boolean attackMode = false;
    private volatile long attackCalmSince = 0L;
    private final AtomicLong blockedConnectionsTotal = new AtomicLong(0L);
    private final AtomicLong blockedConnectionsCurrentAttack = new AtomicLong(0L);
    private final Set<String> blockedIpsCurrentAttack = ConcurrentHashMap.newKeySet();

    private final Map<String, IpWindow> perIpWindows = new ConcurrentHashMap<>();
    private final Map<String, Long> blockedIpsUntil = new ConcurrentHashMap<>();
    private final Map<String, VpnCacheEntry> vpnCache = new ConcurrentHashMap<>();
    private final Map<String, RecentPlayerIdentity> recentIdentityByIp = new ConcurrentHashMap<>();
    private final Map<String, LearningProfile> learningProfiles = new ConcurrentHashMap<>();
    private final Deque<String> learningRecentEvents = new ArrayDeque<>();

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

    @Override
    public void onEnable(Plugin plugin) {
        if (!(plugin instanceof StatusAPI)) return;
        this.plugin = (StatusAPI) plugin;
        ensureModuleConfigExists();
        loadConfig();
        ensureSecurityLogFile();

        if (!enabled) {
            StatusAPI.debugLog(this.plugin, "[AntiBotModule] deaktiviert via " + CONFIG_FILE_NAME);
            return;
        }

        ProxyServer.getInstance().getPluginManager().registerListener(this.plugin, this);
        ProxyServer.getInstance().getPluginManager().registerCommand(this.plugin, new AntiBotCommand());
        ProxyServer.getInstance().getScheduler().schedule(this.plugin, this::tick, 1, 1, TimeUnit.SECONDS);

        this.plugin.getLogger().fine("[AntiBotModule] aktiviert. maxCps=" + maxCps
                + ", attackStartCps=" + attackStartCps + ", ip/min=" + ipConnectionsPerMinute);
    }

    @Override
    public void onDisable(Plugin plugin) {
        perIpWindows.clear();
        blockedIpsUntil.clear();
        vpnCache.clear();
        learningProfiles.clear();
        synchronized (learningRecentEvents) { learningRecentEvents.clear(); }
        blockedIpsCurrentAttack.clear();
        attackMode = false;
    }

    public boolean isEnabled() { return enabled; }

    private void reloadRuntimeState() {
        perIpWindows.clear();
        blockedIpsUntil.clear();
        vpnCache.clear();
        learningProfiles.clear();
        synchronized (learningRecentEvents) { learningRecentEvents.clear(); }
        blockedIpsCurrentAttack.clear();
        attackMode = false;
        attackCalmSince = 0L;
        blockedConnectionsCurrentAttack.set(0L);
        currentSecondConnections.set(0);
        lastCps = 0;
        peakCps.set(0);
        loadConfig();
        ensureSecurityLogFile();
    }

    public Map<String, Object> buildSnapshot() {
        Map<String, Object> out = new LinkedHashMap<>();
        out.put("enabled",               enabled);
        out.put("profile",               profile);
        out.put("attack_mode",           attackMode);
        out.put("protection_enabled",    enabled);
        out.put("attack_mode_status",    attackMode ? "active" : "normal");
        out.put("attack_mode_display",   attackMode ? "Angriff erkannt" : "Normalbetrieb");
        out.put("status_message", enabled
                ? (attackMode ? "AntiBot aktiv: Angriff erkannt" : "AntiBot aktiv: kein Angriff erkannt")
                : "AntiBot deaktiviert");
        out.put("last_cps",                   lastCps);
        out.put("peak_cps",                   peakCps.get());
        out.put("blocked_ips_active",          blockedIpsUntil.size());
        out.put("blocked_connections_total",   blockedConnectionsTotal.get());
        out.put("vpn_check_enabled",           vpnCheckEnabled);
        out.put("learning_mode_enabled",       learningModeEnabled);
        out.put("learning_profiles",           learningProfiles.size());
        out.put("thresholds",                  buildThresholds());
        return out;
    }

    private Map<String, Object> buildThresholds() {
        Map<String, Object> m = new LinkedHashMap<>();
        m.put("max_cps",                   maxCps);
        m.put("attack_start_cps",          attackStartCps);
        m.put("attack_stop_cps",           attackStopCps);
        m.put("attack_calm_seconds",       attackCalmSeconds);
        m.put("ip_connections_per_minute", ipConnectionsPerMinute);
        m.put("ip_block_seconds",          ipBlockSeconds);
        m.put("learning_score_threshold",  learningScoreThreshold);
        m.put("learning_decay_per_second", learningDecayPerSecond);
        return m;
    }

    @EventHandler
    public void onPreLogin(PreLoginEvent event) {
        if (!enabled) return;

        String ip = extractIp(event.getConnection());
        if (ip == null || ip.isEmpty()) return;

        cacheRecentIdentity(ip, event.getConnection(), System.currentTimeMillis());
        recordConnection();
        long now = System.currentTimeMillis();

        // FIX #3: cleanupExpired verwendet removeIf statt Iteration+remove
        cleanupExpired(now);

        Long blockedUntil = blockedIpsUntil.get(ip);
        if (blockedUntil != null && blockedUntil > now) {
            logSecurityEvent("ip_block_active", ip, event.getConnection(), "blocked_until_ms=" + blockedUntil);
            blockEvent(event);
            return;
        }

        if (learningModeEnabled) {
            evaluateLearningBaseline(ip, now);
            Long learningBlock = blockedIpsUntil.get(ip);
            if (learningBlock != null && learningBlock > now) {
                blockEvent(event);
                return;
            }
        }

        boolean ipRateExceeded = isIpRateExceeded(ip, now);
        if (ipRateExceeded) {
            if (learningModeEnabled) {
                int score = addLearningScore(ip, now, learningIpRateExceededPoints, "ip-rate-exceeded", true);
                logSecurityEvent("ip_rate_exceeded_scored", ip, event.getConnection(), "score=" + score + ", threshold=" + learningScoreThreshold);
                if (score >= learningScoreThreshold) {
                    logSecurityEvent("learning_threshold_block", ip, event.getConnection(), "reason=ip-rate-exceeded, score=" + score);
                    blockEvent(event);
                    return;
                }
            } else {
                blockIp(ip, now);
                logSecurityEvent("ip_rate_limit_block", ip, event.getConnection(), "mode=direct");
                blockEvent(event);
                return;
            }
        }

        // FIX Netzwerk-Timeout: VPN-Check asynchron ausführen damit der BungeeCord-
        // Netzwerk-Thread nicht blockiert wird. Ein blockierender HTTP-Request (bis zu
        // vpnTimeoutMs = 2500 ms) im PreLoginEvent-Handler verhindert, dass KeepAlive-
        // Pakete an bereits eingeloggte Spieler verarbeitet werden → Disconnect.
        if (vpnCheckEnabled) {
            // Cache-Treffer: sofort prüfen, kein async nötig
            VpnCacheEntry cached = vpnCache.get(ip);
            if (cached != null && cached.expiresAt > now) {
                VpnCheckResult info = cached.result;
                if (info != null) {
                    boolean shouldBlock = (vpnBlockProxy && info.proxy) || (vpnBlockHosting && info.hosting);
                    if (shouldBlock) {
                        logSecurityEvent("vpn_detected", ip, event.getConnection(), "proxy=" + info.proxy + ", hosting=" + info.hosting);
                        if (learningModeEnabled) {
                            if (vpnBlockProxy && info.proxy)     addLearningScore(ip, now, learningVpnProxyPoints,   "vpn-proxy",   false);
                            if (vpnBlockHosting && info.hosting) addLearningScore(ip, now, learningVpnHostingPoints, "vpn-hosting", false);
                            int current = getLearningScore(ip, now);
                            if (current >= learningScoreThreshold) {
                                blockIp(ip, now);
                                logSecurityEvent("learning_threshold_block", ip, event.getConnection(), "reason=vpn, score=" + current);
                                recordLearningEvent("BLOCK " + ip + " reason=vpn score=" + current);
                                blockEvent(event);
                            }
                        } else {
                            blockIp(ip, now);
                            logSecurityEvent("vpn_block", ip, event.getConnection(), "mode=direct, proxy=" + info.proxy + ", hosting=" + info.hosting);
                            blockEvent(event);
                        }
                    }
                }
            } else {
                // Kein Cache-Treffer → HTTP-Request async ausführen
                event.registerIntent(plugin);
                final long nowFinal = now;
                ProxyServer.getInstance().getScheduler().runAsync(plugin, () -> {
                    try {
                        VpnCheckResult info = requestIpApi(ip);
                        if (info != null) {
                            VpnCacheEntry entry = new VpnCacheEntry();
                            entry.result    = info;
                            entry.expiresAt = nowFinal + Math.max(1, vpnCacheMinutes) * 60_000L;
                            vpnCache.put(ip, entry);

                            boolean shouldBlock = (vpnBlockProxy && info.proxy) || (vpnBlockHosting && info.hosting);
                            if (shouldBlock) {
                                logSecurityEvent("vpn_detected", ip, event.getConnection(), "proxy=" + info.proxy + ", hosting=" + info.hosting);
                                if (learningModeEnabled) {
                                    if (vpnBlockProxy && info.proxy)     addLearningScore(ip, nowFinal, learningVpnProxyPoints,   "vpn-proxy",   false);
                                    if (vpnBlockHosting && info.hosting) addLearningScore(ip, nowFinal, learningVpnHostingPoints, "vpn-hosting", false);
                                    int current = getLearningScore(ip, nowFinal);
                                    if (current >= learningScoreThreshold) {
                                        blockIp(ip, nowFinal);
                                        logSecurityEvent("learning_threshold_block", ip, event.getConnection(), "reason=vpn, score=" + current);
                                        recordLearningEvent("BLOCK " + ip + " reason=vpn score=" + current);
                                        blockEvent(event);
                                    }
                                } else {
                                    blockIp(ip, nowFinal);
                                    logSecurityEvent("vpn_block", ip, event.getConnection(), "mode=direct, proxy=" + info.proxy + ", hosting=" + info.hosting);
                                    blockEvent(event);
                                }
                            }
                        }
                    } finally {
                        event.completeIntent(plugin);
                    }
                });
            }
        }
    }

    @EventHandler
    public void onPostLogin(PostLoginEvent event) {
        if (!enabled || event == null || event.getPlayer() == null) return;
        ProxiedPlayer player = event.getPlayer();
        String ip = extractIpFromPlayer(player);
        if (ip == null || ip.isEmpty()) return;
        cacheRecentIdentityDirect(ip, player.getName(), player.getUniqueId(), System.currentTimeMillis());
    }

    private void blockEvent(PreLoginEvent event) {
        event.setCancelled(true);
    }

    private String extractIp(PendingConnection conn) {
        if (conn == null || conn.getAddress() == null) return null;
        if (conn.getAddress() instanceof InetSocketAddress) {
            InetSocketAddress sa = (InetSocketAddress) conn.getAddress();
            return sa.getAddress() != null ? sa.getAddress().getHostAddress() : sa.getHostString();
        }
        return String.valueOf(conn.getAddress());
    }

    private String extractIpFromPlayer(ProxiedPlayer player) {
        if (player == null || player.getAddress() == null) return null;
        if (player.getAddress() instanceof InetSocketAddress) {
            InetSocketAddress sa = (InetSocketAddress) player.getAddress();
            return sa.getAddress() != null ? sa.getAddress().getHostAddress() : sa.getHostString();
        }
        return String.valueOf(player.getAddress());
    }

    private void recordConnection() {
        long sec = System.currentTimeMillis() / 1000L;
        if (sec != currentSecond) {
            synchronized (this) {
                if (sec != currentSecond) {
                    currentSecond = sec;
                    currentSecondConnections.set(0);
                }
            }
        }
        currentSecondConnections.incrementAndGet();
    }

    private boolean isIpRateExceeded(String ip, long now) {
        IpWindow window = perIpWindows.computeIfAbsent(ip, k -> new IpWindow(now));
        synchronized (window) {
            long diff = now - window.windowStart;
            if (diff > 60_000L) {
                window.windowStart = now;
                window.count = 0;
            }
            window.count++;
            return window.count > Math.max(1, ipConnectionsPerMinute);
        }
    }

    /**
     * Sperrt eine IP f\u00fcr die konfigurierte Block-Dauer (antibot.ip.block_seconds).
     * Kann von anderen Modulen aufgerufen werden (z. B. MultiAccountGuard).
     * @param ip  Die zu sperrende IP-Adresse
     * @param durationSeconds  Sperrdauer in Sekunden (0 = antibot-Standard verwenden)
     */
    public void blockIpExternal(String ip, int durationSeconds) {
        long now = System.currentTimeMillis();
        long duration = durationSeconds > 0 ? durationSeconds : Math.max(1, ipBlockSeconds);
        blockedIpsUntil.put(ip, now + duration * 1000L);
        blockedConnectionsTotal.incrementAndGet();
    }

    private void blockIp(String ip, long now) {
        blockedIpsUntil.put(ip, now + Math.max(1, ipBlockSeconds) * 1000L);
        blockedConnectionsTotal.incrementAndGet();
        if (attackMode) {
            blockedConnectionsCurrentAttack.incrementAndGet();
            blockedIpsCurrentAttack.add(ip);
        }
    }

    /**
     * FIX #3: Verwendet removeIf() statt for-each + remove() um ConcurrentModificationException zu vermeiden.
     */
    private void cleanupExpired(long now) {
        blockedIpsUntil.entrySet().removeIf(e -> e.getValue() <= now);
        vpnCache.entrySet().removeIf(e -> e.getValue().expiresAt <= now);
        recentIdentityByIp.entrySet().removeIf(e -> {
            RecentPlayerIdentity id = e.getValue();
            return id == null || (now - id.updatedAtMs) > 600_000L;
        });
        if (learningModeEnabled) {
            long staleAfter = Math.max(60, learningStateWindowSeconds) * 1000L;
            learningProfiles.entrySet().removeIf(e -> {
                LearningProfile lp = e.getValue();
                return lp == null || ((now - lp.lastSeenAt) > staleAfter && lp.score <= 0);
            });
        }
    }

    private void tick() {
        if (!enabled) return;
        int cps = currentSecondConnections.getAndSet(0);
        lastCps = cps;
        if (cps > peakCps.get()) peakCps.set(cps);

        long now = System.currentTimeMillis();

        if (!attackMode && cps >= Math.max(1, attackStartCps)) {
            attackMode = true;
            attackCalmSince = 0L;
            blockedConnectionsCurrentAttack.set(0L);
            blockedIpsCurrentAttack.clear();
            sendAttackToWebhook("detected", cps, null, null, "StatusAPI AntiBot");
            plugin.getLogger().warning("[AntiBotModule] Attack erkannt. CPS=" + cps);
            return;
        }

        if (attackMode) {
            if (cps <= Math.max(1, attackStopCps)) {
                if (attackCalmSince == 0L) attackCalmSince = now;
                long calmFor = now - attackCalmSince;
                if (calmFor >= Math.max(1, attackCalmSeconds) * 1000L) {
                    attackMode = false;
                    attackCalmSince = 0L;
                    int blockedIps = blockedIpsCurrentAttack.size();
                    long blockedConns = blockedConnectionsCurrentAttack.get();
                    sendAttackToWebhook("stopped", cps, blockedIps, blockedConns, "StatusAPI AntiBot");
                    plugin.getLogger().warning("[AntiBotModule] Attack beendet. blockedIps=" + blockedIps + ", blockedConnections=" + blockedConns);
                }
            } else {
                attackCalmSince = 0L;
            }
        }
    }

    private void sendAttackToWebhook(String type, Integer cps, Integer blockedIps, Long blockedConnections, String source) {
        NetworkInfoModule networkInfoModule = getNetworkInfoModule();
        if (networkInfoModule == null) return;
        networkInfoModule.sendAttackNotification(type, cps, blockedIps, blockedConnections, source);
    }

    private NetworkInfoModule getNetworkInfoModule() {
        if (plugin == null || plugin.getModuleManager() == null) return null;
        return (NetworkInfoModule) plugin.getModuleManager().getModule("NetworkInfoModule");
    }

    private void loadConfig() {
        File file = new File(plugin.getDataFolder(), CONFIG_FILE_NAME);
        if (!file.exists()) return;

        Properties props = new Properties();
        try (FileInputStream in = new FileInputStream(file)) {
            props.load(new InputStreamReader(in, StandardCharsets.UTF_8));

            enabled                    = parseBoolean(props.getProperty("antibot.enabled"), true);
            profile                    = normalizeProfile(props.getProperty("antibot.profile", "high-traffic"));
            applyProfileDefaults(profile);

            maxCps                     = parseInt(props.getProperty("antibot.max_cps"), maxCps);
            attackStartCps             = parseInt(props.getProperty("antibot.attack.start_cps"), attackStartCps);
            attackStopCps              = parseInt(props.getProperty("antibot.attack.stop_cps"), attackStopCps);
            attackCalmSeconds          = parseInt(props.getProperty("antibot.attack.stop_grace_seconds"), attackCalmSeconds);
            ipConnectionsPerMinute     = parseInt(props.getProperty("antibot.ip.max_connections_per_minute"), ipConnectionsPerMinute);
            ipBlockSeconds             = parseInt(props.getProperty("antibot.ip.block_seconds"), ipBlockSeconds);
            kickMessage                = props.getProperty("antibot.kick_message", kickMessage);

            vpnCheckEnabled            = parseBoolean(props.getProperty("antibot.vpn_check.enabled"), vpnCheckEnabled);
            vpnBlockProxy              = parseBoolean(props.getProperty("antibot.vpn_check.block_proxy"), vpnBlockProxy);
            vpnBlockHosting            = parseBoolean(props.getProperty("antibot.vpn_check.block_hosting"), vpnBlockHosting);
            vpnCacheMinutes            = parseInt(props.getProperty("antibot.vpn_check.cache_minutes"), vpnCacheMinutes);
            vpnTimeoutMs               = parseInt(props.getProperty("antibot.vpn_check.timeout_ms"), vpnTimeoutMs);
            securityLogEnabled         = parseBoolean(props.getProperty("antibot.security_log.enabled"), securityLogEnabled);
            securityLogFileName        = props.getProperty("antibot.security_log.file", securityLogFileName).trim();
            if (securityLogFileName.isEmpty()) securityLogFileName = "antibot-security.log";

            learningModeEnabled        = parseBoolean(props.getProperty("antibot.learning.enabled"), learningModeEnabled);
            learningScoreThreshold     = parseInt(props.getProperty("antibot.learning.score_threshold"), learningScoreThreshold);
            learningDecayPerSecond     = parseInt(props.getProperty("antibot.learning.decay_per_second"), learningDecayPerSecond);
            learningStateWindowSeconds = parseInt(props.getProperty("antibot.learning.state_window_seconds"), learningStateWindowSeconds);
            learningRapidWindowMs      = parseInt(props.getProperty("antibot.learning.rapid.window_ms"), learningRapidWindowMs);
            learningRapidPoints        = parseInt(props.getProperty("antibot.learning.rapid.points"), learningRapidPoints);
            learningIpRateExceededPoints = parseInt(props.getProperty("antibot.learning.ip_rate_exceeded.points"), learningIpRateExceededPoints);
            learningVpnProxyPoints     = parseInt(props.getProperty("antibot.learning.vpn_proxy.points"), learningVpnProxyPoints);
            learningVpnHostingPoints   = parseInt(props.getProperty("antibot.learning.vpn_hosting.points"), learningVpnHostingPoints);
            learningAttackModePoints   = parseInt(props.getProperty("antibot.learning.attack_mode.points"), learningAttackModePoints);
            learningHighCpsPoints      = parseInt(props.getProperty("antibot.learning.high_cps.points"), learningHighCpsPoints);
            learningRecentEventLimit   = parseInt(props.getProperty("antibot.learning.recent_events.limit"), learningRecentEventLimit);
        } catch (Exception e) {
            plugin.getLogger().warning("[AntiBotModule] Fehler beim Laden von " + CONFIG_FILE_NAME + ": " + e.getMessage());
        }
    }

    private String normalizeProfile(String raw) {
        if (raw == null) return "high-traffic";
        String v = raw.trim().toLowerCase(Locale.ROOT);
        return "strict".equals(v) ? "strict" : "high-traffic";
    }

    private void applyProfileDefaults(String profileName) {
        if ("strict".equals(profileName)) {
            maxCps = 120; attackStartCps = 220; attackStopCps = 120; attackCalmSeconds = 20;
            ipConnectionsPerMinute = 18; ipBlockSeconds = 900;
            vpnCheckEnabled = true; vpnBlockProxy = true; vpnBlockHosting = true;
            vpnCacheMinutes = 30; vpnTimeoutMs = 2500;
        } else {
            maxCps = 180; attackStartCps = 300; attackStopCps = 170; attackCalmSeconds = 25;
            ipConnectionsPerMinute = 24; ipBlockSeconds = 600;
            vpnCheckEnabled = false; vpnBlockProxy = true; vpnBlockHosting = true;
            vpnCacheMinutes = 30; vpnTimeoutMs = 2500;
        }
    }

    private boolean isSupportedProfile(String raw) {
        if (raw == null) return false;
        String v = raw.trim().toLowerCase(Locale.ROOT);
        return "strict".equals(v) || "high-traffic".equals(v);
    }

    private boolean applyProfileAndPersist(String requestedProfile) {
        if (!isSupportedProfile(requestedProfile)) return false;
        String normalized = normalizeProfile(requestedProfile);
        profile = normalized;
        applyProfileDefaults(normalized);

        Map<String, String> values = new LinkedHashMap<>();
        values.put("antibot.profile",                        normalized);
        values.put("antibot.max_cps",                        String.valueOf(maxCps));
        values.put("antibot.attack.start_cps",               String.valueOf(attackStartCps));
        values.put("antibot.attack.stop_cps",                String.valueOf(attackStopCps));
        values.put("antibot.attack.stop_grace_seconds",      String.valueOf(attackCalmSeconds));
        values.put("antibot.ip.max_connections_per_minute",  String.valueOf(ipConnectionsPerMinute));
        values.put("antibot.ip.block_seconds",               String.valueOf(ipBlockSeconds));
        values.put("antibot.vpn_check.enabled",              String.valueOf(vpnCheckEnabled));
        values.put("antibot.vpn_check.block_proxy",          String.valueOf(vpnBlockProxy));
        values.put("antibot.vpn_check.block_hosting",        String.valueOf(vpnBlockHosting));
        values.put("antibot.vpn_check.cache_minutes",        String.valueOf(vpnCacheMinutes));
        values.put("antibot.vpn_check.timeout_ms",           String.valueOf(vpnTimeoutMs));
        try { updateConfigValues(values); return true; }
        catch (Exception e) { plugin.getLogger().warning("[AntiBotModule] Konnte Profil nicht speichern: " + e.getMessage()); return false; }
    }

    private synchronized void updateConfigValues(Map<String, String> keyValues) throws Exception {
        File target = new File(plugin.getDataFolder(), CONFIG_FILE_NAME);
        List<String> lines = target.exists()
                ? Files.readAllLines(target.toPath(), StandardCharsets.UTF_8)
                : new ArrayList<>();
        for (Map.Entry<String, String> entry : keyValues.entrySet()) {
            String key = entry.getKey();
            String newLine = key + "=" + entry.getValue();
            boolean replaced = false;
            for (int i = 0; i < lines.size(); i++) {
                if (lines.get(i).trim().startsWith(key + "=")) {
                    lines.set(i, newLine);
                    replaced = true;
                    break;
                }
            }
            if (!replaced) lines.add(newLine);
        }
        Files.write(target.toPath(), lines, StandardCharsets.UTF_8);
    }

    private void ensureModuleConfigExists() {
        File target = new File(plugin.getDataFolder(), CONFIG_FILE_NAME);
        if (target.exists()) return;
        if (!plugin.getDataFolder().exists()) plugin.getDataFolder().mkdirs();
        try (InputStream in = plugin.getResourceAsStream(CONFIG_FILE_NAME);
             FileOutputStream out = new FileOutputStream(target)) {
            if (in == null) { plugin.getLogger().warning("[AntiBotModule] Standarddatei nicht im JAR."); return; }
            byte[] buffer = new byte[4096]; int read;
            while ((read = in.read(buffer)) != -1) out.write(buffer, 0, read);
        } catch (Exception e) {
            plugin.getLogger().warning("[AntiBotModule] Konnte Config nicht erstellen: " + e.getMessage());
        }
    }

    private void evaluateLearningBaseline(String ip, long now) {
        LearningProfile lp = learningProfiles.computeIfAbsent(ip, k -> new LearningProfile(now));
        synchronized (lp) {
            decayLearningProfile(lp, now);
            long delta = now - lp.lastConnectionAt;
            if (lp.lastConnectionAt > 0 && delta <= Math.max(250L, learningRapidWindowMs)) {
                lp.rapidStreak++;
                int points = learningRapidPoints + Math.min(lp.rapidStreak, 5);
                lp.score += Math.max(1, points);
                recordLearningEvent("IP=" + ip + " +" + points + " rapid-connect score=" + lp.score);
            } else {
                lp.rapidStreak = 0;
            }
            if (attackMode) { lp.score += Math.max(1, learningAttackModePoints); recordLearningEvent("IP=" + ip + " +" + learningAttackModePoints + " attack-mode score=" + lp.score); }
            if (lastCps >= Math.max(1, maxCps)) { lp.score += Math.max(1, learningHighCpsPoints); recordLearningEvent("IP=" + ip + " +" + learningHighCpsPoints + " high-cps score=" + lp.score); }
            lp.lastConnectionAt = now;
            lp.lastSeenAt = now;
            if (lp.score >= learningScoreThreshold) {
                blockIp(ip, now);
                recordLearningEvent("BLOCK " + ip + " reason=learning-threshold score=" + lp.score);
            }
        }
    }

    private int addLearningScore(String ip, long now, int points, String reason, boolean checkThreshold) {
        LearningProfile lp = learningProfiles.computeIfAbsent(ip, k -> new LearningProfile(now));
        synchronized (lp) {
            decayLearningProfile(lp, now);
            int add = Math.max(1, points);
            lp.score += add;
            lp.lastSeenAt = now;
            recordLearningEvent("IP=" + ip + " +" + add + " " + reason + " score=" + lp.score);
            if (checkThreshold && lp.score >= learningScoreThreshold) {
                blockIp(ip, now);
                recordLearningEvent("BLOCK " + ip + " reason=" + reason + " score=" + lp.score);
            }
            return lp.score;
        }
    }

    private int getLearningScore(String ip, long now) {
        LearningProfile lp = learningProfiles.get(ip);
        if (lp == null) return 0;
        synchronized (lp) { decayLearningProfile(lp, now); return lp.score; }
    }

    private void decayLearningProfile(LearningProfile lp, long now) {
        long elapsedMs = Math.max(0L, now - lp.lastScoreUpdateAt);
        if (elapsedMs > 0L) {
            long decay = (elapsedMs / 1000L) * Math.max(0, learningDecayPerSecond);
            if (decay > 0L) lp.score = (int) Math.max(0L, lp.score - decay);
            lp.lastScoreUpdateAt = now;
        }
        long resetAfter = Math.max(30, learningStateWindowSeconds) * 1000L;
        if (lp.lastSeenAt > 0L && now - lp.lastSeenAt > resetAfter) {
            lp.score = 0;
            lp.rapidStreak = 0;
        }
    }

    private void recordLearningEvent(String event) {
        String line = new SimpleDateFormat("HH:mm:ss").format(new Date()) + " " + event;
        synchronized (learningRecentEvents) {
            learningRecentEvents.addLast(line);
            while (learningRecentEvents.size() > Math.max(5, learningRecentEventLimit)) learningRecentEvents.pollFirst();
        }
    }

    private void ensureSecurityLogFile() {
        if (plugin == null) return;
        if (!plugin.getDataFolder().exists()) plugin.getDataFolder().mkdirs();
        securityLogFile = new File(plugin.getDataFolder(), securityLogFileName);
        try { if (!securityLogFile.exists()) securityLogFile.createNewFile(); }
        catch (Exception e) { plugin.getLogger().warning("[AntiBotModule] Konnte Sicherheitslog nicht erstellen: " + e.getMessage()); }
    }

    private void logSecurityEvent(String eventType, String ip, PendingConnection conn, String details) {
        if (!securityLogEnabled || plugin == null) return;
        if (securityLogFile == null) { ensureSecurityLogFile(); if (securityLogFile == null) return; }

        String name = extractPlayerName(conn);
        String uuid = extractPlayerUuid(conn, name);
        if ((name == null || name.isEmpty() || "unknown".equalsIgnoreCase(name)) && ip != null) {
            RecentPlayerIdentity cached = recentIdentityByIp.get(ip);
            if (cached != null) {
                if (cached.playerName != null && !cached.playerName.trim().isEmpty()) name = cached.playerName;
                if ((uuid == null || uuid.isEmpty()) && cached.playerUuid != null && !cached.playerUuid.trim().isEmpty()) uuid = cached.playerUuid;
            }
        }
        if (name == null || name.trim().isEmpty()) name = "unknown";
        if (uuid == null || uuid.trim().isEmpty()) uuid = "unknown";

        String line = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())
                + " | event=" + safeLog(eventType)
                + " | ip=" + safeLog(ip)
                + " | player=" + safeLog(name)
                + " | uuid=" + safeLog(uuid)
                + " | details=" + safeLog(details);

        synchronized (securityLogLock) {
            try (BufferedWriter bw = new BufferedWriter(new FileWriter(securityLogFile, true))) {
                bw.write(line); bw.newLine();
            } catch (Exception e) {
                plugin.getLogger().warning("[AntiBotModule] Sicherheitslog-Schreibfehler: " + e.getMessage());
            }
        }
    }

    private void cacheRecentIdentity(String ip, PendingConnection conn, long now) {
        if (ip == null || ip.isEmpty() || conn == null) return;
        String name = extractPlayerName(conn);
        String uuid = extractPlayerUuid(conn, name);
        if ((name == null || name.isEmpty()) && (uuid == null || uuid.isEmpty())) return;
        RecentPlayerIdentity identity = recentIdentityByIp.computeIfAbsent(ip, k -> new RecentPlayerIdentity());
        synchronized (identity) {
            if (name != null && !name.trim().isEmpty()) identity.playerName = name.trim();
            if (uuid != null && !uuid.trim().isEmpty()) identity.playerUuid = uuid.trim();
            identity.updatedAtMs = now;
        }
    }

    private void cacheRecentIdentityDirect(String ip, String playerName, UUID playerUuid, long now) {
        if (ip == null || ip.isEmpty()) return;
        String name = playerName == null ? "" : playerName.trim();
        String uuid = playerUuid == null ? "" : playerUuid.toString();
        if (name.isEmpty() && uuid.isEmpty()) return;
        RecentPlayerIdentity identity = recentIdentityByIp.computeIfAbsent(ip, k -> new RecentPlayerIdentity());
        synchronized (identity) {
            if (!name.isEmpty()) identity.playerName = name;
            if (!uuid.isEmpty()) identity.playerUuid = uuid;
            identity.updatedAtMs = now;
        }
    }

    private String extractPlayerName(PendingConnection conn) {
        if (conn == null) return "";
        try { String raw = conn.getName(); return raw == null ? "" : raw.trim(); } catch (Exception ignored) { return ""; }
    }

    private String extractPlayerUuid(PendingConnection conn, String playerName) {
        if (conn != null) {
            try { UUID uuid = conn.getUniqueId(); if (uuid != null) return uuid.toString(); } catch (Exception ignored) {}
        }
        if (playerName != null && !playerName.trim().isEmpty()) {
            return UUID.nameUUIDFromBytes(("OfflinePlayer:" + playerName.trim()).getBytes(StandardCharsets.UTF_8)).toString();
        }
        return "";
    }

    private String safeLog(String input) {
        if (input == null || input.isEmpty()) return "-";
        return input.replace("\n", " ").replace("\r", " ").trim();
    }

    private List<String> getRecentLearningEvents(int max) {
        List<String> out = new ArrayList<>();
        synchronized (learningRecentEvents) {
            int skip = Math.max(0, learningRecentEvents.size() - Math.max(1, max));
            int idx = 0;
            for (String line : learningRecentEvents) {
                if (idx++ < skip) continue;
                out.add(line);
            }
        }
        return out;
    }

    private boolean parseBoolean(String s, boolean fallback) {
        if (s == null) return fallback;
        return Boolean.parseBoolean(s.trim());
    }

    private int parseInt(String s, int fallback) {
        try { return Integer.parseInt(s == null ? "" : s.trim()); } catch (Exception ignored) { return fallback; }
    }

    private VpnCheckResult requestIpApi(String ip) {
        HttpURLConnection conn = null;
        try {
            String url = "http://ip-api.com/json/" + ip + "?fields=status,proxy,hosting";
            conn = (HttpURLConnection) new URL(url).openConnection();
            conn.setRequestMethod("GET");
            conn.setConnectTimeout(vpnTimeoutMs);
            conn.setReadTimeout(vpnTimeoutMs);
            conn.setRequestProperty("User-Agent", "StatusAPI-AntiBot/1.0");
            if (conn.getResponseCode() < 200 || conn.getResponseCode() >= 300) return null;
            StringBuilder sb = new StringBuilder();
            try (BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
                String line; while ((line = br.readLine()) != null) sb.append(line);
            }
            String json = sb.toString();
            if (json.isEmpty() || !json.contains("\"status\":\"success\"")) return null;
            VpnCheckResult result = new VpnCheckResult();
            result.proxy   = json.contains("\"proxy\":true");
            result.hosting = json.contains("\"hosting\":true");
            return result;
        } catch (Exception ignored) { return null; }
        finally { if (conn != null) conn.disconnect(); }
    }

    // --- Interne Klassen ---

    private static class IpWindow {
        long windowStart; int count;
        IpWindow(long now) { this.windowStart = now; this.count = 0; }
    }

    private static class VpnCacheEntry { VpnCheckResult result; long expiresAt; }
    private static class VpnCheckResult { boolean proxy; boolean hosting; }

    private static class LearningProfile {
        long lastConnectionAt, lastScoreUpdateAt, lastSeenAt;
        int rapidStreak, score;
        LearningProfile(long now) { lastConnectionAt = lastScoreUpdateAt = lastSeenAt = now; }
    }

    private static class RecentPlayerIdentity { String playerName; String playerUuid; long updatedAtMs; }

    // --- Command ---

    private class AntiBotCommand extends Command {
        AntiBotCommand() { super("antibot", "statusapi.antibot"); }

        @Override
        public void execute(CommandSender sender, String[] args) {
            if (!enabled) { sender.sendMessage(ChatColor.RED + "AntiBotModule ist deaktiviert."); return; }

            if (args.length == 0 || "status".equalsIgnoreCase(args[0])) {
                sender.sendMessage(ChatColor.GOLD + "----- AntiBot Status -----");
                sender.sendMessage(ChatColor.YELLOW + "Schutz aktiv: " + ChatColor.WHITE + enabled);
                sender.sendMessage(ChatColor.YELLOW + "Profil: " + ChatColor.WHITE + profile);
                if (attackMode) sender.sendMessage(ChatColor.YELLOW + "Attack Mode: " + ChatColor.RED + "AKTIV");
                else            sender.sendMessage(ChatColor.YELLOW + "Attack Mode: " + ChatColor.GREEN + "Normal");
                sender.sendMessage(ChatColor.YELLOW + "CPS: " + ChatColor.WHITE + lastCps + ChatColor.GRAY + " (Peak " + peakCps.get() + ")");
                sender.sendMessage(ChatColor.YELLOW + "Schwellen: " + ChatColor.WHITE + "start " + attackStartCps + ChatColor.GRAY + " / " + ChatColor.WHITE + "stop " + attackStopCps + ChatColor.GRAY + " CPS");
                sender.sendMessage(ChatColor.YELLOW + "Active IP Blocks: " + ChatColor.WHITE + blockedIpsUntil.size());
                sender.sendMessage(ChatColor.YELLOW + "Total blocked connections: " + ChatColor.WHITE + blockedConnectionsTotal.get());
                sender.sendMessage(ChatColor.YELLOW + "VPN Check: " + ChatColor.WHITE + vpnCheckEnabled);
                sender.sendMessage(ChatColor.YELLOW + "Learning Mode: " + ChatColor.WHITE + learningModeEnabled
                        + ChatColor.GRAY + " (threshold=" + learningScoreThreshold + ")");
                List<String> recent = getRecentLearningEvents(3);
                if (!recent.isEmpty()) {
                    sender.sendMessage(ChatColor.YELLOW + "Learning Events:");
                    for (String line : recent) sender.sendMessage(ChatColor.GRAY + "- " + line);
                }
                return;
            }

            if ("clearblocks".equalsIgnoreCase(args[0])) {
                blockedIpsUntil.clear();
                sender.sendMessage(ChatColor.GREEN + "Alle IP-Blocks wurden entfernt.");
                return;
            }

            if ("unblock".equalsIgnoreCase(args[0]) && args.length >= 2) {
                String ip = args[1].trim();
                if (blockedIpsUntil.remove(ip) != null) sender.sendMessage(ChatColor.GREEN + "IP entblockt: " + ip);
                else                                    sender.sendMessage(ChatColor.RED + "IP war nicht geblockt: " + ip);
                return;
            }

            if ("profile".equalsIgnoreCase(args[0])) {
                if (args.length < 2) {
                    sender.sendMessage(ChatColor.YELLOW + "Aktuelles Profil: " + ChatColor.WHITE + profile);
                    sender.sendMessage(ChatColor.YELLOW + "Benutzung: /antibot profile <strict|high-traffic>");
                    return;
                }
                String requested = args[1].trim().toLowerCase(Locale.ROOT);
                if (!isSupportedProfile(requested)) { sender.sendMessage(ChatColor.RED + "Unbekanntes Profil. Erlaubt: strict, high-traffic"); return; }
                boolean ok = applyProfileAndPersist(requested);
                if (!ok) { sender.sendMessage(ChatColor.RED + "Profil konnte nicht gespeichert werden."); return; }
                sender.sendMessage(ChatColor.GREEN + "AntiBot-Profil umgestellt auf: " + requested);
                sender.sendMessage(ChatColor.GRAY + "Werte wurden in " + CONFIG_FILE_NAME + " gespeichert.");
                return;
            }

            if ("reload".equalsIgnoreCase(args[0])) {
                reloadRuntimeState();
                sender.sendMessage(ChatColor.GREEN + "AntiBot-Konfiguration neu geladen.");
                sender.sendMessage(ChatColor.GRAY + "Aktives Profil: " + profile);
                return;
            }

            sender.sendMessage(ChatColor.YELLOW + "/antibot status");
            sender.sendMessage(ChatColor.YELLOW + "/antibot clearblocks");
            sender.sendMessage(ChatColor.YELLOW + "/antibot unblock <ip>");
            sender.sendMessage(ChatColor.YELLOW + "/antibot profile <strict|high-traffic>");
            sender.sendMessage(ChatColor.YELLOW + "/antibot reload");
        }
    }
}