-- TODO: get rid of 'kong.meta'; this module is king
local meta = require "kong.meta"
local PDK = require "kong.pdk"
local process = require "ngx.process"
local phase_checker = require "kong.pdk.private.phases"
local kong_cache = require "kong.cache"
local kong_cluster_events = require "kong.cluster_events"
local private_node = require "kong.pdk.private.node"
local constants = require "kong.constants"

local ngx = ngx
local type = type
local error = error
local setmetatable = setmetatable


local KONG_VERSION = tostring(meta._VERSION)
local KONG_VERSION_NUM = tonumber(string.format("%d%.2d%.2d",
                                  meta._VERSION_TABLE.major * 100,
                                  meta._VERSION_TABLE.minor * 10,
                                  meta._VERSION_TABLE.patch))

local LOCK_OPTS = {
  exptime = 10,
  timeout = 5,
}


local _ns_mt = { __mode = "v" }
local function get_namespaces(self, ctx)
  if not ctx then
    ctx = ngx.ctx
  end

  local namespaces = ctx.KONG_NAMESPACES
  if not namespaces then
    -- 4 namespaces for request, i.e. ~4 plugins
    namespaces = self.table.new(0, 4)
    ctx.KONG_NAMESPACES = setmetatable(namespaces, _ns_mt)
  end

  return namespaces
end


local function set_namespace(self, namespace, namespace_key, ctx)
  local namespaces = get_namespaces(self, ctx)

  local ns = namespaces[namespace]
  if ns and ns == namespace_key then
    return
  end

  namespaces[namespace] = namespace_key
end


local function del_namespace(self, namespace, ctx)
  if not ctx then
    ctx = ngx.ctx
  end

  local namespaces = get_namespaces(self, ctx)
  namespaces[namespace] = nil
end


-- Runloop interface


local _GLOBAL = {
  phases                 = phase_checker.phases,
}


function _GLOBAL.new()
  return {
    version = KONG_VERSION,
    version_num = KONG_VERSION_NUM,

    configuration = nil,
  }
end


function _GLOBAL.set_named_ctx(self, name, key, ctx)
  if not self then
    error("arg #1 cannot be nil", 2)
  end

  if type(name) ~= "string" then
    error("name must be a string", 2)
  end

  if #name == 0 then
    error("name cannot be an empty string", 2)
  end

  if key == nil then
    error("key cannot be nil", 2)
  end

  if not self.table then
    error("ctx PDK module not initialized", 2)
  end

  set_namespace(self, name, key, ctx)
end


function _GLOBAL.del_named_ctx(self, name, ctx)
  if not self then
    error("arg #1 cannot be nil", 2)
  end

  if type(name) ~= "string" then
    error("name must be a string", 2)
  end

  if #name == 0 then
    error("name cannot be an empty string", 2)
  end

  del_namespace(self, name, ctx)
end


do
  local log_facilities = setmetatable({}, { __index = "k" })


  function _GLOBAL.set_namespaced_log(self, namespace, ctx)
    if not self then
      error("arg #1 cannot be nil", 2)
    end

    if type(namespace) ~= "string" then
      error("namespace (arg #2) must be a string", 2)
    end

    local log = log_facilities[namespace]
    if not log then
      log = self._log.new(namespace) -- use default namespaced format
      log_facilities[namespace] = log
    end

    (ctx or ngx.ctx).KONG_LOG = log
  end


  function _GLOBAL.reset_log(self, ctx)
    if not self then
      error("arg #1 cannot be nil", 2)
    end

    (ctx or ngx.ctx).KONG_LOG = self._log
  end
end


function _GLOBAL.init_pdk(self, kong_config)
  if not self then
    error("arg #1 cannot be nil", 2)
  end

  private_node.init_node_id(kong_config)

  PDK.new(kong_config, self)
end


