local constants    = require "kong.constants"
local certificate  = require "kong.runloop.certificate"
local balancer     = require "kong.runloop.balancer"
local workspaces   = require "kong.workspaces"
local wasm         = require "kong.runloop.wasm"


local kong         = kong
local ipairs       = ipairs
local tonumber     = tonumber
local fmt          = string.format
local splitn       = require("kong.tools.string").splitn


local ngx   = ngx
local null  = ngx.null
local log   = ngx.log
local ERR   = ngx.ERR
local CRIT  = ngx.CRIT
local DEBUG = ngx.DEBUG


local ENTITY_CACHE_STORE = constants.ENTITY_CACHE_STORE


-- init in register_events()
local db
local kong_cache
local core_cache
local worker_events
local cluster_events


-- event: "crud", "targets"
local function crud_targets_handler(data)
  local operation = data.operation
  local target = data.entity

  -- => to worker_events: balancer_targets_handler
  local ok, err = worker_events.post("balancer", "targets", {
      operation = operation,
      entity = target,
    })
  if not ok then
    log(ERR, "failed broadcasting target ", operation, " to workers: ", err)
  end

  -- => to cluster_events: cluster_balancer_targets_handler
  local key = fmt("%s:%s", operation, target.upstream.id)
  ok, err = cluster_events:broadcast("balancer:targets", key)
  if not ok then
    log(ERR, "failed broadcasting target ", operation, " to cluster: ", err)
  end
end


-- event: "crud", "upstreams"
local function crud_upstreams_handler(data)
  local operation = data.operation
  local upstream = data.entity

  if not upstream.ws_id then
    log(DEBUG, "Event crud ", operation, " for upstream ", upstream.id,
        " received without ws_id, adding.")
    upstream.ws_id = workspaces.get_workspace_id()
  end

  -- => to worker_events: balancer_upstreams_handler
  local ok, err = worker_events.post("balancer", "upstreams", {
      operation = operation,
      entity = upstream,
    })
  if not ok then
    log(ERR, "failed broadcasting upstream ",
      operation, " to workers: ", err)
  end

  -- => to cluster_events: cluster_balancer_upstreams_handler
  local key = fmt("%s:%s:%s:%s", operation, upstream.ws_id, upstream.id, upstream.name)
  local ok, err = cluster_events:broadcast("balancer:upstreams", key)
  if not ok then
    log(ERR, "failed broadcasting upstream ", operation, " to cluster: ", err)
  end
end


-- event: "balancer", "upstreams"
local function balancer_upstreams_handler(data)
  local operation = data.operation
  local upstream = data.entity

  if not upstream.ws_id then
    log(CRIT, "Operation ", operation, " for upstream ", upstream.id,
        " received without workspace, discarding.")
    return
  end

  core_cache:invalidate_local("balancer:upstreams")
  core_cache:invalidate_local("balancer:upstreams:" .. upstream.id)

  -- => to balancer update
  balancer.on_upstream_event(operation, upstream)
end


-- event: "balancer", "targets"
local function balancer_targets_handler(data)
  -- => to balancer update
  balancer.on_target_event(data.operation, data.entity)
end


-- cluster event: "balancer:targets"
local function cluster_balancer_targets_handler(data)
  local t = splitn(data, ":", 3)
  local operation, key = t[1], t[2]

  local entity = "all"
  if key ~= "all" then
    entity = {
      upstream = { id = key },
    }
  end

  -- => to worker_events: balancer_targets_handler
  local ok, err = worker_events.post("balancer", "targets", {
      operation = operation,
      entity = entity,
    })
  if not ok then
    log(ERR, "failed broadcasting target ", operation, " to workers: ", err)
  end
end


local function cluster_balancer_post_health_handler(data)
  local pattern = "([^|]+)|([^|]*)|([^|]+)|([^|]+)|([^|]+)|(.*)"
  local hostname, ip, port, health, id, name = data:match(pattern)

  port = tonumber(port)
  local upstream = { id = id, name = name }
  if ip == "" then
    ip = nil
  end

  local _, err = balancer.post_health(upstream, hostname, ip, port, health == "1")
  if err then
    log(ERR, "failed posting health of ", name, " to workers: ", err)
  end
end


