--
-- 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 core     = require("apisix.core")
local consumer = require("apisix.consumer")
local json     = require("apisix.core.json")
local sleep    = core.sleep
local ngx_re = require("ngx.re")
local http     = require("resty.http")
local ngx      = ngx
local rawget   = rawget
local rawset   = rawset
local setmetatable = setmetatable
local type     = type
local string   = string
local req_read_body = ngx.req.read_body
local req_get_body_data = ngx.req.get_body_data

local plugin_name = "wolf-rbac"


local schema = {
    type = "object",
    properties = {
        appid = {
            type = "string",
            default = "unset"
        },
        server = {
            type = "string",
            default = "http://127.0.0.1:12180"
        },
        header_prefix = {
            type = "string",
            default = "X-"
        },
        ssl_verify = {
            type = "boolean",
        },
    }
}

local _M = {
    version = 0.1,
    priority = 2555,
    type = 'auth',
    name = plugin_name,
    schema = schema,
}


local token_version = 'V1'
local function create_rbac_token(appid, wolf_token)
    return token_version .. "#" .. appid .. "#" .. wolf_token
end

local function fail_response(message, init_values)
    local response = init_values or {}
    response.message = message
    return response
end

local function success_response(message, init_values)
    local response = init_values or {}
    response.message = message
    return response
end

local function parse_rbac_token(rbac_token)
    local res, err = ngx_re.split(rbac_token, "#", nil, nil, 3)
    if not res then
        return nil, err
    end

    if #res ~= 3 or res[1] ~= token_version then
        return nil, 'invalid rbac token: version'
    end
    local appid = res[2]
    local wolf_token = res[3]

    return {appid = appid, wolf_token = wolf_token}
end

local function new_headers()
    local t = {}
    local lt = {}
    local _mt = {
        __index = function(t, k)
            return rawget(lt, string.lower(k))
        end,
        __newindex = function(t, k, v)
            rawset(t, k, v)
            rawset(lt, string.lower(k), v)
        end,
     }
    return setmetatable(t, _mt)
end

-- timeout in ms
local function http_req(method, uri, body, myheaders, timeout, ssl_verify)
    if not myheaders then
        myheaders = new_headers()
    end

    local httpc = http.new()
    if timeout then
        httpc:set_timeout(timeout)
    end

    local res, err = httpc:request_uri(uri, {
        method = method,
        headers = myheaders,
        body = body,
        ssl_verify = ssl_verify == true,
    })

    if not res then
        core.log.error("FAIL REQUEST [ method: ", method, ", uri: ", uri,
            " ] failed! res is nil, err:", err)
        return nil, err
    end

    return res
end

local function http_get(uri, myheaders, timeout, ssl_verify)
    return http_req("GET", uri, nil, myheaders, timeout, ssl_verify)
end


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

    local check = {"server"}
    core.utils.check_https(check, conf, plugin_name)
    core.log.info("input conf server: ", conf.server)

    core.utils.check_tls_bool({"ssl_verify"}, conf, plugin_name)

    return true
end


local function fetch_rbac_token(ctx)
    if ctx.var.arg_rbac_token then
        return ngx.unescape_uri(ctx.var.arg_rbac_token)
    end

    if ctx.var.http_authorization then
        return ctx.var.http_authorization
    end

    if ctx.var.http_x_rbac_token then
        return ctx.var.http_x_rbac_token
    end

    return ctx.var['cookie_x-rbac-token']
end


local function check_url_permission(server, appid, action, resName,
                                    client_ip, wolf_token, ssl_verify)
    local retry_max = 3
    local errmsg
    local userInfo
    local res
    local err
    local access_check_url = server .. "/wolf/rbac/access_check"
    local headers = new_headers()
    headers["x-rbac-token"] = wolf_token
    headers["Content-Type"] = "application/json; charset=utf-8"
    local args = { appID = appid, resName = resName, action = action, clientIP = client_ip}
    local url = access_check_url .. "?" .. ngx.encode_args(args)
    local timeout = 1000 * 10

    for i = 1, retry_max do
        -- TODO: read apisix info.
        res, err = http_get(url, headers, timeout, ssl_verify)
        if err then
            break
        else
            core.log.info("check permission request:", url, ", status:", res.status)
            if res.status < 500 then
                break
            else
                core.log.info("request [curl -v ", url, "] failed! status:", res.status)
                if i < retry_max then
                    sleep(0.1)
                end
            end
        end
    end

    if err then
        core.log.error("fail request: ", url, ", err:", err)
        return {
            status = 500,
            err = "request to wolf-server failed, err:" .. err
        }
    end

    if res.status ~= 200 and res.status >= 500 then
        return {
            status = 500,
            err = 'request to wolf-server failed, status:' .. res.status
        }
    end

    local body, err = json.decode(res.body)
    if not body then
        errmsg = 'check permission failed! parse response json failed!'
        core.log.error( "json.decode(", res.body, ") failed! err:", err)
        return {status = res.status, err = errmsg}
    else
        if body.data then
            userInfo = body.data.userInfo
        end
        errmsg = body.reason
        return {status = res.status, err = errmsg, userInfo = userInfo}
    end
