#!/usr/bin/env lua

--
-- 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 pkg_cpath_org = package.cpath
local pkg_path_org = package.path

local apisix_home = "/usr/local/apisix"
local pkg_cpath = apisix_home .. "/deps/lib64/lua/5.1/?.so;"
                  .. apisix_home .. "/deps/lib/lua/5.1/?.so;;"
local pkg_path = apisix_home .. "/deps/share/lua/5.1/?.lua;;"

-- modify the load path to load our dependencies
package.cpath = pkg_cpath .. pkg_cpath_org
package.path  = pkg_path .. pkg_path_org

-- pass path to construct the final result
local env = require("apisix.cli.env")(apisix_home, pkg_cpath_org, pkg_path_org)
local file = require("apisix.cli.file")
local util = require("apisix.cli.util")
local ngx_tpl = require("apisix.cli.ngx_tpl")
local etcd = require("apisix.cli.etcd")
local template = require("resty.template")

local function write_file(file_path, data)
    local file, err = io.open(file_path, "w+")
    if not file then
        return false, "failed to open file: " .. file_path .. ", error info:" .. err
    end

    file:write(data)
    file:close()
    return true
end

local function is_file_exist(file_path)
    local file, err = io.open(file_path)
    if not file then
        return false, "failed to open file: " .. file_path .. ", error info: " .. err
    end

    file:close()
    return true
end


local function get_openresty_version()
    local str = "nginx version: openresty/"
    local ret = util.execute_cmd("openresty -v 2>&1")
    local pos = string.find(ret, str)
    if pos then
        return string.sub(ret, pos + string.len(str))
    end

    str = "nginx version: nginx/"
    pos = string.find(ret, str)
    if pos then
        return string.sub(ret, pos + string.len(str))
    end

    return nil
end


local function is_32bit_arch()
    local ok, ffi = pcall(require, "ffi")
    if ok then
        -- LuaJIT
        return ffi.abi("32bit")
    end
    local ret = util.execute_cmd("getconf LONG_BIT")
    local bits = tonumber(ret)
    return bits <= 32
end


