package me.sucixr.kitin.config;

import com.google.common.base.Throwables;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.tags.TagKey;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.item.Item;
import net.minecraft.world.level.block.Block;
import org.bukkit.Bukkit;
import org.bukkit.NamespacedKey;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.InvalidConfigurationException;
import org.bukkit.configuration.file.YamlConfiguration;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.*;
import java.util.logging.Level;

/**
 * Kitin network-optimization configuration, ported into Neko Core.
 *
 * <p>Self-contained Bukkit-YAML config (config/Network.yml) with reflective per-module loaders,
 * kept independent from Luminol's IConfigModule/TOML system so the ported Kitin network classes
 * can reference {@code KitinConfig.x} unchanged. Adapted for MC 1.21.8: Identifier -> ResourceLocation.
 */
public class KitinConfig {

    private static final org.slf4j.Logger LOGGER = org.slf4j.LoggerFactory.getLogger("NekoCore/Network");

    private static final String CONFIG_PATH = "config/Network.yml";

    public static void load() {
        try {
            init(new File(CONFIG_PATH));
        } catch (Exception e) {
            LOGGER.error("Failed to load Network configuration", e);
        }
    }

    private static final String HEADER = "Neko Core Network Optimizations\n" +
            "Ported from Kitin (https://github.com/SucIXR/Kitin).\n" +
            "See readme/kitin-yml.md for option meanings.";

    private static File CONFIG_FILE;
    public static YamlConfiguration config;

    public static int version;
    private static boolean isReloading = false;

    private static void loadYaml() {
        if (CONFIG_FILE == null) CONFIG_FILE = new File(CONFIG_PATH);
        config = new YamlConfiguration();
        try {
            config.load(CONFIG_FILE);
        } catch (IOException ignore) {
        } catch (InvalidConfigurationException ex) {
            Bukkit.getLogger().log(Level.SEVERE, "Could not load Network.yml", ex);
            throw Throwables.propagate(ex);
        }
        config.options().header(HEADER);
        config.options().copyDefaults(true);
    }

    public static void init(File configFile) {
        CONFIG_FILE = configFile;
        loadYaml();
        version = getInt("config-version", 1);
        set("config-version", 1);
        readConfig(KitinConfig.class, null);
    }

    public static String reload(String module) {
        isReloading = true;
        loadYaml();
        String target = module.toLowerCase(Locale.ROOT);
        if (target.equals("all")) {
            readConfig(KitinConfig.class, null);
            isReloading = false;
            return "All configurations";
        }
        boolean found = false;
        try {
            for (Method method : KitinConfig.class.getDeclaredMethods()) {
                if (Modifier.isPrivate(method.getModifiers()) && method.getParameterCount() == 0) {
                    String methodName = method.getName().toLowerCase(Locale.ROOT);
                    if (methodName.contains(target) && methodName.endsWith("settings")) {
                        method.setAccessible(true);
                        method.invoke(null);
                        found = true;
                        break;
                    }
                }
            }
        } catch (Exception ex) {
            isReloading = false;
            throw new RuntimeException(ex);
        }
        isReloading = false;
        if (found) return "Module '" + module + "'";
        throw new IllegalArgumentException("Unknown module: " + module);
    }

    public static Set<String> getReloadableModules() {
        Set<String> modules = new HashSet<>();
        modules.add("all");
        for (Method method : KitinConfig.class.getDeclaredMethods()) {
            if (method.getName().endsWith("Settings")) {
                modules.add(method.getName().replace("Settings", "").toLowerCase(Locale.ROOT));
            }
        }
        return modules;
    }

    static void readConfig(Class<?> clazz, Object instance) {
        for (Method method : clazz.getDeclaredMethods()) {
            if (Modifier.isPrivate(method.getModifiers())) {
                if (method.getParameterTypes().length == 0 && method.getReturnType() == Void.TYPE) {
                    if (!method.getName().endsWith("Settings")) {
                        continue;
                    }
                    try {
                        method.setAccessible(true);
                        method.invoke(instance);
                    } catch (InvocationTargetException ex) {
                        throw Throwables.propagate(ex.getCause());
                    } catch (Exception ex) {
                        Bukkit.getLogger().log(Level.SEVERE, "Error invoking " + method, ex);
                    }
                }
            }
        }
        try {
            config.save(CONFIG_FILE);
        } catch (IOException ex) {
            Bukkit.getLogger().log(Level.SEVERE, "Could not save " + CONFIG_FILE, ex);
        }
    }

    private static void set(String path, Object val) {
        config.addDefault(path, val);
        config.set(path, val);
    }

    private static boolean getBoolean(String path, boolean def) {
        config.addDefault(path, def);
        return config.getBoolean(path, config.getBoolean(path));
    }