end


function _M.rewrite(conf, ctx)
    local url = ctx.var.uri
    local action = ctx.var.request_method
    local client_ip = core.request.get_remote_client_ip(ctx)
    local perm_item = {action = action, url = url, clientIP = client_ip}
    core.log.info("hit wolf-rbac rewrite")

    local rbac_token = fetch_rbac_token(ctx)
    if rbac_token == nil then
        core.log.info("no permission to access ",
                      core.json.delay_encode(perm_item), ", need login!")
        return 401, fail_response("Missing rbac token in request")
    end

    local tokenInfo, err = parse_rbac_token(rbac_token)
    core.log.info("token info appid: ", tokenInfo and tokenInfo.appid,
                  ", err: ", err)
    if err then
        return 401, fail_response('invalid rbac token: parse failed')
    end

    local appid = tokenInfo.appid
    local wolf_token = tokenInfo.wolf_token
    perm_item.appid = appid

    local consumer_conf = consumer.plugin(plugin_name)
    if not consumer_conf then
        return 401, fail_response("Missing related consumer")
    end

    local consumers = consumer.consumers_kv(plugin_name, consumer_conf, "appid")

    local cur_consumer = consumers[appid]
    if not cur_consumer then
        core.log.error("consumer [", appid, "] not found")
        return 401, fail_response("Invalid appid in rbac token")
    end
    core.log.info("consumer appid: ", appid)
    local server = cur_consumer.auth_conf.server
    local ssl_verify = cur_consumer.auth_conf.ssl_verify

    local res = check_url_permission(server, appid, action, url,
                    client_ip, wolf_token, ssl_verify)
    core.log.info(" check_url_permission(appid: ", appid,
                  ", action: ", action, ", url: ", url,
                  ") res status: ", res.status, ", err: ", res.err)

    local username = nil
    local nickname = nil
    if type(res.userInfo) == 'table' then
        local userInfo = res.userInfo
        ctx.userInfo = userInfo
        local userId = userInfo.id
        username = userInfo.username
        nickname = userInfo.nickname or userInfo.username
        local prefix = cur_consumer.auth_conf.header_prefix or ''
        core.response.set_header(prefix .. "UserId", userId)
        core.response.set_header(prefix .. "Username", username)
        core.response.set_header(prefix .. "Nickname", ngx.escape_uri(nickname))
        core.request.set_header(ctx, prefix .. "UserId", userId)
        core.request.set_header(ctx, prefix .. "Username", username)
        core.request.set_header(ctx, prefix .. "Nickname", ngx.escape_uri(nickname))
    end

    if res.status ~= 200 then
        core.log.error(" check_url_permission(appid: ", appid,
            ", action: ", action, ", url: ", url,
            ") failed, res status: ", res.status, ", err: ", res.err)
        return res.status, fail_response(res.err, { username = username, nickname = nickname })
    end
    consumer.attach_consumer(ctx, cur_consumer, consumer_conf)
    core.log.info("wolf-rbac check permission passed")
end

local function get_args()
    local ctx = ngx.ctx.api_ctx
    local args, err
    req_read_body()
    if string.find(ctx.var.http_content_type or "","application/json",
                   1, true) then
        local req_body = req_get_body_data()
        args, err = json.decode(req_body)
        if not args then
            core.log.error("json.decode(", req_body, ") failed! ", err)
        end
    else
        args = core.request.get_post_args(ctx)
    end

    return args
end

local function get_consumer(appid)
    local consumer_conf = consumer.plugin(plugin_name)
    if not consumer_conf then
        core.response.exit(500)
    end

    local consumers = consumer.consumers_kv(plugin_name, consumer_conf, "appid")

    local consumer = consumers[appid]
    if not consumer then
        core.log.info("request appid [", appid, "] not found")
        core.response.exit(400,
                fail_response("appid not found")
            )
    end
    return consumer
end