local function check_version(cur_ver_s, need_ver_s)
    local cur_vers = util.split(cur_ver_s, [[.]])
    local need_vers = util.split(need_ver_s, [[.]])
    local len = math.max(#cur_vers, #need_vers)

    for i = 1, len do
        local cur_ver = tonumber(cur_vers[i]) or 0
        local need_ver = tonumber(need_vers[i]) or 0
        if cur_ver > need_ver then
            return true
        end

        if cur_ver < need_ver then
            return false
        end
    end

    return true
end


local function local_dns_resolver(file_path)
    local file, err = io.open(file_path, "rb")
    if not file then
        return false, "failed to open file: " .. file_path .. ", error info:" .. err
    end
    local dns_addrs = {}
    for line in file:lines() do
        local addr, n = line:gsub("^nameserver%s+(%d+%.%d+%.%d+%.%d+)%s*$", "%1")
        if n == 1 then
            table.insert(dns_addrs, addr)
        end
    end
    file:close()
    return dns_addrs
end


local _M = {version = 0.1}

function _M.help()
    print([[
Usage: apisix [action] <argument>

help:       show this message, then exit
init:       initialize the local nginx.conf
init_etcd:  initialize the data of etcd
start:      start the apisix server
stop:       stop the apisix server
restart:    restart the apisix server
reload:     reload the apisix server
version:    print the version of apisix
]])
end


local checked_admin_key = false
local function init()
    if env.is_root_path then
        print('Warning! Running apisix under /root is only suitable for development environments'
            .. ' and it is dangerous to do so. It is recommended to run APISIX in a directory other than /root.')
    end

    -- read_yaml_conf
    local yaml_conf, err = file.read_yaml_conf(env.apisix_home)
    if not yaml_conf then
        error("failed to read local yaml config of apisix: " .. err)
    end
    -- print("etcd: ", yaml_conf.etcd.host)

    -- check the Admin API token
    if yaml_conf.apisix.enable_admin and yaml_conf.apisix.allow_admin then
        for _, allow_ip in ipairs(yaml_conf.apisix.allow_admin) do
            if allow_ip == "127.0.0.0/24" then
                checked_admin_key = true
            end
        end
    end

    if yaml_conf.apisix.enable_admin and not checked_admin_key then
        checked_admin_key = true
        local help = [[

%s
Please modify "admin_key" in conf/config.yaml .

]]
        if type(yaml_conf.apisix.admin_key) ~= "table" or
           #yaml_conf.apisix.admin_key == 0
        then
            io.stderr:write(help:format("ERROR: missing valid Admin API token."))
            os.exit(1)
        end

        for _, admin in ipairs(yaml_conf.apisix.admin_key) do
            if type(admin.key) == "table" then
                admin.key = ""
            else
                admin.key = tostring(admin.key)
            end

            if admin.key == "" then
                io.stderr:write(help:format("ERROR: missing valid Admin API token."), "\n")
                os.exit(1)
            end

            if admin.key == "edd1c9f034335f136f87ad84b625c8f1" then
                io.stderr:write(
                    help:format([[WARNING: using fixed Admin API token has security risk.]]),
                    "\n"
                )
            end
        end
    end

    local or_ver = util.execute_cmd("openresty -V 2>&1")
    local with_module_status = true
    if or_ver and not or_ver:find("http_stub_status_module", 1, true) then
        io.stderr:write("'http_stub_status_module' module is missing in ",
                        "your openresty, please check it out. Without this ",
                        "module, there will be fewer monitoring indicators.\n")
        with_module_status = false
    end

    local enabled_plugins = {}
    for i, name in ipairs(yaml_conf.plugins) do
        enabled_plugins[name] = true
    end

    if enabled_plugins["proxy-cache"] and not yaml_conf.apisix.proxy_cache then
        error("missing apisix.proxy_cache for plugin proxy-cache")
    end

    --support multiple ports listen, compatible with the original style
    if type(yaml_conf.apisix.node_listen) == "number" then
        local node_listen = {yaml_conf.apisix.node_listen}
        yaml_conf.apisix.node_listen = node_listen
    end

    if type(yaml_conf.apisix.ssl.listen_port) == "number" then
        local listen_port = {yaml_conf.apisix.ssl.listen_port}
        yaml_conf.apisix.ssl.listen_port = listen_port
    end

    if yaml_conf.apisix.ssl.ssl_trusted_certificate ~= nil then
        local ok, err = is_file_exist(yaml_conf.apisix.ssl.ssl_trusted_certificate)
        if not ok then
            io.stderr:write(err, "\n")
            os.exit(1)
        end
    end

    admin_api_mtls = yaml_conf.apisix.admin_api_mtls
    if yaml_conf.apisix.https_admin and not (admin_api_mtls and
        admin_api_mtls.admin_ssl_cert and admin_api_mtls.admin_ssl_cert ~= "" and
        admin_api_mtls.admin_ssl_cert_key and admin_api_mtls.admin_ssl_cert_key ~= "" ) then
        error("missing ssl cert for https admin")
    end

    ssl = yaml_conf.apisix.ssl
    if ssl and ssl.enable and not (
        ssl.ssl_cert and ssl.ssl_cert ~= "" and
        ssl.ssl_cert_key and ssl.ssl_cert_key ~= "") then
        error("missing ssl cert for ssl")
    end

    -- Using template.render
    local sys_conf = {
        lua_path = env.pkg_path_org,
        lua_cpath = env.pkg_cpath_org,
        os_name = util.trim(util.execute_cmd("uname")),
        apisix_lua_home = env.apisix_home,
        with_module_status = with_module_status,
        error_log = {level = "warn"},
        enabled_plugins = enabled_plugins,
    }

    if not yaml_conf.apisix then
        error("failed to read `apisix` field from yaml file")
    end

    if not yaml_conf.nginx_config then
        error("failed to read `nginx_config` field from yaml file")
    end

    if is_32bit_arch() then
        sys_conf["worker_rlimit_core"] = "4G"
    else
        sys_conf["worker_rlimit_core"] = "16G"
    end

    for k,v in pairs(yaml_conf.apisix) do
        sys_conf[k] = v
    end
    for k,v in pairs(yaml_conf.nginx_config) do
        sys_conf[k] = v
    end

    local wrn = sys_conf["worker_rlimit_nofile"]
    local wc = sys_conf["event"]["worker_connections"]
    if not wrn or wrn <= wc then
        -- ensure the number of fds is slightly larger than the number of conn
        sys_conf["worker_rlimit_nofile"] = wc + 128
    end

    if(sys_conf["enable_dev_mode"] == true) then
        sys_conf["worker_processes"] = 1
        sys_conf["enable_reuseport"] = false
    elseif tonumber(sys_conf["worker_processes"]) == nil then
        sys_conf["worker_processes"] = "auto"
    end

    if sys_conf.allow_admin and #sys_conf.allow_admin == 0 then
        sys_conf.allow_admin = nil
    end

    local dns_resolver = sys_conf["dns_resolver"]
    if not dns_resolver or #dns_resolver == 0 then
        local dns_addrs, err = local_dns_resolver("/etc/resolv.conf")
        if not dns_addrs then
            error("failed to import local DNS: " .. err)
        end

        if #dns_addrs == 0 then
            error("local DNS is empty")
        end
        sys_conf["dns_resolver"] = dns_addrs
    end

    local env_worker_processes = os.getenv("APISIX_WORKER_PROCESSES")
    if env_worker_processes then
        sys_conf["worker_processes"] = math.floor(tonumber(env_worker_processes))
    end

    local conf_render = template.compile(ngx_tpl)
    local ngxconf = conf_render(sys_conf)

    local ok, err = write_file(env.apisix_home .. "/conf/nginx.conf", ngxconf)
    if not ok then
        error("failed to update nginx.conf: " .. err)
    end

    local op_ver = get_openresty_version()
    if op_ver == nil then
        io.stderr:write("can not find openresty\n")
        return
    end

    local need_ver = "1.15.8"
    if not check_version(op_ver, need_ver) then
        io.stderr:write("openresty version must >=", need_ver, " current ", op_ver, "\n")
        return
    end
end
_M.init = init

function _M.start(...)
    local cmd_logs = "mkdir -p " .. env.apisix_home .. "/logs"
    util.execute_cmd(cmd_logs)
    -- check running
    local pid_path = env.apisix_home .. "/logs/nginx.pid"
    local pid, err = util.read_file(pid_path)
    if pid then
        local hd = io.popen("lsof -p " .. pid)
        local res = hd:read("*a")
        if res and res ~= "" then
            print("APISIX is running...")
            return nil
        end
    end

    init(...)
    _M.init_etcd(...)

    os.execute(env.openresty_args)
end

function _M.stop()
    local cmd = env.openresty_args .. [[ -s stop]]
    -- print(cmd)
    os.execute(cmd)
end

function _M.restart()
  _M.stop()
  _M.start()
end

function _M.reload()
    -- reinit nginx.conf
    init()

    local test_cmd = env.openresty_args .. [[ -t -q ]]
    -- When success,
    -- On linux, os.execute returns 0,
    -- On macos, os.execute returns 3 values: true, exit, 0, and we need the first.
    local test_ret = os.execute((test_cmd))
    if (test_ret == 0 or test_ret == true) then
        local cmd = env.openresty_args .. [[ -s reload]]
        -- print(cmd)
        os.execute(cmd)
        return
    end
    print("test openresty failed")
end

function _M.version()
    local ver = require("apisix.core.version")
    print(ver['VERSION'])
end

_M.init_etcd = function(show_output)
    etcd.init(env, show_output)
end

local cmd_action = arg[1]
if not cmd_action then
    return _M.help()
end

if not _M[cmd_action] then
    print("invalid argument: ", cmd_action, "\n")
    _M.help()
    return
end

_M[cmd_action](arg[2])
