--
-- 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 ngx         = ngx
local core        = require("apisix.core")
local schema_def  = require("apisix.schema_def")
local proto       = require("apisix.plugins.grpc-transcode.proto")
local request     = require("apisix.plugins.grpc-transcode.request")
local response    = require("apisix.plugins.grpc-transcode.response")


local plugin_name = "grpc-transcode"

local pb_option_def = {
    {   description = "enum as result",
        type = "string",
        enum = {"enum_as_name", "enum_as_value"},
    },
    {   description = "int64 as result",
        type = "string",
        enum = {"int64_as_number", "int64_as_string", "int64_as_hexstring"},
    },
    {   description ="default values option",
        type = "string",
        enum = {"auto_default_values", "no_default_values",
                "use_default_values", "use_default_metatable"},
    },
    {   description = "hooks option",
        type = "string",
        enum = {"enable_hooks", "disable_hooks" },
    },
}

local schema = {
    type = "object",
    properties = {
        proto_id  = schema_def.id_schema,
        service = {
            description = "the grpc service name",
            type        = "string"
        },
        method = {
            description = "the method name in the grpc service.",
            type    = "string"
        },
        deadline = {
            description = "deadline for grpc, millisecond",
            type        = "number",
            default     = 0
        },
        pb_option = {
            type = "array",
            items = { type="string", anyOf = pb_option_def },
            minItems = 1,
            default = {
                "enum_as_name",
                "int64_as_number",
                "auto_default_values",
                "disable_hooks",
            }
        },
        show_status_in_body = {
            description = "show decoded grpc-status-details-bin in response body",
            type        = "boolean",
            default     = false
        },
        -- https://github.com/googleapis/googleapis/blob/b7cb84f5d42e6dba0fdcc2d8689313f6a8c9d7b9/
        -- google/rpc/status.proto#L46
        status_detail_type = {
            description = "the message type of the grpc-status-details-bin's details part, "
                            .. "if not given, the details part will not be decoded",
            type        = "string",
        },
    },
    additionalProperties = true,
    required = { "proto_id", "service", "method" },
}

-- Based on https://cloud.google.com/apis/design/errors#handling_errors
local status_rel = {
    ["1"] = 499,    -- CANCELLED
    ["2"] = 500,    -- UNKNOWN
    ["3"] = 400,    -- INVALID_ARGUMENT
    ["4"] = 504,    -- DEADLINE_EXCEEDED
    ["5"] = 404,    -- NOT_FOUND
    ["6"] = 409,    -- ALREADY_EXISTS
    ["7"] = 403,    -- PERMISSION_DENIED
    ["8"] = 429,    -- RESOURCE_EXHAUSTED
    ["9"] = 400,    -- FAILED_PRECONDITION
    ["10"] = 409,   -- ABORTED
    ["11"] = 400,   -- OUT_OF_RANGE
    ["12"] = 501,   -- UNIMPLEMENTED
    ["13"] = 500,   -- INTERNAL
    ["14"] = 503,   -- UNAVAILABLE
    ["15"] = 500,   -- DATA_LOSS
    ["16"] = 401,   -- UNAUTHENTICATED
}

local _M = {
    version = 0.1,
    priority = 506,
    name = plugin_name,
    schema = schema,
}


function _M.init()
    proto.init()
end


function _M.destroy()
    proto.destroy()
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.access(conf, ctx)
    core.log.info("conf: ", core.json.delay_encode(conf))

    local proto_id = conf.proto_id
    if not proto_id then
        core.log.error("proto id miss: ", proto_id)
        return
    end

    local proto_obj, err = proto.fetch(proto_id)
    if err then
        core.log.error("proto load error: ", err)
        return
    end

    local ok, err, err_code = request(proto_obj, conf.service,
                                      conf.method, conf.pb_option, conf.deadline)
    if not ok then
        core.log.error("transform request error: ", err)
        return err_code
    end

    ctx.proto_obj = proto_obj

end


function _M.header_filter(conf, ctx)
    if ngx.status >= 300 then
        return
    end

    ngx.header["Content-Type"] = "application/json"
    ngx.header.content_length = nil

    local headers = ngx.resp.get_headers()

    if headers["grpc-status"] ~= nil and headers["grpc-status"] ~= "0" then
        local http_status = status_rel[headers["grpc-status"]]
        if http_status ~= nil then
            ngx.status = http_status
        else
            ngx.status = 599
        end
    else
        -- The error response body does not contain grpc-status and grpc-message
        ngx.header["Trailer"] = {"grpc-status", "grpc-message"}
    end

end


function _M.body_filter(conf, ctx)
    if ngx.status >= 300 and not conf.show_status_in_body then
        return
    end

    local proto_obj = ctx.proto_obj
    if not proto_obj then
        return
    end

    local err = response(ctx, proto_obj, conf.service, conf.method, conf.pb_option,
                         conf.show_status_in_body, conf.status_detail_type)
    if err then
        core.log.error("transform response error: ", err)
        return
    end
end


return _M