local function cluster_balancer_upstreams_handler(data)
  local t = splitn(data, ":", 5)
  local operation, ws_id, id, name = t[1], t[2], t[3], t[4]
  local entity = {
    id = id,
    name = name,
    ws_id = ws_id,
  }

  -- => to worker_events: balancer_upstreams_handler
  local ok, err = worker_events.post("balancer", "upstreams", {
      operation = operation,
      entity = entity,
    })
  if not ok then
    log(ERR, "failed broadcasting upstream ", operation, " to workers: ", err)
  end
end


local function dao_crud_handler(data)
  local schema = data.schema
  if not schema then
    log(ERR, "[events] missing schema in crud subscriber")
    return
  end

  local entity = data.entity
  if not entity then
    log(ERR, "[events] missing entity in crud subscriber")
    return
  end

  -- invalidate this entity anywhere it is cached if it has a
  -- caching key

  local schema_name = schema.name

  local cache_key = db[schema_name]:cache_key(entity)
  local cache_obj = kong[ENTITY_CACHE_STORE[schema_name]]

  if cache_key then
    cache_obj:invalidate(cache_key)
  end

  -- if we had an update, but the cache key was part of what was updated,
  -- we need to invalidate the previous entity as well

  local old_entity = data.old_entity
  if old_entity then
    local old_cache_key = db[schema_name]:cache_key(old_entity)
    if old_cache_key and cache_key ~= old_cache_key then
      cache_obj:invalidate(old_cache_key)
    end
  end

  local operation = data.operation
  if not operation then
    log(ERR, "[events] missing operation in crud subscriber")
    return
  end

  -- public worker events propagation

  local entity_channel           = schema.table or schema_name
  local entity_operation_channel = fmt("%s:%s", entity_channel, operation)

  -- crud:routes
  local ok, err = worker_events.post_local("crud", entity_channel, data)
  if not ok then
    log(ERR, "[events] could not broadcast crud event: ", err)
    return
  end

  -- crud:routes:create
  ok, err = worker_events.post_local("crud", entity_operation_channel, data)
  if not ok then
    log(ERR, "[events] could not broadcast crud event: ", err)
    return
  end
end


local function crud_routes_handler()
  log(DEBUG, "[events] Route updated, invalidating router")
  core_cache:invalidate("router:version")
end


local function crud_services_handler(data)
  if data.operation == "create" or data.operation == "delete" then
    return
  end

  -- no need to rebuild the router if we just added a Service
  -- since no Route is pointing to that Service yet.
  -- ditto for deletion: if a Service if being deleted, it is
  -- only allowed because no Route is pointing to it anymore.
  log(DEBUG, "[events] Service updated, invalidating router")
  core_cache:invalidate("router:version")
end


local function crud_plugins_handler(data)
  log(DEBUG, "[events] Plugin updated, invalidating plugins iterator")
  core_cache:invalidate("plugins_iterator:version")
end


local function invalidate_snis(sni_name)
  local sni_wild_pref, sni_wild_suf = certificate.produce_wild_snis(sni_name)
  core_cache:invalidate("snis:" .. sni_name)

  if sni_wild_pref and sni_wild_pref ~= sni_name then
    core_cache:invalidate("snis:" .. sni_wild_pref)
  end

  if sni_wild_suf and sni_wild_suf ~= sni_name then
    core_cache:invalidate("snis:" .. sni_wild_suf)
  end
end


local function crud_snis_handler(data)
  log(DEBUG, "[events] SNI updated, invalidating cached certificates")

  local new_name = data.entity.name
  local old_name = data.old_entity and data.old_entity.name

  invalidate_snis(new_name)
  if old_name and old_name ~= new_name then
    invalidate_snis(old_name)
  end
end


local function crud_consumers_handler(data)
  workspaces.set_workspace(data.workspace)

  local old_entity = data.old_entity
  local old_username
  if old_entity then
    old_username = old_entity.username
    if old_username and old_username ~= null and old_username ~= "" then
      kong_cache:invalidate(db.consumers:cache_key(old_username))
    end
  end

  local entity = data.entity
  if entity then
    local username = entity.username
    if username and username ~= null and username ~= "" and username ~= old_username then
      kong_cache:invalidate(db.consumers:cache_key(username))
    end
  end
end


