local kong_meta = require "kong.meta"
local redis = require "resty.redis"
local aes = require "resty.aes"
local re_gmatch = ngx.re.gmatch
local re_match = ngx.re.match
local decode_base64 = ngx.decode_base64
local cjson_safe = require "cjson.safe"
cjson_safe.encode_empty_table_as_object(false)

local SECRET_AES_256_KEY = os.getenv("SECRET_AES_256_KEY")
local SECRET_AES_256_IV = os.getenv("SECRET_AES_256_IV")
local secret_aes_256

local CustomBasicAuth = {PRIORITY = 1098, VERSION = kong_meta.version}

local function gen_error_response(code, message, error_format)
    if error_format == "openapi" then
        return cjson_safe.encode({code = code, message = message, details = {}})
    elseif error_format == "dataapi" then
        return cjson_safe.encode({
            type = "",
            data = {
                columns = {},
                rows = {},
                result = {
                    start_ms = 0,
                    end_ms = 0,
                    latency = "",
                    row_count = 0,
                    row_affect = 0,
                    limit = 0,
                    code = code,
                    message = message
                }
            }
        })
    else
        return ""
    end
end

local function extract_authorization()
    local username, password
    local authorization_header = kong.request.get_header("authorization")

    if authorization_header then
        local iterator, iter_err = re_gmatch(authorization_header,
                                             "\\s*[Bb]asic\\s*(.+)")
        if not iterator then
            kong.log
                .err(iter_err, "trace_id: ", kong.ctx.shared.trace_id or "-")
            return
        end

        local m, err = iterator()
        if err then
            kong.log.err(err, "trace_id: ", kong.ctx.shared.trace_id or "-")
            return
        end

        if m and m[1] then
            local decoded_basic = decode_base64(m[1])
            if decoded_basic then
                local basic_parts, err =
                    re_match(decoded_basic, "([^:]+):(.*)", "oj")
                if err then
                    kong.log.err(err, "trace_id: ",
                                 kong.ctx.shared.trace_id or "-")
                    return
                end

                if not basic_parts then
                    kong.log.err("header has unrecognized format", "trace_id: ",
                                 kong.ctx.shared.trace_id or "-")
                    return
                end

                username = basic_parts[1]
                password = basic_parts[2]
            end
        end
    end

    return username, password
end

local function is_present(s)
    return s and s ~= "" and s ~= ngx.null
end

local sock_opts = {}
local function get_redis_connection(conf)
    local red = redis:new()
    red:set_timeout(conf.redis_timeout)

    sock_opts.ssl = conf.redis_ssl
    sock_opts.ssl_verify = conf.redis_ssl_verify

    -- use a special pool name only if redis_database is set to non-zero
    -- otherwise use the default pool name host:port
    if conf.redis_database ~= 0 then
        sock_opts.pool = string.format("%s:%d;%d", conf.redis_host,
                                       conf.redis_port, conf.redis_database)
    end

    local ok, err = red:connect(conf.redis_host, conf.redis_port, sock_opts)
    if not ok then
        return nil, string.format("failed to connect to Redis: %s", err)
    end

    local times, err = red:get_reused_times()
    if err then
        return nil, string.format("failed to get connect reused times: %s", err)
    end

    if times == 0 then
        if is_present(conf.redis_password) then
            local ok, err
            if is_present(conf.redis_username) then
                ok, err = red:auth(conf.redis_username, conf.redis_password)
            else
                ok, err = red:auth(conf.redis_password)
            end

            if not ok then
                return nil, string.format("failed to auth Redis: %s", err)
            end
        end

        if conf.redis_database ~= 0 then
            -- Only call select first time, since we know the connection is shared
            -- between instances that use the same redis database

            local ok, err = red:select(conf.redis_database)
            if not ok then
                return nil, string.format("failed to change Redis database: %s",
                                          err)
            end
        end
    end

    return red
end

local function hex_to_char(c)
    return string.char(tonumber(c, 16))
end

local function from_hex(s)
    if s ~= nil then
        s = string.gsub(s, "%x%x", hex_to_char)
    end
    return s
end

