---
-- Redis Cluster backend for session library
--
-- @module resty.session.redis.cluster


local common = require "resty.session.redis.common"
local redis = require "resty.rediscluster"


local setmetatable = setmetatable
local error = error
local null = ngx.null


local SET = common.SET
local GET = common.GET
local UNLINK = common.UNLINK
local READ_METADATA = common.READ_METADATA


local function exec(self, func, ...)
  local red, err = redis:new(self.options)
  if err then
    return nil, err
  end

  local ok, err = func(self, red, ...)
  if err then
    red:close()
    return nil, err
  end

  if ok == null then
    ok = nil
  end

  return ok, err
end


---
-- Storage
-- @section instance


local metatable = {}


metatable.__index = metatable


function metatable.__newindex()
  error("attempt to update a read-only table", 2)
end


---
-- Store session data.
--
-- @function instance:set
-- @tparam string name 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 boolean remember whether storing persistent session or not
-- @treturn true|nil ok
-- @treturn string error message
function metatable:set(...)
  return exec(self, SET, ...)
end


---
-- Retrieve session data.
--
-- @function instance:get
-- @tparam string name cookie name
-- @tparam string key session key
-- @treturn string|nil session data
-- @treturn string error message
function metatable:get(...)
  return exec(self, GET, ...)
end


---
-- Delete session data.
--
-- @function instance:delete
-- @tparam string name cookie name
-- @tparam string key session key
-- @tparam[opt] table metadata session meta data
-- @treturn boolean|nil session data
-- @treturn string error message
function metatable:delete(...)
  return exec(self, UNLINK, ...)
end


---
-- Read session metadata.
--
-- @function instance:read_metadata
-- @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
function metatable:read_metadata(...)
  return exec(self, READ_METADATA, ...)
end


local storage = {}


---
-- Configuration
-- @section configuration


---
-- Redis Cluster storage backend configuration
-- @field prefix Prefix for the keys stored in redis.
-- @field suffix Suffix for the keys stored in redis.
-- @field name Redis cluster name.
-- @field nodes Redis cluster nodes.
-- @field lock_zone Shared dictionary name for locks.
-- @field lock_prefix Shared dictionary name prefix for lock.
-- @field max_redirections Maximum retry attempts for redirection.
-- @field max_connection_attempts Maximum retry attempts for connection.
-- @field max_connection_timeout Maximum connection timeout in total among the retries.
-- @field username The database username to authenticate.
-- @field password Password for authentication.
-- @field connect_timeout Controls the default timeout value used in TCP/unix-domain socket object's `connect` method.
-- @field send_timeout controls The default timeout value used in TCP/unix-domain socket object's `send` method.
-- @field read_timeout controls The default timeout value used in TCP/unix-domain socket object's `receive` method.
-- @field keepalive_timeout Controls the default maximal idle time of the connections in the connection pool.
-- @field pool A custom name for the connection pool being used.
-- @field pool_size The size of the connection pool.
-- @field backlog A queue size to use when the connection pool is full (configured with @pool_size).
-- @field ssl Enable SSL (defaults to `false`).
-- @field ssl_verify Verify server certificate (defaults to `nil`).
-- @field server_name The server name for the new TLS extension Server Name Indication (SNI).
-- @table configuration


---
-- Cluster Nodes
--
-- An array of cluster nodes.
--
-- @table nodes


---
-- Cluster Node
-- @field ip The IP address to connect (defaults to `"127.0.0.1"`).
-- @field port The port to connect (defaults to `6379`).
-- @table node


---
-- Constructors
-- @section constructors


---
-- Create a Redis Cluster storage.
--
-- This creates a new Redis Cluster storage instance.
--
-- @function module.new
-- @tparam[opt] table configuration redis cluster storage @{configuration}
-- @treturn table redis cluster storage instance
function storage.new(configuration)
  local prefix                  = configuration and configuration.prefix
  local suffix                  = configuration and configuration.suffix

  local name                    = configuration and configuration.name
  local nodes                   = configuration and configuration.nodes

  local lock_zone               = configuration and configuration.lock_zone
  local lock_prefix             = configuration and configuration.lock_prefix
  local max_redirections        = configuration and configuration.max_redirections
  local max_connection_attempts = configuration and configuration.max_connection_attempts
  local max_connection_timeout  = configuration and configuration.max_connection_timeout

  local username                = configuration and configuration.username
  local password                = configuration and configuration.password

  local connect_timeout         = configuration and configuration.connect_timeout
  local send_timeout            = configuration and configuration.send_timeout
  local read_timeout            = configuration and configuration.read_timeout
  local keepalive_timeout       = configuration and configuration.keepalive_timeout

  local pool                    = configuration and configuration.pool
  local pool_size               = configuration and configuration.pool_size
  local backlog                 = configuration and configuration.backlog
  local ssl                     = configuration and configuration.ssl
  local ssl_verify              = configuration and configuration.ssl_verify
  local server_name             = configuration and configuration.server_name


  local auth
  if not username then
    if password then
      auth = password
      password = nil
    end
  end

  if ssl ~= nil or ssl_verify ~= nil or server_name or pool or pool_size or backlog then
    return setmetatable({
      prefix = prefix,
      suffix = suffix,
      options = {
        name = name,
        dict_name = lock_zone,
        refresh_lock_key = lock_prefix,
        serv_list = nodes,
        connect_timeout = connect_timeout,
        send_timeout = send_timeout,
        read_timeout = read_timeout,
        keepalive_timeout = keepalive_timeout,
        keepalive_cons = pool_size,
        max_redirection = max_redirections,
        max_connection_attempts = max_connection_attempts,
        max_connection_timeout = max_connection_timeout,
        auth = auth,
        username = username,
        password = password,
        connect_opts = {
          ssl = ssl,
          ssl_verify = ssl_verify,
          server_name = server_name,
          pool = pool,
          pool_size = pool_size,
          backlog = backlog,
        },
      },
    }, metatable)
  end

  return setmetatable({
    prefix = prefix,
    suffix = suffix,
    options = {
      name = name,
      dict_name = lock_zone,
      refresh_lock_key = lock_prefix,
      serv_list = nodes,
      connect_timeout = connect_timeout,
      send_timeout = send_timeout,
      read_timeout = read_timeout,
      keepalive_timeout = keepalive_timeout,
      keepalive_cons = pool_size,
      max_redirection = max_redirections,
      max_connection_attempts = max_connection_attempts,
      max_connection_timeout = max_connection_timeout,
      auth = auth,
      username = username,
      password = password,
    },
  }, metatable)
end


return storage
