local EMPTY = require("kong.tools.table").EMPTY


local kong = kong
local type = type
local mt_cache = { __mode = "k" }
local setmetatable = setmetatable
local consumer_groups_cache = setmetatable({}, mt_cache)
local consumer_in_groups_cache = setmetatable({}, mt_cache)


local function load_groups_into_memory(consumer_pk)
  local groups
  local len = 0

  for row, err in kong.db.acls:each_for_consumer(consumer_pk) do
    if err then
      return nil, err
    end

    if not groups then
      groups = {}
    end

    len = len + 1
    groups[len] = row
  end

  if len == 0 then
    return EMPTY
  end

  return groups
end


--- Returns the database records with groups the consumer belongs to
-- @param consumer_id (string) the consumer for which to fetch the groups it belongs to
-- @return table with group records (empty table if none), or nil+error
local function get_consumer_groups_raw(consumer_id)
  local cache_key = kong.db.acls:cache_key(consumer_id)
  local raw_groups, err = kong.cache:get(cache_key, nil,
                                         load_groups_into_memory,
                                         { id = consumer_id })
  if err then
    return nil, err
  end

  if raw_groups and #raw_groups > 0 then
    return raw_groups
  end

  -- use EMPTY to be able to use it as a cache key, since a new table would
  -- immediately be collected again and not allow for negative caching.
  return EMPTY
end


--- Returns a table with all group names a consumer belongs to.
-- The table will have an array part to iterate over, and a hash part
-- where each group name is indexed by itself. Eg.
-- {
--   [1] = "users",
--   [2] = "admins",
--   users = "users",
--   admins = "admins",
-- }
-- If there are no groups defined, it will return an empty table
-- @param consumer_id (string) the consumer for which to fetch the groups it belongs to
-- @return table with groups, nil or nil+error
local function get_consumer_groups(consumer_id)
  local raw_groups, err = get_consumer_groups_raw(consumer_id)
  if not raw_groups then
    return nil, err
  end

  local groups = consumer_groups_cache[raw_groups]
  if not groups then
    if raw_groups == EMPTY then
      consumer_groups_cache[raw_groups] = EMPTY

    else
      groups = {}
      consumer_groups_cache[raw_groups] = groups
      for i = 1, #raw_groups do
        local group = raw_groups[i].group
        groups[i] = group
        groups[group] = group
      end
    end
  end

  if groups == EMPTY then
    return nil
  end

  return groups
end


--- checks whether a consumer-group-list is part of a given list of groups.
-- @param groups_to_check (table) an array of group names. Note: since the
-- results will be cached by this table, always use the same table for the
-- same set of groups!
-- @param consumer_groups (table) list of consumer groups (result from
-- `get_consumer_groups`)
-- @return (boolean) whether the consumer is part of any of the groups.
local function consumer_in_groups(groups_to_check, consumer_groups)
  -- 1st level cache on "groups_to_check"
  local result1 = consumer_in_groups_cache[groups_to_check]
  if result1 == nil then
    result1 = setmetatable({}, mt_cache)
    consumer_in_groups_cache[groups_to_check] = result1
  end

  -- 2nd level cache on "consumer_groups"
  local result2 = result1[consumer_groups]
  if result2 ~= nil then
    return result2
  end

  -- not found, so validate and populate 2nd level cache
  result2 = false
  for i = 1, #groups_to_check do
    if consumer_groups[groups_to_check[i]] then
      result2 = true
      break
    end
  end

  result1[consumer_groups] = result2

  return result2
end


--- Gets the currently identified consumer for the request.
-- Checks both consumer and if not found the credentials.
-- @return consumer_id (string), or alternatively `nil` if no consumer was
-- authenticated.
local function get_current_consumer_id()
  return (kong.client.get_consumer() or EMPTY).id or
         (kong.client.get_credential() or EMPTY).consumer_id
end


--- Returns a table with all group names.
-- The table will have an array part to iterate over, and a hash part
-- where each group name is indexed by itself. Eg.
-- {
--   [1] = "users",
--   [2] = "admins",
--   users = "users",
--   admins = "admins",
-- }
-- If there are no authenticated_groups defined, it will return nil
-- @return table with groups or nil
local function get_authenticated_groups()
  local authenticated_groups = kong.ctx.shared.authenticated_groups
  if type(authenticated_groups) ~= "table" then
    authenticated_groups = ngx.ctx.authenticated_groups
    if authenticated_groups == nil then
      return nil
    end

    if type(authenticated_groups) ~= "table" then
      kong.log.warn("invalid authenticated_groups, a table was expected")
      return nil
    end
  end

  local groups = {}
  for i = 1, #authenticated_groups do
    groups[i] = authenticated_groups[i]
    groups[authenticated_groups[i]] = authenticated_groups[i]
  end

  return groups
end


--- checks whether a group-list is part of a given list of groups.
-- @param groups_to_check (table) an array of group names.
-- @param groups (table) list of groups (result from
-- `get_authenticated_groups`)
-- @return (boolean) whether the authenticated group is part of any of the
-- groups.
local function group_in_groups(groups_to_check, groups)
  for i = 1, #groups_to_check do
    if groups[groups_to_check[i]] then
      return true
    end
  end
end

local function warmup_groups_cache(consumer_id)
  local cache_key = kong.db.acls:cache_key(consumer_id)
  local _, err = kong.cache:get(cache_key, nil,
                                         load_groups_into_memory,
                                         { id = consumer_id })
  if err then
    return nil, err
  end
end


return {
  get_current_consumer_id = get_current_consumer_id,
  get_consumer_groups = get_consumer_groups,
  get_authenticated_groups = get_authenticated_groups,
  consumer_in_groups = consumer_in_groups,
  group_in_groups = group_in_groups,
  warmup_groups_cache = warmup_groups_cache,
}
