local _M = {}
local _MT = { __index = _M, }


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


local ipairs = ipairs
local ngx_log = ngx.log
local ngx_ERR = ngx.ERR
local ngx_DEBUG = ngx.DEBUG


local DEFAULT_PAGE_SIZE = 512


function _M.new(strategy)
  local self = {
    strategy = strategy,
  }

  return setmetatable(self, _MT)
end


local function get_all_nodes_with_sync_cap()
  local res, err = kong.db.clustering_data_planes:page(DEFAULT_PAGE_SIZE)
  if err then
    return nil, "unable to query DB " .. err
  end

  if not res then
    return EMPTY
  end

  local ret = {}
  local ret_n = 0

  for _, row in ipairs(res) do
    for _, c in ipairs(row.rpc_capabilities) do
      if c == "kong.sync.v2" then
        ret_n = ret_n + 1
        ret[ret_n] = row.id
        break
      end
    end
  end

  return ret
end


function _M:notify_all_nodes()
  local latest_version, err = self.strategy:get_latest_version()
  if not latest_version then
    ngx_log(ngx_ERR, "can not get the latest version: ", err)
    return
  end

  ngx_log(ngx_DEBUG, "[kong.sync.v2] notifying all nodes of new version: ", latest_version)

  local msg = { default = { new_version = latest_version, }, }

  for _, node in ipairs(get_all_nodes_with_sync_cap()) do
    local res, err = kong.rpc:call(node, "kong.sync.v2.notify_new_version", msg)
    if not res then
      if not err:find("requested capability does not exist", nil, true) and
         not err:find("node is not connected", nil, true)
      then
        ngx_log(ngx_ERR, "unable to notify ", node, " new version: ", err)
      end
    end
  end
end


function _M:entity_delta_writer(entity, name, options, ws_id, is_delete)
  local res, err = self.strategy:insert_delta()
  if not res then
    self.strategy:cancel_txn()
    return nil, err
  end

  res, err = self.strategy:commit_txn()
  if not res then
    self.strategy:cancel_txn()
    return nil, err
  end

  -- event "dao:crud" => handle_dao_crud_event() =>
  --   post_push_config_event() => self:notify_all_nodes()

  return entity -- for other hooks
end


-- only control plane has these delta operations
function _M:register_dao_hooks()
  local function is_db_export(name)
    local db_export = kong.db[name].schema.db_export

    kong.log.trace("[kong.sync.v2] name: ", name, " db_export: ", db_export)

    return db_export == nil or db_export == true
  end

  -- common hook functions (pre/fail/post)

  local function pre_hook_func(entity, name, options)
    if not is_db_export(name) then
      return true
    end

    return self.strategy:begin_txn()
  end

  local function fail_hook_func(err, entity, name)
    if not is_db_export(name) then
      return
    end

    ngx_log(ngx_DEBUG, "[kong.sync.v2] failed. Canceling ", name)

    local res, err = self.strategy:cancel_txn()
    if not res then
      ngx_log(ngx_ERR, "unable to cancel cancel_txn: ", tostring(err))
    end
  end

  local function post_hook_writer_func(entity, name, options, ws_id)
    if not is_db_export(name) then
      return entity
    end

    ngx_log(ngx_DEBUG, "[kong.sync.v2] new delta due to writing ", name)

    return self:entity_delta_writer(entity, name, options, ws_id)
  end

  local function post_hook_delete_func(entity, name, options, ws_id, cascade_entries)
    if not is_db_export(name) then
      return entity
    end

    ngx_log(ngx_DEBUG, "[kong.sync.v2] new delta due to deleting ", name)

    return self:entity_delta_writer(entity, name, options, ws_id)
  end

  local dao_hooks = {
    -- dao:insert
    ["dao:insert:pre"]  = pre_hook_func,
    ["dao:insert:fail"] = fail_hook_func,
    ["dao:insert:post"] = post_hook_writer_func,

    -- dao:delete
    ["dao:delete:pre"]  = pre_hook_func,
    ["dao:delete:fail"] = fail_hook_func,
    ["dao:delete:post"] = post_hook_delete_func,

    -- dao:update
    ["dao:update:pre"]  = pre_hook_func,
    ["dao:update:fail"] = fail_hook_func,
    ["dao:update:post"] = post_hook_writer_func,

    -- dao:upsert
    ["dao:upsert:pre"]  = pre_hook_func,
    ["dao:upsert:fail"] = fail_hook_func,
    ["dao:upsert:post"] = post_hook_writer_func,

    -- dao:upsert_by
    ["dao:upsert_by:pre"]  = pre_hook_func,
    ["dao:upsert_by:fail"] = fail_hook_func,
    ["dao:upsert_by:post"] = post_hook_writer_func,

    -- dao:delete_by
    ["dao:delete_by:pre"]  = pre_hook_func,
    ["dao:delete_by:fail"] = fail_hook_func,
    ["dao:delete_by:post"] = post_hook_delete_func,

    -- dao:update_by
    ["dao:update_by:pre"]  = pre_hook_func,
    ["dao:update_by:fail"] = fail_hook_func,
    ["dao:update_by:post"] = post_hook_writer_func,
  }

  for ev, func in pairs(dao_hooks) do
    hooks.register_hook(ev, func)
  end
end


return _M
