package com.tiedup.remake.network;

import com.mojang.logging.LogUtils;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import net.minecraft.server.level.ServerPlayer;
import org.slf4j.Logger;

/**
 * Rate limiter for network packets to prevent server abuse.
 *
 * <p>Uses the Token Bucket algorithm to limit packet rates per player.
 * Each player has a bucket of tokens that refills over time. Each packet
 * consumes one token. If the bucket is empty, the packet is rejected.
 *
 * <p>Phase: Server Protection & Performance
 *
 * <p>This prevents malicious clients from:
 * <ul>
 *   <li>Spamming packets to overload the server</li>
 *   <li>Creating lag for other players</li>
 *   <li>Exploiting game mechanics through rapid packet sending</li>
 * </ul>
 *
 * <p>Example usage in packet handlers:
 * <pre>{@code
 * public void handle(Supplier<NetworkEvent.Context> ctx) {
 *     ctx.get().enqueueWork(() -> {
 *         ServerPlayer player = ctx.get().getSender();
 *         if (player == null) return;
 *
 *         // Rate limit check
 *         if (!PacketRateLimiter.allowPacket(player, "struggle")) {
 *             return; // Packet rejected
 *         }
 *
 *         handleServer(player);
 *     });
 *     ctx.get().setPacketHandled(true);
 * }
 * }</pre>
 */
public class PacketRateLimiter {

    private static final Logger LOGGER = LogUtils.getLogger();

    /**
     * Map of player UUID -> packet type -> token bucket.
     * Concurrent to support multi-threaded packet handling.
     */
    private static final Map<UUID, Map<String, TokenBucket>> playerBuckets =
        new ConcurrentHashMap<>();

    /**
     * Rate limit configurations for different packet categories.
     *
     * <p>Categories:
     * <ul>
     *   <li><b>struggle</b>: Struggle keybind spam (5 tokens, 1/sec refill)</li>
     *   <li><b>minigame</b>: Minigame inputs like QTE or lockpick (20 tokens, 5/sec refill)</li>
     *   <li><b>action</b>: Player actions like tying/untying (10 tokens, 2/sec refill)</li>
     *   <li><b>selfbondage</b>: Self-bondage continuous packets (15 tokens, 6/sec refill)</li>
     *   <li><b>ui</b>: UI interactions like opening screens (3 tokens, 0.5/sec refill)</li>
     *   <li><b>default</b>: Fallback for uncategorized packets (10 tokens, 1/sec refill)</li>
     * </ul>
     */
    private static final Map<String, RateLimitConfig> configs = Map.of(
        "struggle",
        new RateLimitConfig(5, 1.0),
        "minigame",
        new RateLimitConfig(20, 5.0),
        "action",
        new RateLimitConfig(10, 2.0),
        "selfbondage",
        new RateLimitConfig(15, 6.0),
        "ui",
        new RateLimitConfig(3, 0.5),
        "default",
        new RateLimitConfig(10, 1.0)
    );

    /**
     * Check if a packet from a player should be allowed.
     *
     * <p>This method is thread-safe and can be called from packet handlers
     * running on different threads.
     *
     * @param player The player sending the packet
     * @param packetType The packet category (struggle, minigame, action, ui)
     * @return true if the packet should be processed, false if rate limited
     */
    public static boolean allowPacket(ServerPlayer player, String packetType) {
        if (player == null) {
            return false;
        }

        UUID playerId = player.getUUID();

        // Get or create the bucket for this player + packet type
        TokenBucket bucket = playerBuckets
            .computeIfAbsent(playerId, k -> new ConcurrentHashMap<>())
            .computeIfAbsent(packetType, k -> {
                RateLimitConfig config = configs.getOrDefault(
                    k,
                    configs.get("default")
                );
                return new TokenBucket(config);
            });

        boolean allowed = bucket.tryConsume();

        if (!allowed) {
            LOGGER.warn(
                "Rate limit exceeded for player '{}' on packet type '{}'. " +
                    "This may indicate packet spam or a malicious client.",
                player.getName().getString(),
                packetType
            );

            // Future enhancement: Track violations and kick/ban repeat offenders
            // For now, we just log and reject the packet
        }

        return allowed;
    }

    /**
     * Clean up rate limiter state for a disconnected player.
     *
     * <p>This should be called when a player logs out to prevent
     * the map from growing unbounded.
     *
     * @param playerId The UUID of the disconnected player
     */
    public static void cleanup(UUID playerId) {
        Map<String, TokenBucket> removed = playerBuckets.remove(playerId);
        if (removed != null) {
            LOGGER.debug(
                "Cleaned up rate limiter state for player: {}",
                playerId
            );
        }
    }

    /**
     * Get current statistics for a player (for debugging/monitoring).
     *
     * @param playerId The player UUID
     * @param packetType The packet type
     * @return String representation of bucket state, or null if not found
     */
    public static String getStats(UUID playerId, String packetType) {
        Map<String, TokenBucket> buckets = playerBuckets.get(playerId);
        if (buckets == null) {
            return null;
        }

        TokenBucket bucket = buckets.get(packetType);
        if (bucket == null) {
            return null;
        }

        return String.format(
            "Tokens: %.2f/%.0f (refill: %.1f/s)",
            bucket.getCurrentTokens(),
            bucket.maxTokens,
            bucket.refillRate
        );
    }

    /**
     * Token Bucket implementation for rate limiting.
     *
     * <p>The bucket starts full and refills over time. Each packet consumes
     * one token. If the bucket is empty, packets are rejected.
     *
     * <p>This allows for "bursts" of activity (using accumulated tokens)
     * while enforcing a long-term rate limit.
     */
    private static class TokenBucket {

        private final double maxTokens;
        private final double refillRate; // tokens per second
        private double tokens;
        private long lastRefillNanos;

        /**
         * Create a new token bucket.
         *
         * @param config The rate limit configuration
         */
        TokenBucket(RateLimitConfig config) {
            this.maxTokens = config.capacity;
            this.refillRate = config.refillRate;
            this.tokens = maxTokens; // Start full
            this.lastRefillNanos = System.nanoTime();
        }

        /**
         * Try to consume one token from the bucket.
         *
         * @return true if a token was consumed, false if bucket is empty
         */
        synchronized boolean tryConsume() {
            refill();

            if (tokens >= 1.0) {
                tokens -= 1.0;
                return true;
            }

            return false;
        }

        /**
         * Get current token count (for debugging).
         */
        synchronized double getCurrentTokens() {
            refill();
            return tokens;
        }

        /**
         * Refill the bucket based on elapsed time.
         */
        private void refill() {
            long now = System.nanoTime();
            double elapsedSeconds = (now - lastRefillNanos) / 1_000_000_000.0;

            // Add tokens based on elapsed time, capped at max capacity
            tokens = Math.min(maxTokens, tokens + elapsedSeconds * refillRate);
            lastRefillNanos = now;
        }
    }

    /**
     * Configuration for a rate limit category.
     *
     * @param capacity Maximum number of tokens (burst size)
     * @param refillRate Number of tokens added per second
     */
    private record RateLimitConfig(double capacity, double refillRate) {}
}