function _GLOBAL.init_worker_events(kong_config)
  -- Note: worker_events will not work correctly if required at the top of the file.
  --       It must be required right here, inside the init function
  local worker_events
  local opts

  local socket_path = kong_config.socket_path
  local sock = ngx.config.subsystem == "stream" and
               constants.SOCKETS.STREAM_WORKER_EVENTS or
               constants.SOCKETS.WORKER_EVENTS

  local listening = "unix:" .. socket_path .. "/" .. sock

  local max_payload_len = kong_config.worker_events_max_payload

  if max_payload_len and max_payload_len > 65535 then   -- default is 64KB
    ngx.log(ngx.WARN,
            "Increasing 'worker_events_max_payload' value has potential " ..
            "negative impact on Kong's response latency and memory usage")
  end

  local enable_privileged_agent = false
  if kong_config.dedicated_config_processing and
     kong_config.role == "data_plane" and
     not kong.sync  -- for rpc sync there is no privileged_agent
  then
    enable_privileged_agent = true
  end

  -- for debug and test
  ngx.log(ngx.DEBUG,
          "lua-resty-events enable_privileged_agent is ",
          enable_privileged_agent)

  opts = {
    unique_timeout = 5,     -- life time of unique event data in lrucache
    broker_id = 0,          -- broker server runs in nginx worker #0
    listening = listening,  -- unix socket for broker listening
    max_queue_len = 1024 * 50,  -- max queue len for events buffering
    max_payload_len = max_payload_len,  -- max payload size in bytes
    enable_privileged_agent = enable_privileged_agent,
  }

  worker_events = require "resty.events.compat"

  local ok, err = worker_events.configure(opts)
  if not ok then
    return nil, err
  end

  return worker_events
end


function _GLOBAL.init_cluster_events(kong_config, db)
  return kong_cluster_events.new({
    db            = db,
    poll_interval = kong_config.db_update_frequency,
    poll_offset   = kong_config.db_update_propagation,
    poll_delay    = kong_config.db_update_propagation,
  })
end


local function get_lru_size(kong_config)
  if (process.type() == "privileged agent")
  or (kong_config.role == "control_plane")
  or (kong_config.role == "traditional" and #kong_config.proxy_listeners  == 0
                                        and #kong_config.stream_listeners == 0)
  then
    return 1000
  end
end


function _GLOBAL.init_cache(kong_config, cluster_events, worker_events)
  local db_cache_ttl = kong_config.db_cache_ttl
  local db_cache_neg_ttl = kong_config.db_cache_neg_ttl
  local page = 1
  local cache_pages = 1

  if kong_config.database == "off" then
    db_cache_ttl = 0
    db_cache_neg_ttl = 0
   end

  return kong_cache.new({
    shm_name             = "kong_db_cache",
    cluster_events       = cluster_events,
    worker_events        = worker_events,
    ttl                  = db_cache_ttl,
    neg_ttl              = db_cache_neg_ttl or db_cache_ttl,
    resurrect_ttl        = kong_config.db_resurrect_ttl,
    page                 = page,
    cache_pages          = cache_pages,
    resty_lock_opts      = LOCK_OPTS,
    lru_size             = get_lru_size(kong_config),
    invalidation_channel = "invalidations",
  })
end


function _GLOBAL.init_core_cache(kong_config, cluster_events, worker_events)
  local db_cache_ttl = kong_config.db_cache_ttl
  local db_cache_neg_ttl = kong_config.db_cache_neg_ttl
  local page = 1
  local cache_pages = 1

  if kong_config.database == "off" then
    db_cache_ttl = 0
    db_cache_neg_ttl = 0
  end

  return kong_cache.new({
    shm_name        = "kong_core_db_cache",
    cluster_events  = cluster_events,
    worker_events   = worker_events,
    ttl             = db_cache_ttl,
    neg_ttl         = db_cache_neg_ttl or db_cache_ttl,
    resurrect_ttl   = kong_config.db_resurrect_ttl,
    page            = page,
    cache_pages     = cache_pages,
    resty_lock_opts = LOCK_OPTS,
    lru_size        = get_lru_size(kong_config),
  })
end


function _GLOBAL.init_timing()
  return require("kong.timing")
end


return _GLOBAL