local function crud_wasm_handler(data, schema_name)
  if not wasm.enabled() then
    return
  end

  local invalidate = false

  -- always invalidate for filter_chain entity changes
  if schema_name == "filter_chains" then
    invalidate = true

  -- invalidate on service/route deletion to ensure we don't have any orphaned
  -- filter chain data cached
  elseif schema_name == "services" or schema_name == "routes" then
    invalidate = data.operation == "delete"

  -- invalidate for changes to wasm filter plugin entities
  elseif schema_name == "plugins" then
    local new_name = data.entity.name
    local old_name = data.old_entity and data.old_entity.name
    invalidate = (new_name and wasm.filters_by_name[new_name]) or
                 (old_name and wasm.filters_by_name[old_name])
  end

  if invalidate then
    log(DEBUG, "[events] wasm filter chains updated, invalidating cache")
    core_cache:invalidate("filter_chains:version")
  end
end


local function crud_ca_certificates_handler(data)
  if data.operation ~= "update" then
    return
  end

  log(DEBUG, "[events] CA certificate updated, invalidating ca certificate store caches")

  local ca_id = data.entity.id

  local done_keys = {}
  for _, entity in ipairs(certificate.get_ca_certificate_reference_entities()) do
    local elements, err = kong.db[entity]:select_by_ca_certificate(ca_id)
    if err then
      log(ERR, "[events] failed to select ", entity, " by ca certificate ", ca_id, ": ", err)
      return
    end

    if elements then
      for _, e in ipairs(elements) do
        local key = certificate.ca_ids_cache_key(e.ca_certificates)

        if not done_keys[key] then
          done_keys[key] = true
          kong.core_cache:invalidate(key)
        end
      end
    end
  end

  local plugin_done_keys = {}
  local plugins, err = kong.db.plugins:select_by_ca_certificate(ca_id, nil,
    certificate.get_ca_certificate_reference_plugins())
  if err then
    log(ERR, "[events] failed to select plugins by ca certificate ", ca_id, ": ", err)
    return
  end

  if plugins then
    for _, e in ipairs(plugins) do
      local key = certificate.ca_ids_cache_key(e.config.ca_certificates)

      if not plugin_done_keys[key] then
        plugin_done_keys[key] = true
        kong.cache:invalidate(key)
      end
    end
  end
end


local LOCAL_HANDLERS = {
  { "dao:crud", nil         , dao_crud_handler },

  -- local events (same worker)
  { "crud"    , "routes"    , crud_routes_handler },
  { "crud"    , "services"  , crud_services_handler },
  { "crud"    , "plugins"   , crud_plugins_handler },

  -- SSL certs / SNIs invalidations
  { "crud"    , "snis"      , crud_snis_handler },

  -- Consumers invalidations
  -- As we support conifg.anonymous to be configured as Consumer.username,
  -- so add an event handler to invalidate the extra cache in case of data inconsistency
  { "crud"    , "consumers" , crud_consumers_handler },

  { "crud"    , "filter_chains"  , crud_wasm_handler },
  { "crud"    , "services"       , crud_wasm_handler },
  { "crud"    , "routes"         , crud_wasm_handler },
  { "crud"    , "plugins"        , crud_wasm_handler },

  -- ca certificate store caches invalidations
  { "crud"    , "ca_certificates" , crud_ca_certificates_handler },
}


local BALANCER_HANDLERS = {
  { "crud"    , "targets"   , crud_targets_handler },
  { "crud"    , "upstreams" , crud_upstreams_handler },

  { "balancer", "targets"   , balancer_targets_handler },
  { "balancer", "upstreams" , balancer_upstreams_handler },
}


local CLUSTER_HANDLERS = {
  -- target updates
  { "balancer:targets"    , cluster_balancer_targets_handler },
  -- manual health updates
  { "balancer:post_health", cluster_balancer_post_health_handler },
  -- upstream updates
  { "balancer:upstreams"  , cluster_balancer_upstreams_handler },
}


local function subscribe_worker_events(source, event, handler)
  worker_events.register(handler, source, event)
end


local function subscribe_cluster_events(source, handler)
  cluster_events:subscribe(source, handler)
end


local function register_local_events()
  for _, v in ipairs(LOCAL_HANDLERS) do
    local source  = v[1]
    local event   = v[2]
    local handler = v[3]

    subscribe_worker_events(source, event, handler)
  end
end


