--
-- 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 plugin          = require("apisix.plugin")
local tostring        = tostring
local http            = require("resty.http")
local log_util        = require("apisix.utils.log-util")
local bp_manager_mod  = require("apisix.utils.batch-processor-manager")
local google_oauth    = require("apisix.utils.google-cloud-oauth")


local lrucache = core.lrucache.new({
    type = "plugin",
})

local plugin_name = "google-cloud-logging"
local batch_processor_manager = bp_manager_mod.new(plugin_name)
local schema = {
    type = "object",
    properties = {
        auth_config = {
            type = "object",
            properties = {
                client_email = { type = "string" },
                private_key = { type = "string" },
                project_id = { type = "string" },
                token_uri = {
                    type = "string",
                    default = "https://oauth2.googleapis.com/token"
                },
                -- https://developers.google.com/identity/protocols/oauth2/scopes#logging
                scope = {
                    type = "array",
                    items = {
                        description = "Google OAuth2 Authorization Scopes",
                        type = "string",
                    },
                    minItems = 1,
                    uniqueItems = true,
                    default = {
                        "https://www.googleapis.com/auth/logging.read",
                        "https://www.googleapis.com/auth/logging.write",
                        "https://www.googleapis.com/auth/logging.admin",
                        "https://www.googleapis.com/auth/cloud-platform"
                    }
                },
                scopes = {
                    type = "array",
                    items = {
                        description = "Google OAuth2 Authorization Scopes",
                        type = "string",
                    },
                    minItems = 1,
                    uniqueItems = true
                },
                entries_uri = {
                    type = "string",
                    default = "https://logging.googleapis.com/v2/entries:write"
                },
            },
            required = { "client_email", "private_key", "project_id", "token_uri" }
        },
        ssl_verify = {
            type = "boolean",
            default = true
        },
        auth_file = { type = "string" },
        -- https://cloud.google.com/logging/docs/reference/v2/rest/v2/MonitoredResource
        resource = {
            type = "object",
            properties = {
                type = { type = "string" },
                labels = { type = "object" }
            },
            default = {
                type = "global"
            },
            required = { "type" }
        },
        -- https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry
        log_id = {
            type = "string",
            default = "apisix.apache.org%2Flogs"
        },
        log_format = {type = "object"},
    },
    oneOf = {
        { required = { "auth_config" } },
        { required = { "auth_file" } },
    },
    encrypt_fields = {"auth_config.private_key"},
}

local metadata_schema = {
    type = "object",
    properties = {
        log_format = {
            type = "object"
        },
        max_pending_entries = {
            type = "integer",
            description = "maximum number of pending entries in the batch processor",
            minimum = 1,
        },
    },
}


local function send_to_google(oauth, entries)
    local http_new = http.new()
    local access_token = oauth:generate_access_token()
    if not access_token then
        return nil, "failed to get google oauth token"
    end

    local res, err = http_new:request_uri(oauth.entries_uri, {
        ssl_verify = oauth.ssl_verify,
        method = "POST",
        body = core.json.encode({
            entries = entries,
            partialSuccess = false,
        }),
        headers = {
            ["Content-Type"] = "application/json",
            ["Authorization"] = (oauth.access_token_type or "Bearer") .. " " .. access_token,
        },
    })

    if not res then
        return nil, "failed to write log to google, " .. err
    end

    if res.status ~= 200 then
        return nil, res.body
    end

    return res.body
end


local function fetch_oauth_conf(conf)
    if conf.auth_config then
        return conf.auth_config
    end

    if not conf.auth_file then
        return nil, "configuration is not defined"
    end

    local file_content, err = core.io.get_file(conf.auth_file)
    if not file_content then
        return nil, "failed to read configuration, file: " .. conf.auth_file .. " err: " .. err
    end

    local config_tab
    config_tab, err = core.json.decode(file_content)
    if not config_tab then
        return nil, "config parse failure, file: " .. conf.auth_file .. ", err: " .. err
    end

    return config_tab
end


local function create_oauth_object(conf)
    local auth_conf, err = fetch_oauth_conf(conf)
    if not auth_conf then
        return nil, err
    end

    auth_conf.scope = auth_conf.scopes or auth_conf.scope

    return google_oauth.new(auth_conf, conf.ssl_verify)
end


local function get_logger_entry(conf, ctx, oauth)
    local entry, customized = log_util.get_log_entry(plugin_name, conf, ctx)
    local google_entry
    if not customized then
        google_entry = {
            httpRequest = {
                requestMethod = entry.request.method,
                requestUrl = entry.request.url,
                requestSize = entry.request.size,
                status = entry.response.status,
                responseSize = entry.response.size,
                userAgent = entry.request.headers and entry.request.headers["user-agent"],
                remoteIp = entry.client_ip,
                serverIp = entry.upstream,
                latency = tostring(core.string.format("%0.3f", entry.latency / 1000)) .. "s"
            },
            jsonPayload = {
                route_id = entry.route_id,
                service_id = entry.service_id,
            },
        }
    else
        google_entry = {
            jsonPayload = entry,
        }
    end

    google_entry.labels = {
        source = "apache-apisix-google-cloud-logging"
    }
    google_entry.timestamp = log_util.get_rfc3339_zulu_timestamp()
    google_entry.resource = conf.resource
    google_entry.insertId = ctx.var.request_id
    google_entry.logName = core.string.format("projects/%s/logs/%s", oauth.project_id, conf.log_id)

    return google_entry
end


local _M = {
    version = 0.1,
    priority = 407,
    name = plugin_name,
    metadata_schema = metadata_schema,
    schema = batch_processor_manager:wrap_schema(schema),
}


function _M.check_schema(conf, schema_type)
    if schema_type == core.schema.TYPE_METADATA then
        return core.schema.check(metadata_schema, conf)
    end

    return core.schema.check(schema, conf)
end


function _M.log(conf, ctx)
    local metadata = plugin.plugin_metadata(plugin_name)
    local max_pending_entries = metadata and metadata.value and
                                metadata.value.max_pending_entries or nil
    local oauth, err = core.lrucache.plugin_ctx(lrucache, ctx, nil,
                                                create_oauth_object, conf)
    if not oauth then
        core.log.error("failed to fetch google-cloud-logging.oauth object: ", err)
        return
    end

    local entry = get_logger_entry(conf, ctx, oauth)

    if batch_processor_manager:add_entry(conf, entry, max_pending_entries) then
        return
    end

    local process = function(entries)
        return send_to_google(oauth, entries)
    end

    batch_processor_manager:add_entry_to_new_processor(conf, entry, ctx,
                                                       process, max_pending_entries)
end


return _M
