local constants = require "kong.constants"
local kong_meta = require "kong.meta"


local kong = kong
local type = type
local error = error
local ipairs = ipairs
local tostring = tostring
local fmt = string.format


local HEADERS_CONSUMER_ID           = constants.HEADERS.CONSUMER_ID
local HEADERS_CONSUMER_CUSTOM_ID    = constants.HEADERS.CONSUMER_CUSTOM_ID
local HEADERS_CONSUMER_USERNAME     = constants.HEADERS.CONSUMER_USERNAME
local HEADERS_CREDENTIAL_IDENTIFIER = constants.HEADERS.CREDENTIAL_IDENTIFIER
local HEADERS_ANONYMOUS             = constants.HEADERS.ANONYMOUS


local KeyAuthHandler = {
  VERSION = kong_meta.version,
  PRIORITY = 1250,
}


local EMPTY = {}


local ERR_DUPLICATE_API_KEY   = "Duplicate API key found"
local ERR_NO_API_KEY          = "No API key found in request"
local ERR_INVALID_AUTH_CRED   = "Unauthorized"
local ERR_INVALID_PLUGIN_CONF = "Invalid plugin configuration"
local ERR_UNEXPECTED          = "An unexpected error occurred"


local function load_credential(key)
  local cred, err = kong.db.keyauth_credentials:select_by_key(key)
  if not cred then
    return nil, err
  end

  if cred.ttl == 0 then
    kong.log.debug("key expired")

    return nil
  end

  return cred, nil, cred.ttl
end


local function set_consumer(consumer, credential)
  kong.client.authenticate(consumer, credential)

  local set_header = kong.service.request.set_header
  local clear_header = kong.service.request.clear_header

  if consumer and consumer.id then
    set_header(HEADERS_CONSUMER_ID, consumer.id)
  else
    clear_header(HEADERS_CONSUMER_ID)
  end

  if consumer and consumer.custom_id then
    set_header(HEADERS_CONSUMER_CUSTOM_ID, consumer.custom_id)
  else
    clear_header(HEADERS_CONSUMER_CUSTOM_ID)
  end

  if consumer and consumer.username then
    set_header(HEADERS_CONSUMER_USERNAME, consumer.username)
  else
    clear_header(HEADERS_CONSUMER_USERNAME)
  end

  if credential and credential.id then
    set_header(HEADERS_CREDENTIAL_IDENTIFIER, credential.id)
  else
    clear_header(HEADERS_CREDENTIAL_IDENTIFIER)
  end

  if credential then
    clear_header(HEADERS_ANONYMOUS)
  else
    set_header(HEADERS_ANONYMOUS, true)
  end
end


local function get_body()
  local body, err = kong.request.get_body()
  if err then
    kong.log.info("Cannot process request body: ", err)
    return EMPTY
  end

  return body
end

local function server_error(message)
  return { status = 500, message = message }
end

local function unauthorized(message, www_auth_content)
  return { status = 401, message = message, headers = { ["WWW-Authenticate"] = www_auth_content } }
end

local function do_authentication(conf)
  if type(conf.key_names) ~= "table" then
    kong.log.err("no conf.key_names set, aborting plugin execution")
    return nil, server_error(ERR_INVALID_PLUGIN_CONF)
  end

  local www_auth_content = conf.realm and fmt('Key realm="%s"', conf.realm) or 'Key'
  local headers = kong.request.get_headers()
  local query = kong.request.get_query()
  local key
  local body

  -- search in headers & querystring
  for _, name in ipairs(conf.key_names) do
    local v

    if conf.key_in_header then
      v = headers[name]
    end

    if not v and conf.key_in_query then
      -- search in querystring
      v = query[name]
    end

    -- search the body, if we asked to
    if not v and conf.key_in_body then
      if not body then
        body = get_body()
      end

      v = body[name]
    end

    if type(v) == "string" then
      key = v

      if conf.hide_credentials then
        kong.service.request.clear_query_arg(name)
        kong.service.request.clear_header(name)

        if conf.key_in_body then
          if not body then
            body = get_body()
          end

          if body ~= EMPTY then
            if body then
              body[name] = nil
            end

            kong.service.request.set_body(body)
          end
        end
      end

      break

    elseif type(v) == "table" then
      -- duplicate API key
      return nil, unauthorized(ERR_DUPLICATE_API_KEY, www_auth_content)
    end
  end

  -- this request is missing an API key, HTTP 401
  if not key or key == "" then
    return nil, unauthorized(ERR_NO_API_KEY, www_auth_content)
  end

  -- retrieve our consumer linked to this API key

  local cache = kong.cache

  local credential_cache_key = kong.db.keyauth_credentials:cache_key(key)
  -- hit_level be 1 if stale value is propelled into L1 cache; so set a minimal `resurrect_ttl`
  local credential, err, hit_level = cache:get(credential_cache_key, { resurrect_ttl = 0.001 }, load_credential,
                                    key)

  if err then
    return error(err)
  end

  kong.log.debug("credential hit_level: ", tostring(hit_level))

  -- no credential in DB, for this key, it is invalid, HTTP 401
  if not credential or hit_level == 4 then
    return nil, unauthorized(ERR_INVALID_AUTH_CRED, www_auth_content)
  end

  -----------------------------------------
  -- Success, this request is authenticated
  -----------------------------------------

  -- retrieve the consumer linked to this API key, to set appropriate headers
  local consumer_cache_key, consumer
  consumer_cache_key = kong.db.consumers:cache_key(credential.consumer.id)
  consumer, err      = cache:get(consumer_cache_key, nil,
                                 kong.client.load_consumer,
                                 credential.consumer.id)
  if err then
    kong.log.err(err)
    return nil, server_error(ERR_UNEXPECTED)
  end

  set_consumer(consumer, credential)

  return true
end

local function set_anonymous_consumer(anonymous)
  local consumer_cache_key = kong.db.consumers:cache_key(anonymous)
  local consumer, err = kong.cache:get(consumer_cache_key, nil,
                                        kong.client.load_consumer,
                                        anonymous, true)
  if err then
    return error(err)
  end

  if not consumer then
    local err_msg = "anonymous consumer " .. anonymous .. " is configured but doesn't exist"
    kong.log.err(err_msg)
    return kong.response.error(500, err_msg)
  end

  set_consumer(consumer)
end


--- When conf.anonymous is enabled we are in "logical OR" authentication flow.
--- Meaning - either anonymous consumer is enabled or there are multiple auth plugins
--- and we need to passthrough on failed authentication.
local function logical_OR_authentication(conf)
  if kong.client.get_credential() then
    -- we're already authenticated and in "logical OR" between auth methods -- early exit
    return
  end

  local ok, _ = do_authentication(conf)
  if not ok then
    set_anonymous_consumer(conf.anonymous)
  end
end

--- When conf.anonymous is not set we are in "logical AND" authentication flow.
--- Meaning - if this authentication fails the request should not be authorized
--- even though other auth plugins might have successfully authorized user.
local function logical_AND_authentication(conf)
  local ok, err = do_authentication(conf)
  if not ok then
    return kong.response.error(err.status, err.message, err.headers)
  end
end


function KeyAuthHandler:access(conf)
  -- check if preflight request and whether it should be authenticated
  if not conf.run_on_preflight and kong.request.get_method() == "OPTIONS" then
    return
  end

  if conf.anonymous then
    return logical_OR_authentication(conf)
  else
    return logical_AND_authentication(conf)
  end
end


return KeyAuthHandler
