--
-- 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 type = type
local support_wasm, wasm = pcall(require, "resty.proxy-wasm")
local ngx_var = ngx.var


local schema = {
    type = "object",
    properties = {
        conf = {
            oneOf = {
                { type = "object", minProperties = 1},
                { type = "string", minLength = 1},
            }
        },
    },
    required = {"conf"}
}
local _M = {}


local function check_schema(conf)
    return core.schema.check(schema, conf)
end


local function get_plugin_ctx_key(ctx)
    return ctx.conf_type .. "#" .. ctx.conf_id
end

local function fetch_plugin_ctx(conf, ctx, plugin)
    if not conf.plugin_ctxs then
        conf.plugin_ctxs = {}
    end

    local ctxs = conf.plugin_ctxs
    local key = get_plugin_ctx_key(ctx)
    local plugin_ctx = ctxs[key]
    local err
    if not plugin_ctx then
        if type(conf.conf) == "table" then
            plugin_ctx, err = wasm.on_configure(plugin, core.json.encode(conf.conf))
        elseif type(conf.conf) == "string" then
            plugin_ctx, err = wasm.on_configure(plugin, conf.conf)
        else
            return nil, "invalid conf type"
        end
        if not plugin_ctx then
            return nil, err
        end

        ctxs[key] = plugin_ctx
    end

    return plugin_ctx
end


local function http_request_wrapper(self, conf, ctx)
    local name = self.name
    local plugin_ctx, err = fetch_plugin_ctx(conf, ctx, self.plugin)
    if not plugin_ctx then
        core.log.error(name, ": failed to fetch wasm plugin ctx: ", err)
        return 503
    end

    local ok, err = wasm.on_http_request_headers(plugin_ctx)
    if not ok then
        core.log.error(name, ": failed to run wasm plugin: ", err)
        return 503
    end

    -- $wasm_process_req_body is predefined in ngx_tpl.lua
    local handle_body = ngx_var.wasm_process_req_body
    if handle_body ~= '' then
        -- reset the flag so we can use it for the next Wasm plugin
        -- use ngx.var to bypass the cache
        ngx_var.wasm_process_req_body = ''

        local body, err = core.request.get_body()
        if err ~= nil then
            core.log.error(name, ": failed to get request body: ", err)
            return 503
        end

        local ok, err = wasm.on_http_request_body(plugin_ctx, body, true)
        if not ok then
            core.log.error(name, ": failed to run wasm plugin: ", err)
            return 503
        end
    end
end


local function header_filter_wrapper(self, conf, ctx)
    local name = self.name
    local plugin_ctx, err = fetch_plugin_ctx(conf, ctx, self.plugin)
    if not plugin_ctx then
        core.log.error(name, ": failed to fetch wasm plugin ctx: ", err)
        return 503
    end

    local ok, err = wasm.on_http_response_headers(plugin_ctx)
    if not ok then
        core.log.error(name, ": failed to run wasm plugin: ", err)
        return 503
    end

    -- $wasm_process_resp_body is predefined in ngx_tpl.lua
    local handle_body = ngx_var.wasm_process_resp_body
    if handle_body ~= '' then
        -- reset the flag so we can use it for the next Wasm plugin
        -- use ngx.var to bypass the cache
        ngx_var.wasm_process_resp_body = ""
        ctx["wasm_" .. name .. "_process_resp_body"] = true
    end
end


local function body_filter_wrapper(self, conf, ctx)
    local name = self.name

    local enabled = ctx["wasm_" .. name .. "_process_resp_body"]
    if not enabled then
        return
    end

    local plugin_ctx, err = fetch_plugin_ctx(conf, ctx, self.plugin)
    if not plugin_ctx then
        core.log.error(name, ": failed to fetch wasm plugin ctx: ", err)
        return
    end

    local ok, err = wasm.on_http_response_body(plugin_ctx)
    if not ok then
        core.log.error(name, ": failed to run wasm plugin: ", err)
        return
    end
end


function _M.require(attrs)
    if not support_wasm then
        return nil, "need to build APISIX-Runtime to support wasm"
    end

    local name = attrs.name
    local priority = attrs.priority
    local plugin, err = wasm.load(name, attrs.file)
    if not plugin then
        return nil, err
    end

    local mod = {
        version = 0.1,
        name = name,
        priority = priority,
        schema = schema,
        check_schema = check_schema,
        plugin = plugin,
        type = "wasm",
    }

    if attrs.http_request_phase == "rewrite" then
        mod.rewrite = function (conf, ctx)
            return http_request_wrapper(mod, conf, ctx)
        end
    else
        mod.access = function (conf, ctx)
            return http_request_wrapper(mod, conf, ctx)
        end
    end

    mod.header_filter = function (conf, ctx)
        return header_filter_wrapper(mod, conf, ctx)
    end

    mod.body_filter = function (conf, ctx)
        return body_filter_wrapper(mod, conf, ctx)
    end

    -- the returned values need to be the same as the Lua's 'require'
    return true, mod
end


return _M