local function validate(conf, username, password)
    local red, err = get_redis_connection(conf)
    if not red then
        return nil, string.format("failed to get redis connection: %s", err)
    end
    local user_secret, err = red:get(string.format("dbaas:account:api_keys:%s",
                                                   username))
    if err then
        return nil, string.format("failed to exec get redis: %s", err)
    end
    local ok, err = red:set_keepalive(10000, 100)
    if not ok then
        return nil, string.format("failed to set Redis keepalive: %s", err)
    end
    if user_secret == ngx.null then
        return nil, string.format(
                   "get empty user secret, result is null: username %s",
                   username)
    end
    local secret_obj = cjson_safe.decode(user_secret)
    if not secret_obj then
        return nil, "can't decode redis user secret"
    end

    local expired_at = secret_obj["expired_ts"]
    if expired_at~=nil and expired_at>0 and ngx.now()*1000>expired_at then
        return nil, "KEY_EXPIRED"
    end

    if not secret_obj["private_key"] then
        return nil, "can't get user private_key"
    end
    local passwd = secret_aes_256:decrypt(from_hex(secret_obj["private_key"]))
    if not passwd then
        return nil, "can't decrypt passwd"
    end
    if password ~= passwd then
        return nil, "password unmatch"
    end
    return secret_obj
end

local function reportUnauth(conf)
    if string.find(conf.redis_host,"^replica")~=nil then
        return
    end
    local red, err = get_redis_connection(conf)
    if not red then
        kong.log.err("reportUnauth failed to get redis connection",err)
        return
    end
    local path=kong.request.get_path()
    local host=kong.request.get_host()
    local res, err = red:hincrby("gateway_unauth_report",host.."/"..path,1)
    if err then
        kong.log.err("reportUnauth failed to exec hincrby redis",err)
        return
    end
    red:expire("gateway_unauth_report",60)
    red:set_keepalive(10000, 100)
end

function CustomBasicAuth:access(conf)
    if kong.ctx.shared.has_authed then
        kong.log.info("skip basic auth since has bear auth, trace_id: ",
                      kong.ctx.shared.trace_id or "-")
        return
    end
    if not kong.request.get_header("authorization") then
        kong.log.info(
            "skip basic auth since no authorization header, trace_id: ",
            kong.ctx.shared.trace_id or "-")
        return
    end

    local username, password = extract_authorization()
    if not username or not password then
        kong.log.info(
            "skip basic auth since can not get username and password, trace_id: ",
            kong.ctx.shared.trace_id or "-")
        return
    end
    secret_aes_256 = aes:new(SECRET_AES_256_KEY, nil, aes.cipher(256, "cbc"),
                             {iv = SECRET_AES_256_IV})

    local secret_obj, err = validate(conf, username, password)
    if err then
        kong.log.err("validate failed, username: ", username, ", error: ", err,
                     ", trace_id: ", kong.ctx.shared.trace_id or "-")
        reportUnauth(conf)
        local msg = "unauthorized"
        if err=="KEY_EXPIRED" then
            msg="API Key is no longer valid"
        end
        local resp = gen_error_response(49900002, msg,
                                        conf.error_format)
        kong.response.set_header("X-Debug-Trace-Id",
                                 kong.ctx.shared.trace_id or "-")
        kong.response.set_header("Access-Control-Allow-Headers",
                                 "accept, content-type, authorization")
        kong.response.set_header("Access-Control-Allow-Methods",
                                 "POST, GET, OPTIONS, PUT, DELETE, PATCH")
        kong.response.set_header("Access-Control-Allow-Origin", "*")
        kong.response.add_header("Access-Control-Expose-Headers",
                                 "x-debug-trace-id,x-kong-proxy-latency,x-kong-upstream-latency,x-kong-response-latency,x-pingcap-requestid,x-ratelimit-remaining-minute,x-ratelimit-limit-minute,x-ratelimit-reset")
        kong.response.exit(401, resp, {["Content-Type"] = "application/json"})
    end

    local auth_content = {}
    auth_content["public_key"] = username
    if secret_obj and secret_obj["rate_limit_rpm"] then
        auth_content["rate_limit_rpm"] = secret_obj["rate_limit_rpm"]
    end
    kong.service.request.set_header("x-auth-type", "openapi")
    kong.service.request.set_header("x-auth-method", "basic")
    kong.service.request.set_header("x-auth-username", username)
    kong.service.request.set_header("x-auth-content",
                                    cjson_safe.encode(auth_content))
    kong.service.request.clear_header("authorization")
    kong.ctx.shared.has_authed = true
    kong.ctx.shared.auth_content = auth_content
end

return CustomBasicAuth
