package net.viper.status.ratelimit;

import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Gemeinsames Rate-Limit-Framework fuer mehrere Module.
 */
public final class GlobalRateLimitFramework {

    private static final GlobalRateLimitFramework INSTANCE = new GlobalRateLimitFramework();

    private final Map<String, Bucket> buckets = new ConcurrentHashMap<String, Bucket>();

    private GlobalRateLimitFramework() {
    }

    public static GlobalRateLimitFramework getInstance() {
        return INSTANCE;
    }

    public Result check(String scope, String actorId, Rule rule) {
        return check(scope, actorId, rule, System.currentTimeMillis());
    }

    public Result check(String scope, String actorId, Rule rule, long now) {
        if (rule == null || !rule.enabled || scope == null || scope.isEmpty() || actorId == null || actorId.isEmpty()) {
            return Result.allowed(0, 0L);
        }

        String key = scope + ":" + actorId;
        Bucket bucket = buckets.computeIfAbsent(key, k -> new Bucket());

        synchronized (bucket) {
            if (bucket.blockedUntil > now) {
                return Result.blocked(Math.max(0L, bucket.blockedUntil - now), bucket.hits.size());
            }

            long minTs = now - Math.max(1L, rule.windowMs);
            while (!bucket.hits.isEmpty() && bucket.hits.peekFirst() < minTs) {
                bucket.hits.pollFirst();
            }

            bucket.hits.addLast(now);

            if (bucket.hits.size() > Math.max(1, rule.maxActions)) {
                long blockMs = Math.max(1L, rule.blockMs);
                bucket.blockedUntil = now + blockMs;
                return Result.blocked(blockMs, bucket.hits.size());
            }

            return Result.allowed(bucket.hits.size(), 0L);
        }
    }

    public void clearActor(String actorId) {
        if (actorId == null || actorId.isEmpty()) {
            return;
        }
        for (String key : buckets.keySet()) {
            if (key.endsWith(":" + actorId)) {
                buckets.remove(key);
            }
        }
    }

    public static final class Rule {
        public final boolean enabled;
        public final long windowMs;
        public final int maxActions;
        public final long blockMs;

        public Rule(boolean enabled, long windowMs, int maxActions, long blockMs) {
            this.enabled = enabled;
            this.windowMs = Math.max(1L, windowMs);
            this.maxActions = Math.max(1, maxActions);
            this.blockMs = Math.max(1L, blockMs);
        }
    }

    public static final class Result {
        private final boolean blocked;
        private final int currentHits;
        private final long remainingBlockMs;

        private Result(boolean blocked, int currentHits, long remainingBlockMs) {
            this.blocked = blocked;
            this.currentHits = Math.max(0, currentHits);
            this.remainingBlockMs = Math.max(0L, remainingBlockMs);
        }

        public static Result blocked(long remainingBlockMs, int currentHits) {
            return new Result(true, currentHits, remainingBlockMs);
        }

        public static Result allowed(int currentHits, long remainingBlockMs) {
            return new Result(false, currentHits, remainingBlockMs);
        }

        public boolean isBlocked() {
            return blocked;
        }

        public int getCurrentHits() {
            return currentHits;
        }

        public long getRemainingBlockMs() {
            return remainingBlockMs;
        }
    }

    private static final class Bucket {
        final Deque<Long> hits = new ArrayDeque<Long>();
        long blockedUntil = 0L;
    }
}