    private static int getInt(String path, int def) {
        config.addDefault(path, def);
        return config.getInt(path, config.getInt(path));
    }

    private static double getDouble(String path, double def) {
        config.addDefault(path, def);
        return config.getDouble(path, config.getDouble(path));
    }

    private static String getString(String path, String def) {
        config.addDefault(path, def);
        return config.getString(path, config.getString(path));
    }

    private static List<String> getList(String path, List<String> def) {
        config.addDefault(path, def);
        return config.getStringList(path);
    }

    private static <T> List<T> getListFromSection(String path, java.util.function.BiFunction<String, ConfigurationSection, T> mapper) {
        ConfigurationSection section = config.getConfigurationSection(path);
        if (section == null) {
            config.addDefault(path, new HashMap<>());
            return new ArrayList<>();
        }
        List<T> result = new ArrayList<>();
        for (String key : section.getKeys(false)) {
            ConfigurationSection subSection = section.getConfigurationSection(key);
            if (subSection != null) {
                T obj = mapper.apply(key, subSection);
                if (obj != null) result.add(obj);
            }
        }
        return result;
    }

    private static <T> Set<T> resolveRegistry(String key, net.minecraft.core.Registry<T> registry, net.minecraft.resources.ResourceKey<net.minecraft.core.Registry<T>> registryKey) {
        Set<T> result = new HashSet<>();
        try {
            if (key.contains("*")) {
                String match = key.replace("*", "");
                for (T obj : registry) {
                    ResourceLocation id = registry.getKey(obj);
                    if (id != null && id.getPath().contains(match)) {
                        result.add(obj);
                    }
                }
                return result;
            }
            if (key.startsWith("#")) {
                String tagString = key.substring(1);
                NamespacedKey nsKey = key.contains(":") ? NamespacedKey.fromString(tagString) : NamespacedKey.minecraft(tagString);
                if (nsKey != null) {
                    ResourceLocation tagId = ResourceLocation.tryParse(nsKey.toString());
                    if (tagId != null) {
                        TagKey<T> tagKey = TagKey.create(registryKey, tagId);
                        registry.getTagOrEmpty(tagKey).forEach(holder -> result.add(holder.value()));
                    }
                }
                return result;
            }
            NamespacedKey nsKey = key.contains(":") ? NamespacedKey.fromString(key) : NamespacedKey.minecraft(key.toLowerCase(Locale.ROOT));
            if (nsKey != null) {
                ResourceLocation nmsId = ResourceLocation.tryParse(nsKey.toString());
                if (nmsId != null) {
                    registry.getOptional(nmsId).ifPresent(result::add);
                }
            }
        } catch (Exception e) {
            Bukkit.getLogger().warning("[Kitin] Parsing error (" + key + ")");
        }
        return result;
    }

    private static Set<EntityType<?>> getEntityType(String key) {
        return resolveRegistry(key, BuiltInRegistries.ENTITY_TYPE, Registries.ENTITY_TYPE);
    }
    private static Set<Item> getItemType(String key) {
        return resolveRegistry(key, BuiltInRegistries.ITEM, Registries.ITEM);
    }
    private static Set<Block> getBlockType(String key) {
        return resolveRegistry(key, BuiltInRegistries.BLOCK, Registries.BLOCK);
    }

    // ===================================================================
    // Network module fields (added incrementally as features are ported)
    // ===================================================================

    public static volatile boolean chunkLazyLoading = true;

    // reduce-high-frequency-entity-sync-packets
    public static volatile boolean optimizeAllBoats = false;
    public static volatile boolean optimizeAllMinecarts = false;
    public static volatile Set<EntityType<?>> optimizedSyncEntities = Collections.emptySet();

    // particle throttling and packing
    public static volatile int playerMaxParticlesPerPacket = 250;
    public static volatile int playerMaxBufferSize = 5000;
    public static volatile int playerMinOptimizeThreshold = 50;
    public static volatile int globalMaxDelayTicks = 10;
    public static volatile int globalMaxPacketParticlesPerTick = 499;

    // global chunk send rate limiting + QoS
    public static volatile int globalMaxChunkSendRate = -1;
    public static volatile double globalChunkSendBurstFactor = 0.05;
    public static boolean useVanillaChunkSender = false; // controls 0004 throttling + 0024 ACK sender

    // multi-port listening
    public static class ListenerConfig {
        public final String name;
        public final String bindAddress;
        public final int port;
        public final boolean proxyProtocol;
        public ListenerConfig(String name, String bindAddress, int port, boolean proxyProtocol) {
            this.name = name;
            this.bindAddress = bindAddress;
            this.port = port;
            this.proxyProtocol = proxyProtocol;
        }
    }
    public static volatile List<ListenerConfig> extraListeners = Collections.emptyList();

