--[[
  Move next job to be processed to active, lock it and fetch its data. The job
  may be delayed, in that case we need to move it to the delayed set instead.

  This operation guarantees that the worker owns the job during the locks
  expiration time. The worker is responsible of keeping the lock fresh
  so that no other worker picks this job again.

  Input:
      KEYS[1] wait key
      KEYS[2] active key
      KEYS[3] priority key
      KEYS[4] active event key
      KEYS[5] stalled key

      -- Rate limiting
      KEYS[6] rate limiter key
      KEYS[7] delayed key

      --
      KEYS[8] drained key

      ARGV[1] key prefix
      ARGV[2] lock token
      ARGV[3] lock duration in milliseconds
      ARGV[4] timestamp
      ARGV[5] optional jobid

      ARGV[6] optional jobs per time unit (rate limiter)
      ARGV[7] optional time unit (rate limiter)
      ARGV[8] optional do not do anything with job if rate limit hit
      ARGV[9] optional rate limit by key
]]

local rcall = redis.call

local rateLimit = function(jobId, maxJobs)
  local rateLimiterKey = KEYS[6];
  local limiterIndexTable = rateLimiterKey .. ":index"

  -- Rate limit by group?
  if(ARGV[9]) then
    local group = string.match(jobId, "[^:]+$")
    if group ~= nil then
      rateLimiterKey = rateLimiterKey .. ":" .. group
    end
  end

  -- -- key for storing rate limited jobs
  -- When a job has been previously rate limited it should be part of this set
  -- if the job is back here means that the delay time for this job has passed and now we should
  -- be able to process it again.
  local limitedSetKey = rateLimiterKey .. ":limited"
  local delay = 0

  -- -- Check if job was already limited
  local isLimited = rcall("SISMEMBER", limitedSetKey, jobId);

  if isLimited == 1 then
     -- Remove from limited zset since we are going to try to process it
     rcall("SREM", limitedSetKey, jobId)
     rcall("HDEL", limiterIndexTable, jobId)
  else
    -- If not, check if there are any limited jobs
    -- If the job has not been rate limited, we should check if there are any other rate limited jobs, because if that
    -- is the case we do not want to process this job, just calculate a delay for it and put it to "sleep".
    local numLimitedJobs = rcall("SCARD", limitedSetKey)

    if numLimitedJobs > 0 then
      -- Note, add some slack to compensate for drift.
      delay = ((numLimitedJobs * ARGV[7] * 1.1) /  maxJobs) + tonumber(rcall("PTTL", rateLimiterKey))
    end
  end

  local jobCounter = tonumber(rcall("GET", rateLimiterKey))
  if(jobCounter == nil) then
    jobCounter = 0
  end
  -- check if rate limit hit
  if (delay == 0) and (jobCounter >= maxJobs) then
    -- Seems like there are no current rated limited jobs, but the jobCounter has exceeded the number of jobs for this unit of time so we need to rate limit this job.
    local exceedingJobs = jobCounter - maxJobs
    delay = tonumber(rcall("PTTL", rateLimiterKey)) + ((exceedingJobs) * ARGV[7]) / maxJobs
  end

  if delay > 0 then
    local bounceBack = ARGV[8]
    if bounceBack == 'false' then
      local timestamp = delay + tonumber(ARGV[4])
      -- put job into delayed queue
      rcall("ZADD", KEYS[7], timestamp * 0x1000 + bit.band(jobCounter, 0xfff), jobId)
      rcall("PUBLISH", KEYS[7], timestamp)
      rcall("SADD", limitedSetKey, jobId)

      -- store index so that we can delete rate limited data
      rcall("HSET", limiterIndexTable, jobId, limitedSetKey)

    end

    -- remove from active queue
    rcall("LREM", KEYS[2], 1, jobId)
    return true
  else
    -- false indicates not rate limited
    -- increment jobCounter only when a job is not rate limited
    if (jobCounter == 0) then
      rcall("PSETEX", rateLimiterKey, ARGV[7], 1)
    else
      rcall("INCR", rateLimiterKey)
    end
    return false
  end
end

local jobId = ARGV[5]

if jobId ~= '' then
  -- clean stalled key
  rcall("SREM", KEYS[5], jobId)
else
  -- move from wait to active
  jobId = rcall("RPOPLPUSH", KEYS[1], KEYS[2])
end

if jobId then
  -- Check if we need to perform rate limiting.
  local maxJobs = tonumber(ARGV[6])

  if maxJobs then
    if rateLimit(jobId, maxJobs) then
       return
    end
  end

  -- get a lock
  local jobKey = ARGV[1] .. jobId
  local lockKey = jobKey .. ':lock'
  rcall("SET", lockKey, ARGV[2], "PX", ARGV[3])

  -- remove from priority
  rcall("ZREM", KEYS[3], jobId)
  rcall("PUBLISH", KEYS[4], jobId)
  rcall("HSET", jobKey, "processedOn", ARGV[4])

  return {rcall("HGETALL", jobKey), jobId} -- get job data
else
  rcall("PUBLISH", KEYS[8], "")
end

