package net.viper.backendguard;

import org.bukkit.ChatColor;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.AsyncPlayerPreLoginEvent;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.scheduler.BukkitTask;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;

public class BackendJoinGuardPlugin extends JavaPlugin implements Listener {

    private boolean enforcementEnabled;
    private boolean logBlockedAttempts;
    private String kickMessage;
    private final Set<String> allowedExactIps = new HashSet<String>();
    private final List<SubnetRule> allowedCidrs = new ArrayList<SubnetRule>();
    private boolean statusApiSyncEnabled;
    private String statusApiBaseUrl;
    private String statusApiEndpointPath;
    private String statusApiApiKey;
    private int statusApiIntervalSeconds;
    private boolean statusApiLogSyncErrors;
    private BukkitTask syncTask;

    @Override
    public void onEnable() {
        saveDefaultConfig();
        reloadGuardConfig();
        getServer().getPluginManager().registerEvents(this, this);
        getLogger().info("BackendJoinGuard aktiviert. Erlaubte Proxy-IPs=" + allowedExactIps.size() + ", CIDRs=" + allowedCidrs.size() + ", StatusAPI-Sync=" + statusApiSyncEnabled);
    }

    @Override
    public void onDisable() {
        cancelSyncTask();
    }

    @EventHandler(priority = EventPriority.HIGHEST)
    public void onAsyncPlayerPreLogin(AsyncPlayerPreLoginEvent event) {
        if (!enforcementEnabled) {
            return;
        }

        InetAddress address = event.getAddress();
        if (address == null) {
            return;
        }

        if (isAllowed(address)) {
            return;
        }

        event.disallow(AsyncPlayerPreLoginEvent.Result.KICK_OTHER, colorize(kickMessage));
        if (logBlockedAttempts) {
            getLogger().warning("Direktjoin blockiert: player=" + event.getName() + ", ip=" + address.getHostAddress());
        }
    }

    @Override
    public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
        if (!"backendguard".equalsIgnoreCase(command.getName())) {
            return false;
        }

        if (!sender.hasPermission("backendguard.admin")) {
            sender.sendMessage(colorize("&cDafuer hast du keine Rechte."));
            return true;
        }

        if (args.length == 1 && "reload".equalsIgnoreCase(args[0])) {
            reloadConfig();
            reloadGuardConfig();
            if (statusApiSyncEnabled) {
                syncFromStatusApi(true);
                sender.sendMessage(colorize("&aBackendJoinGuard neu geladen. StatusAPI-Sync aktiv."));
            } else {
                sender.sendMessage(colorize("&aBackendJoinGuard neu geladen. Standalone-Modus aktiv."));
            }
            return true;
        }