    private static void networkSettings() {
        chunkLazyLoading = getBoolean("network.chunk-lazy-loading", true);

        // reduce-high-frequency-entity-sync-packets (0005)
        optimizeAllBoats = false;
        optimizeAllMinecarts = false;
        Set<EntityType<?>> tempEntities = new HashSet<>();
        List<String> defaultEntities = new ArrayList<>();
        defaultEntities.add("$AbstractMinecart");
        defaultEntities.add("$AbstractBoat");
        defaultEntities.add("shulker");
        List<String> syncList = getList("network.reduce-high-frequency-entity-sync-packets.entitys", defaultEntities);
        for (String key : syncList) {
            if (key.equalsIgnoreCase("$AbstractMinecart")) { optimizeAllMinecarts = true; continue; }
            if (key.equalsIgnoreCase("$AbstractBoat")) { optimizeAllBoats = true; continue; }
            tempEntities.addAll(getEntityType(key));
        }
        optimizedSyncEntities = Set.copyOf(tempEntities);

        // particle throttling and packing (0018)
        playerMaxParticlesPerPacket = getInt("network.particle.player-max-particles-per-packet", playerMaxParticlesPerPacket);
        playerMinOptimizeThreshold = getInt("network.particle.player-min-optimize-threshold", playerMinOptimizeThreshold);
        globalMaxDelayTicks = getInt("network.particle.global-max-delay-ticks", globalMaxDelayTicks);
        globalMaxPacketParticlesPerTick = getInt("network.particle.global-max-packet-particles-per-tick", globalMaxPacketParticlesPerTick);

        // chunk-send: vanilla sender toggle (0004 throttling / 0024 ACK sender)
        useVanillaChunkSender = getBoolean("network.chunk-send.use-vanilla-chunk-sender", false);

        // global chunk send rate + QoS (0015)
        globalMaxChunkSendRate = getInt("network.chunk-send.global-max-chunk-send-rate", globalMaxChunkSendRate);
        globalChunkSendBurstFactor = getDouble("network.chunk-send.global-chunk-send-burst-factor", globalChunkSendBurstFactor);

        List<me.sucixr.kitin.network.qos.GlobalChunkLimiter.GroupRule> qosRules = getListFromSection(
                "network.chunk-send.qos-groups", (key, section) -> {
            double rate = section.getDouble("rate", -1.0);
            String bindAddress = section.getString("bind-address", null);
            String upstream = section.getString("upstream", null);
            String virtualHost = section.getString("virtual-host", null);

            java.util.function.Predicate<net.minecraft.server.level.ServerPlayer> matcher = p -> {
                if (p.connection == null || p.connection.connection == null) return false;
                net.minecraft.network.Connection conn = p.connection.connection;
                if (virtualHost != null && !virtualHost.isEmpty()) {
                    if (conn.virtualHost != null) {
                        if (!conn.virtualHost.getHostString().equalsIgnoreCase(virtualHost)) return false;
                    } else if (conn.hostname != null && !conn.hostname.isEmpty()) {
                        String h = conn.hostname;
                        if (h.contains(":")) h = h.split(":")[0];
                        if (!h.equalsIgnoreCase(virtualHost)) return false;
                    } else {
                        return false;
                    }
                }
                if (bindAddress != null && !bindAddress.isEmpty()) {
                    if (conn.channel == null) return false;
                    java.net.SocketAddress sa = conn.channel.localAddress();
                    if (sa instanceof java.net.InetSocketAddress inet) {
                        if (!inet.getAddress().getHostAddress().equals(bindAddress)) return false;
                    } else {
                        return false;
                    }
                }
                return true;
            };
            return new me.sucixr.kitin.network.qos.GlobalChunkLimiter.GroupRule(key, rate, upstream, virtualHost, matcher);
        });
        // default fallback group
        qosRules.add(new me.sucixr.kitin.network.qos.GlobalChunkLimiter.GroupRule(
                me.sucixr.kitin.network.qos.GlobalChunkLimiter.DEFAULT_GROUP, globalMaxChunkSendRate, null, null, p -> true));
        me.sucixr.kitin.network.qos.GlobalChunkLimiter.reload(qosRules);

        // multi-port listening (0020) - not hot-reloadable (sockets bound at startup)
        if (!isReloading) {
            extraListeners = getListFromSection("network.extra-listeners", (key, section) -> {
                int port = section.getInt("port", -1);
                if (port < 0 || port > 65535) return null;
                String bindAddress = section.getString("bind-address", "0.0.0.0");
                boolean proxyProtocol = section.getBoolean("proxy-protocol", false);
                return new ListenerConfig(key, bindAddress, port, proxyProtocol);
            });
        }
    }
}