local function request_to_wolf_server(method, uri, headers, body, ssl_verify)
    headers["Content-Type"] = "application/json; charset=utf-8"
    local timeout = 1000 * 5
    local request_debug = core.json.delay_encode(
        {method = method, uri = uri, timeout = timeout}
    )

    core.log.info("request [", request_debug, "] ....")
    local encoded_body, err = core.json.encode(body)
    if not encoded_body then
        core.log.error("request [", request_debug, "] failed! err: ", err)
        return core.response.exit(500,
            fail_response("request to wolf-server failed!")
        )
    end

    local res, err = http_req(method, uri, encoded_body, headers, timeout, ssl_verify)
    if not res then
        core.log.error("request [", request_debug, "] failed! err: ", err)
        return core.response.exit(500,
            fail_response("request to wolf-server failed!")
        )
    end
    core.log.info("request [", request_debug, "] status: ", res.status)

    if res.status ~= 200 then
        core.log.error("request [", request_debug, "] failed! status: ",
                       res.status)
        return core.response.exit(500, fail_response("request to wolf-server failed!"))
    end
    local body, err = json.decode(res.body)
    if not body then
        core.log.error("request [", request_debug, "] failed! err:", err)
        return core.response.exit(500, fail_response("request to wolf-server failed!"))
    end
    if not body.ok then
        core.log.error("request [", request_debug, "] failed! reason: ", body.reason)
        return core.response.exit(200, fail_response("request to wolf-server failed!"))
    end

    core.log.info("request [", request_debug, "] success!")
    return body
end

local function wolf_rbac_login()
    local args = get_args()
    if not args then
        return core.response.exit(400, fail_response("invalid request"))
    end
    if not args.appid then
        return core.response.exit(400, fail_response("appid is missing"))
    end

    local appid = args.appid
    local consumer = get_consumer(appid)
    core.log.info("consumer appid: ", appid)

    local uri = consumer.auth_conf.server .. '/wolf/rbac/login.rest'
    local headers = new_headers()
    local body = request_to_wolf_server('POST', uri, headers, args, consumer.auth_conf.ssl_verify)

    local userInfo = body.data.userInfo
    local wolf_token = body.data.token

    local rbac_token = create_rbac_token(appid, wolf_token)
    core.response.exit(200, success_response(nil, {rbac_token = rbac_token, user_info = userInfo}))
end

local function get_wolf_token(ctx)
    core.log.info("hit wolf-rbac change_password api")
    local rbac_token = fetch_rbac_token(ctx)
    if rbac_token == nil then
        local url = ctx.var.uri
        local action = ctx.var.request_method
        local client_ip = core.request.get_remote_client_ip(ctx)
        local perm_item = {action = action, url = url, clientIP = client_ip}
        core.log.info("no permission to access ",
                      core.json.delay_encode(perm_item), ", need login!")
        return core.response.exit(401, fail_response("Missing rbac token in request"))
    end

    local tokenInfo, err = parse_rbac_token(rbac_token)
    core.log.info("token info appid: ", tokenInfo and tokenInfo.appid,
                  ", err: ", err)
    if err then
        return core.response.exit(401, fail_response('invalid rbac token: parse failed'))
    end
    return tokenInfo
end

local function wolf_rbac_change_pwd()
    local args = get_args()

    local ctx = ngx.ctx.api_ctx
    local tokenInfo = get_wolf_token(ctx)
    local appid = tokenInfo.appid
    local wolf_token = tokenInfo.wolf_token
    local consumer = get_consumer(appid)
    core.log.info("consumer appid: ", appid)

    local uri = consumer.auth_conf.server .. '/wolf/rbac/change_pwd'
    local headers = new_headers()
    headers['x-rbac-token'] = wolf_token
    request_to_wolf_server('POST', uri, headers, args, consumer.auth_conf.ssl_verify)
    core.response.exit(200, success_response('success to change password', { }))
end

local function wolf_rbac_user_info()
    local ctx = ngx.ctx.api_ctx
    local tokenInfo = get_wolf_token(ctx)
    local appid = tokenInfo.appid
    local wolf_token = tokenInfo.wolf_token
    local consumer = get_consumer(appid)
    core.log.info("consumer appid: ", appid)

    local uri = consumer.auth_conf.server .. '/wolf/rbac/user_info'
    local headers = new_headers()
    headers['x-rbac-token'] = wolf_token
    local body = request_to_wolf_server('GET', uri, headers, {}, consumer.auth_conf.ssl_verify)
    local userInfo = body.data.userInfo
    core.response.exit(200, success_response(nil, {user_info = userInfo}))
end

function _M.api()
    return {
        {
            methods = {"POST"},
            uri = "/apisix/plugin/wolf-rbac/login",
            handler = wolf_rbac_login,
        },
        {
            methods = {"PUT"},
            uri = "/apisix/plugin/wolf-rbac/change_pwd",
            handler = wolf_rbac_change_pwd,
        },
        {
            methods = {"GET"},
            uri = "/apisix/plugin/wolf-rbac/user_info",
            handler = wolf_rbac_user_info,
        },
    }
end

return _M