        sender.sendMessage(colorize("&e/backendguard reload"));
        return true;
    }

    private void reloadGuardConfig() {
        FileConfiguration config = getConfig();
        enforcementEnabled = config.getBoolean("enforcement-enabled", true);
        logBlockedAttempts = config.getBoolean("log-blocked-attempts", true);
        kickMessage = config.getString("kick-message", "&cBitte verbinde dich nur ueber den Proxy.");

        allowedExactIps.clear();
        allowedCidrs.clear();

        for (String entry : config.getStringList("allowed-proxy-ips")) {
            String normalized = normalizeIp(entry);
            if (normalized != null) {
                allowedExactIps.add(normalized);
            }
        }

        for (String entry : config.getStringList("allowed-proxy-cidrs")) {
            SubnetRule rule = parseSubnet(entry);
            if (rule != null) {
                allowedCidrs.add(rule);
            }
        }

        statusApiSyncEnabled = config.getBoolean("statusapi-sync.enabled", false);
        statusApiBaseUrl = config.getString("statusapi-sync.base-url", "http://127.0.0.1:9191");
        statusApiEndpointPath = config.getString("statusapi-sync.endpoint-path", "/network/backendguard/config");
        statusApiApiKey = config.getString("statusapi-sync.api-key", "");
        statusApiIntervalSeconds = Math.max(10, config.getInt("statusapi-sync.interval-seconds", 60));
        statusApiLogSyncErrors = config.getBoolean("statusapi-sync.log-sync-errors", true);

        cancelSyncTask();
        if (statusApiSyncEnabled) {
            // Erst lokale Werte laden, dann zentral ueberschreiben falls Sync klappt.
            syncFromStatusApi(false);
            syncTask = getServer().getScheduler().runTaskTimerAsynchronously(
                    this,
                    () -> syncFromStatusApi(false),
                    statusApiIntervalSeconds * 20L,
                    statusApiIntervalSeconds * 20L
            );
        }
    }

    private void cancelSyncTask() {
        if (syncTask != null) {
            syncTask.cancel();
            syncTask = null;
        }
    }

    private void syncFromStatusApi(boolean manualTrigger) {
        if (!statusApiSyncEnabled) {
            return;
        }

        HttpURLConnection conn = null;
        try {
            URL url = new URL(buildSyncUrl());
            conn = (HttpURLConnection) url.openConnection();
            conn.setRequestMethod("GET");
            conn.setConnectTimeout(4000);
            conn.setReadTimeout(6000);
            conn.setRequestProperty("Accept", "application/json");
            if (statusApiApiKey != null && !statusApiApiKey.trim().isEmpty()) {
                conn.setRequestProperty("x-api-key", statusApiApiKey.trim());
            }

            int code = conn.getResponseCode();
            if (code < 200 || code >= 300) {
                if (statusApiLogSyncErrors || manualTrigger) {
                    getLogger().warning("StatusAPI-Sync fehlgeschlagen (HTTP " + code + "). Nutze lokale Guard-Werte.");
                }
                return;
            }

            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();
            Boolean remoteEnforcement = extractJsonBoolean(json, "enforcement_enabled");
            Boolean remoteLogBlocked = extractJsonBoolean(json, "log_blocked_attempts");
            String remoteKickMessage = extractJsonString(json, "kick_message");
            List<String> remoteIps = extractJsonStringArray(json, "allowed_proxy_ips");
            List<String> remoteCidrs = extractJsonStringArray(json, "allowed_proxy_cidrs");

            if (remoteEnforcement != null) {
                enforcementEnabled = remoteEnforcement;
            }
            if (remoteLogBlocked != null) {
                logBlockedAttempts = remoteLogBlocked;
            }
            if (remoteKickMessage != null && !remoteKickMessage.trim().isEmpty()) {
                kickMessage = remoteKickMessage;
            }

            if (remoteIps != null) {
                allowedExactIps.clear();
                for (String ip : remoteIps) {
                    String normalized = normalizeIp(ip);
                    if (normalized != null) {
                        allowedExactIps.add(normalized);
                    }
                }
            }

            if (remoteCidrs != null) {
                allowedCidrs.clear();
                for (String cidr : remoteCidrs) {
                    SubnetRule rule = parseSubnet(cidr);
                    if (rule != null) {
                        allowedCidrs.add(rule);
                    }
                }
            }

            if (manualTrigger) {
                getLogger().info("StatusAPI-Sync erfolgreich. Erlaubte Proxy-IPs=" + allowedExactIps.size() + ", CIDRs=" + allowedCidrs.size());
            }
        } catch (Exception e) {
            if (statusApiLogSyncErrors || manualTrigger) {
                getLogger().warning("StatusAPI-Sync Fehler: " + e.getMessage() + ". Nutze lokale Guard-Werte.");
            }
        } finally {
            if (conn != null) {
                conn.disconnect();
            }
        }
    }

    private String buildSyncUrl() {
        String base = statusApiBaseUrl == null ? "" : statusApiBaseUrl.trim();
        String path = statusApiEndpointPath == null ? "" : statusApiEndpointPath.trim();

        if (base.endsWith("/") && path.startsWith("/")) {
            return base.substring(0, base.length() - 1) + path;
        }
        if (!base.endsWith("/") && !path.startsWith("/")) {
            return base + "/" + path;
        }
        return base + path;
    }

    private Boolean extractJsonBoolean(String json, String key) {
        String token = extractJsonToken(json, key);
        if (token == null) {
            return null;
        }
        if ("true".equalsIgnoreCase(token)) {
            return Boolean.TRUE;
        }
        if ("false".equalsIgnoreCase(token)) {
            return Boolean.FALSE;
        }
        return null;
    }

    private String extractJsonString(String json, String key) {
        if (json == null || key == null) {
            return null;
        }
        String search = "\"" + key + "\"";
        int idx = json.indexOf(search);
        if (idx < 0) {
            return null;
        }
        int colon = json.indexOf(':', idx + search.length());
        if (colon < 0) {
            return null;
        }
        int i = colon + 1;
        while (i < json.length() && Character.isWhitespace(json.charAt(i))) {
            i++;
        }
        if (i >= json.length() || json.charAt(i) != '"') {
            return null;
        }

        i++;
        StringBuilder sb = new StringBuilder();
        boolean escape = false;
        while (i < json.length()) {
            char ch = json.charAt(i++);
            if (escape) {
                if (ch == 'n') {
                    sb.append('\n');
                } else if (ch == 'r') {
                    sb.append('\r');
                } else {
                    sb.append(ch);
                }
                escape = false;
                continue;
            }
            if (ch == '\\') {
                escape = true;
                continue;
            }
            if (ch == '"') {
                break;
            }
            sb.append(ch);
        }
        return sb.toString();
    }

    private String extractJsonToken(String json, String key) {
        if (json == null || key == null) {
            return null;
        }
        String search = "\"" + key + "\"";
        int idx = json.indexOf(search);
        if (idx < 0) {
            return null;
        }
        int colon = json.indexOf(':', idx + search.length());
        if (colon < 0) {
            return null;
        }
        int i = colon + 1;
        while (i < json.length() && Character.isWhitespace(json.charAt(i))) {
            i++;
        }
        if (i >= json.length()) {
            return null;
        }

        StringBuilder sb = new StringBuilder();
        while (i < json.length()) {
            char ch = json.charAt(i);
            if (ch == ',' || ch == '}' || ch == ']' || Character.isWhitespace(ch)) {
                break;
            }
            sb.append(ch);
            i++;
        }
        return sb.toString().trim();
    }

    private List<String> extractJsonStringArray(String json, String key) {
        if (json == null || key == null) {
            return null;
        }

        String search = "\"" + key + "\"";
        int idx = json.indexOf(search);
        if (idx < 0) {
            return null;
        }

        int colon = json.indexOf(':', idx + search.length());
        if (colon < 0) {
            return null;
        }

        int start = json.indexOf('[', colon + 1);
        if (start < 0) {
            return null;
        }

        int depth = 0;
        int end = -1;
        for (int i = start; i < json.length(); i++) {
            char ch = json.charAt(i);
            if (ch == '[') {
                depth++;
            } else if (ch == ']') {
                depth--;
                if (depth == 0) {
                    end = i;
                    break;
                }
            }
        }

        if (end < 0 || end <= start) {
            return null;
        }

        String raw = json.substring(start + 1, end);
        List<String> out = new ArrayList<String>();
        boolean inString = false;
        boolean escape = false;
        StringBuilder current = new StringBuilder();

        for (int i = 0; i < raw.length(); i++) {
            char ch = raw.charAt(i);
            if (!inString) {
                if (ch == '"') {
                    inString = true;
                    current.setLength(0);
                }
                continue;
            }

            if (escape) {
                if (ch == 'n') {
                    current.append('\n');
                } else if (ch == 'r') {
                    current.append('\r');
                } else {
                    current.append(ch);
                }
                escape = false;
                continue;
            }

            if (ch == '\\') {
                escape = true;
                continue;
            }

            if (ch == '"') {
                inString = false;
                out.add(current.toString());
                continue;
            }

            current.append(ch);
        }

        return out;
    }

    private boolean isAllowed(InetAddress address) {
        String normalized = normalizeIp(address.getHostAddress());
        if (normalized != null && allowedExactIps.contains(normalized)) {
            return true;
        }

        for (SubnetRule rule : allowedCidrs) {
            if (rule.matches(address)) {
                return true;
            }
        }
        return false;
    }

    private String normalizeIp(String raw) {
        if (raw == null || raw.trim().isEmpty()) {
            return null;
        }
        try {
            return InetAddress.getByName(raw.trim()).getHostAddress().toLowerCase(Locale.ROOT);
        } catch (UnknownHostException e) {
            getLogger().warning("Ungueltige IP in Config ignoriert: " + raw);
            return null;
        }
    }

    private SubnetRule parseSubnet(String raw) {
        if (raw == null || raw.trim().isEmpty() || !raw.contains("/")) {
            return null;
        }

        String[] parts = raw.trim().split("/", 2);
        try {
            InetAddress network = InetAddress.getByName(parts[0]);
            int prefix = Integer.parseInt(parts[1]);
            return new SubnetRule(network, prefix);
        } catch (Exception e) {
            getLogger().warning("Ungueltige CIDR in Config ignoriert: " + raw);
            return null;
        }
    }

    private String colorize(String text) {
        return ChatColor.translateAlternateColorCodes('&', text == null ? "" : text);
    }

    private static final class SubnetRule {
        private final byte[] networkBytes;
        private final int prefixLength;

        private SubnetRule(InetAddress network, int prefixLength) {
            this.networkBytes = network.getAddress();
            this.prefixLength = prefixLength;
        }

        private boolean matches(InetAddress address) {
            byte[] candidate = address.getAddress();
            if (candidate.length != networkBytes.length) {
                return false;
            }

            int fullBytes = prefixLength / 8;
            int remainderBits = prefixLength % 8;

            for (int i = 0; i < fullBytes; i++) {
                if (candidate[i] != networkBytes[i]) {
                    return false;
                }
            }

            if (remainderBits == 0) {
                return true;
            }

            int mask = (-1) << (8 - remainderBits);
            return (candidate[fullBytes] & mask) == (networkBytes[fullBytes] & mask);
        }
    }
}
