--
-- Licensed to the Apache Software Foundation (ASF) under one or more
-- contributor license agreements.  See the NOTICE file distributed with
-- this work for additional information regarding copyright ownership.
-- The ASF licenses this file to You under the Apache License, Version 2.0
-- (the "License"); you may not use this file except in compliance with
-- the License.  You may obtain a copy of the License at
--
--     http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
local require = require
local core = require("apisix.core")
local timers = require("apisix.timers")
local plugin = require("apisix.plugin")

local ngx_time = ngx.time
local ngx_timer_at = ngx.timer.at
local ngx_worker_id = ngx.worker.id
local type = type

local load_time = os.time()
local plugin_name = "server-info"
local default_report_ttl = 60
local lease_id

local schema = {
    type = "object",
}
local attr_schema = {
    type = "object",
    properties = {
        report_ttl = {
            type = "integer",
            description = "live time for server info in etcd",
            default = default_report_ttl,
            minimum = 3,
            maximum = 86400,
        }
    }
}

local internal_status = ngx.shared["internal-status"]
if not internal_status then
    error("lua_shared_dict \"internal-status\" not configured")
end


local _M = {
    version = 0.1,
    priority = 990,
    name = plugin_name,
    schema = schema,
    scope = "global",
}


local function get_boot_time()
    local time, err = internal_status:get("server_info:boot_time")
    if err ~= nil then
        core.log.error("failed to get boot_time from shdict: ", err)
        return load_time
    end

    if time ~= nil then
        return time
    end

    local _, err = internal_status:set("server_info:boot_time", load_time)
    if err ~= nil then
        core.log.error("failed to save boot_time to shdict: ", err)
    end

    return load_time
end


local function uninitialized_server_info()
    local boot_time = get_boot_time()
    return {
        etcd_version     = "unknown",
        hostname         = core.utils.gethostname(),
        id               = core.id.get(),
        version          = core.version.VERSION,
        boot_time        = boot_time,
    }
end


local function get()
    local data, err = internal_status:get("server_info")
    if err ~= nil then
        core.log.error("get error: ", err)
        return nil, err
    end

    if not data then
        return uninitialized_server_info()
    end

    local server_info, err = core.json.decode(data)
    if not server_info then
        core.log.error("failed to decode server_info: ", err)
        return nil, err
    end

    return server_info
end


local function get_server_info()
    local info, err = get()
    if not info then
        core.log.error("failed to get server_info: ", err)
        return 500
    end

    return 200, info
end


local function set(key, value, ttl)
    local res_new, err = core.etcd.set(key, value, ttl)
    if not res_new then
        core.log.error("failed to set server_info: ", err)
        return nil, err
    end

    if not res_new.body.lease_id then
        core.log.error("failed to get lease_id: ", err)
        return nil, err
    end

    lease_id = res_new.body.lease_id

    -- set or update lease_id
    local ok, err = internal_status:set("lease_id", lease_id)
    if not ok then
        core.log.error("failed to set lease_id to shdict: ", err)
        return nil, err
    end

    return true
end


local function report(premature, report_ttl)
    if premature then
        return
    end

    -- get apisix node info
    local server_info, err = get()
    if not server_info then
        core.log.error("failed to get server_info: ", err)
        return
    end

    if server_info.etcd_version == "unknown" then
        local res, err = core.etcd.server_version()
        if not res then
            core.log.error("failed to fetch etcd version: ", err)
            return

        elseif type(res.body) ~= "table" then
            core.log.error("failed to fetch etcd version: bad version info")
            return

        else
            if res.body.etcdcluster == "" then
                server_info.etcd_version = res.body.etcdserver
            else
                server_info.etcd_version = res.body.etcdcluster
            end
        end
    end

    -- get inside etcd data, if not exist, create it
    local key = "/data_plane/server_info/" .. server_info.id
    local res, err = core.etcd.get(key)
    if not res or (res.status ~= 200 and res.status ~= 404) then
        core.log.error("failed to get server_info from etcd: ", err)
        return
    end

    if not res.body.node then
        local ok, err = set(key, server_info, report_ttl)
        if not ok then
            core.log.error("failed to set server_info to etcd: ", err)
            return
        end

        return
    end

    local ok = core.table.deep_eq(server_info, res.body.node.value)
    -- not equal, update it
    if not ok then
        local ok, err = set(key, server_info, report_ttl)
        if not ok then
            core.log.error("failed to set server_info to etcd: ", err)
            return
        end

        return
    end

    -- get lease_id from ngx dict
    lease_id, err = internal_status:get("lease_id")
    if not lease_id then
        core.log.error("failed to get lease_id from shdict: ", err)
        return
    end

    -- call keepalive
    local res, err = core.etcd.keepalive(lease_id)
    if not res then
        core.log.error("send heartbeat failed: ", err)
        return
    end

    local data, err = core.json.encode(server_info)
    if not data then
        core.log.error("failed to encode server_info: ", err)
        return
    end

    local ok, err = internal_status:set("server_info", data)
    if not ok then
        core.log.error("failed to encode and save server info: ", err)
        return
    end
end


function _M.check_schema(conf)
    local ok, err = core.schema.check(schema, conf)
    if not ok then
        return false, err
    end

    return true
end


function _M.control_api()
    return {
        {
            methods = {"GET"},
            uris ={"/v1/server_info"},
            handler = get_server_info,
        }
    }
end


function _M.init()
    core.log.warn("The server-info plugin is deprecated and will be removed in a future release.")
    if core.config ~= require("apisix.core.config_etcd") then
        -- we don't need to report server info if etcd is not in use.
        return
    end


    local local_conf = core.config.local_conf()
    local deployment_role = core.table.try_read_attr(
                       local_conf, "deployment", "role")
    if deployment_role == "data_plane" then
        -- data_plane should not write to etcd
        return
    end

    local attr = plugin.plugin_attr(plugin_name)
    local ok, err = core.schema.check(attr_schema, attr)
    if not ok then
        core.log.error("failed to check plugin_attr: ", err)
        return
    end

    local report_ttl = attr and attr.report_ttl or default_report_ttl
    local start_at = ngx_time()

    local fn = function()
        local now = ngx_time()
        -- If ttl remaining time is less than half, then flush the ttl
        if now - start_at >= (report_ttl / 2) then
            start_at = now
            report(nil, report_ttl)
        end
    end

    if ngx_worker_id() == 0 then
        local ok, err = ngx_timer_at(0, report, report_ttl)
        if not ok then
            core.log.error("failed to create initial timer to report server info: ", err)
            return
        end
    end

    timers.register_timer("plugin#server-info", fn, true)

    core.log.info("timer update the server info ttl, current ttl: ", report_ttl)
end


function _M.destroy()
    timers.unregister_timer("plugin#server-info", true)
end


return _M
