-- Sliding window quota grant script (the fairness partial is prepended by RuleExecutor).
-- KEYS[1]: quota key (sorted set of grant timestamps)
-- KEYS[2]: waiters key (sorted set of waiters, see partials/fairness.lua)
-- ARGV[1]: window duration (ms)
-- ARGV[2]: limit
-- ARGV[3]: request uuid (member added to the window on grant)
-- ARGV[4]: quota key TTL (seconds)
-- ARGV[5]: waiterId ('' for a fresh, non-waiting caller)
-- ARGV[6]: maxWait (ms) of the caller
-- ARGV[7]: mayWait (1 = register as waiter on denial, 0 = never)
-- Returns: {granted (0/1), currentCount, retryIn (ms until a slot frees, -1 = no hint), grantedTs}
--   grantedTs is the Redis-time timestamp stored with the grant; the caller needs it for rollback.

local window = tonumber(ARGV[1])
local limit = tonumber(ARGV[2])
local now = now_ms()

-- Remove entries that have left the window
redis.call('ZREMRANGEBYSCORE', KEYS[1], 0, now - window)

local currentCount = redis.call('ZCARD', KEYS[1])
local free = limit - currentCount
local ahead = waiters_ahead(KEYS[2], ARGV[5], now)

if ahead < free then
    redis.call('ZADD', KEYS[1], now, ARGV[3])
    redis.call('EXPIRE', KEYS[1], ARGV[4])
    deregister_waiter(KEYS[2], ARGV[5], ARGV[6])
    return { 1, currentCount + 1, -1, now }
end

if tonumber(ARGV[7]) == 1 then
    register_waiter(KEYS[2], ARGV[5], ARGV[6])
end

-- The caller needs (ahead + 1 - free) more slots; slots free when window entries expire, oldest
-- first, so the entry at that index tells the caller exactly when to retry. Clamped to the newest
-- entry when there are more waiters than window entries (a conservative early wake).
local retryIn = -1
if currentCount > 0 then
    local idx = math.min(math.max(ahead + 1 - free, 1) - 1, currentCount - 1)
    local entry = redis.call('ZRANGE', KEYS[1], idx, idx, 'WITHSCORES')
    retryIn = math.max(math.ceil(tonumber(entry[2]) + window - now), 1)
end

return { 0, currentCount, retryIn, 0 }
