---
-- Common Redis functions shared between Redis,
-- Redis Cluster and Redis Sentinel implementations.
--
-- @module resty.session.redis.common


local utils = require "resty.session.utils"


local get_name = utils.get_name
local ipairs = ipairs


---
-- Store session data.
--
-- @function module.SET
-- @tparam table storage the storage
-- @tparam table red the redis instance
-- @tparam string name the cookie name
-- @tparam string key session key
-- @tparam string value session value
-- @tparam number ttl session ttl
-- @tparam number current_time current time
-- @tparam[opt] string old_key old session id
-- @tparam string stale_ttl stale ttl
-- @tparam[opt] table metadata table of metadata
-- @tparam table remember whether storing persistent session or not
-- @treturn true|nil ok
-- @treturn string error message
local function SET(storage, red, name, key, value, ttl, current_time, old_key, stale_ttl, metadata, remember)
  if not metadata and not old_key then
    return red:set(get_name(storage, name, key), value, "EX", ttl)
  end

  local old_name
  local old_ttl
  if old_key then
    old_name = get_name(storage, name, old_key)
    if not remember then
      -- redis < 7.0
      old_ttl = red:ttl(old_name)
    end
  end

  red:init_pipeline()
  red:set(get_name(storage, name, key), value, "EX", ttl)

  -- redis < 7.0
  if old_name then
    if remember then
      red:unlink(old_name)
    elseif not old_ttl or old_ttl > stale_ttl then
      red:expire(old_name, stale_ttl)
    end
  end

  -- redis >= 7.0
  --if old_key then
  --  if remember then
  --    red:unlink(get_name(storage, name, old_key))
  --  else
  --    red:expire(get_name(storage, name, old_key), stale_ttl, "LT")
  --  end
  --end

  if metadata then
    local audiences = metadata.audiences
    local subjects = metadata.subjects
    local score = current_time - 1
    local new_score = current_time + ttl
    local count = #audiences
    for i = 1, count do
      local meta_key = get_name(storage, name, audiences[i], subjects[i])
      red:zremrangebyscore(meta_key, 0, score)
      red:zadd(meta_key, new_score, key)
      if old_key then
        red:zrem(meta_key, old_key)
      end
      red:expire(meta_key, ttl)
    end
  end

  return red:commit_pipeline()
end


---
-- Retrieve session data.
--
-- @function module.GET
-- @tparam table storage the storage
-- @tparam table red the redis instance
-- @tparam string name cookie name
-- @tparam string key session key
-- @treturn string|nil session data
-- @treturn string error message
local function GET(storage, red, name, key)
  return red:get(get_name(storage, name, key))
end


---
-- Delete session data.
--
-- @function module.UNLINK
-- @tparam table storage the storage
-- @tparam table red the redis instance
-- @tparam string name cookie name
-- @tparam string key session key
-- @tparam number current_time current time
-- @tparam[opt] table metadata session meta data
-- @treturn boolean|nil session data
-- @treturn string error message
local function UNLINK(storage, red, name, key, current_time, metadata)
  if not metadata then
    return red:unlink(get_name(storage, name, key))
  end

  red:init_pipeline()
  red:unlink(get_name(storage, name, key))
  local audiences = metadata.audiences
  local subjects = metadata.subjects
  local score = current_time - 1
  local count = #audiences
  for i = 1, count do
    local meta_key = get_name(storage, name, audiences[i], subjects[i])
    red:zremrangebyscore(meta_key, 0, score)
    red:zrem(meta_key, key)
  end

  return red:commit_pipeline()
end


---
-- Read session metadata.
--
-- @function module.READ_METADATA
-- @tparam table storage the storage
-- @tparam table red the redis instance
-- @tparam string name cookie name
-- @tparam string audience session key
-- @tparam string subject session key
-- @tparam number current_time current time
-- @treturn table|nil session metadata
-- @treturn string error message
local function READ_METADATA(storage, red, name, audience, subject, current_time)
  local meta_key = get_name(storage, name, audience, subject)
  local res, err = red:zrange(meta_key, current_time, "+inf", "BYSCORE", "WITHSCORES")
  if not res then
    return nil, err
  end

  local sessions = {}
  for i, v in ipairs(res) do
    if i % 2 ~= 0 then
      sessions[v] = res[i + 1]
    end
  end
  return sessions
end


return {
  SET = SET,
  GET = GET,
  UNLINK = UNLINK,
  READ_METADATA = READ_METADATA,
}