local function register_balancer_events()
  for _, v in ipairs(BALANCER_HANDLERS) do
    local source  = v[1]
    local event   = v[2]
    local handler = v[3]

    subscribe_worker_events(source, event, handler)
  end

  for _, v in ipairs(CLUSTER_HANDLERS) do
    local source  = v[1]
    local handler = v[2]

    subscribe_cluster_events(source, handler)
  end
end


local function register_for_db()
  -- initialize local local_events hooks
  kong_cache     = kong.cache
  core_cache     = kong.core_cache
  worker_events  = kong.worker_events
  cluster_events = kong.cluster_events

  -- events dispatcher

  register_local_events()

  register_balancer_events()
end


local function register_for_dbless(reconfigure_handler)
  -- initialize local local_events hooks
  worker_events = kong.worker_events

  subscribe_worker_events("declarative", "reconfigure", reconfigure_handler)
end


local function register_events(reconfigure_handler)
  -- initialize local local_events hooks
  db = kong.db

  if db.strategy == "off" then
    -- declarative config updates
    register_for_dbless(reconfigure_handler)

    -- dbless (not dataplane) has no other events
    if not kong.sync then
      return
    end
  end

  register_for_db()
end


local function _register_balancer_events(f)
  register_balancer_events = f
end


local declarative_reconfigure_notify
local stream_reconfigure_listener
do
  local buffer = require "string.buffer"

  -- this module may be loaded before `kong.configuration` is initialized
  local socket_path = kong and kong.configuration
                      and kong.configuration.socket_path

  if not socket_path then
    -- `kong.configuration.socket_path` is already normalized to an absolute
    -- path, but `ngx.config.prefix()` is not
    socket_path = require("pl.path").abspath(ngx.config.prefix() .. "/"
                                             .. constants.SOCKET_DIRECTORY)
  end

  local STREAM_CONFIG_SOCK = "unix:" .. socket_path .. "/" .. constants.SOCKETS.STREAM_CONFIG
  local IS_HTTP_SUBSYSTEM  = ngx.config.subsystem == "http"

  local function broadcast_reconfigure_event(data)
    return kong.worker_events.post("declarative", "reconfigure", data)
  end

  declarative_reconfigure_notify = function(reconfigure_data)

    -- call reconfigure_handler in each worker's http subsystem
    local ok, err = broadcast_reconfigure_event(reconfigure_data)
    if ok ~= "done" then
      return nil, "failed to broadcast reconfigure event: " .. (err or ok)
    end

    -- only http should notify stream
    if not IS_HTTP_SUBSYSTEM or
       #kong.configuration.stream_listeners == 0
    then
      return true
    end

    -- update stream if necessary

    local str, err = buffer.encode(reconfigure_data)
    if not str then
      return nil, err
    end

    local sock = ngx.socket.tcp()
    ok, err = sock:connect(STREAM_CONFIG_SOCK)
    if not ok then
      return nil, err
    end

    -- send to stream_reconfigure_listener()

    local bytes
    bytes, err = sock:send(str)
    sock:close()

    if not bytes then
      return nil, err
    end

    assert(bytes == #str,
           "incomplete reconfigure data sent to the stream subsystem")

    return true
  end

  stream_reconfigure_listener = function()
    local sock, err = ngx.req.socket()
    if not sock then
      ngx.log(ngx.CRIT, "unable to obtain request socket: ", err)
      return
    end

    local data, err = sock:receive("*a")
    if not data then
      ngx.log(ngx.CRIT, "unable to receive reconfigure data: ", err)
      return
    end

    local reconfigure_data, err = buffer.decode(data)
    if not reconfigure_data then
      ngx.log(ngx.ERR, "failed to decode reconfigure data: ", err)
      return
    end

    -- call reconfigure_handler in each worker's stream subsystem
    local ok, err = broadcast_reconfigure_event(reconfigure_data)
    if ok ~= "done" then
      ngx.log(ngx.ERR, "failed to rebroadcast reconfigure event in stream: ", err or ok)
    end
  end
end


return {
  -- runloop/handler.lua
  register_events = register_events,

  -- db/declarative/import.lua
  declarative_reconfigure_notify = declarative_reconfigure_notify,

  -- init.lua
  stream_reconfigure_listener = stream_reconfigure_listener,

  -- exposed only for tests
  _register_balancer_events = _register_balancer_events,
}
