--
-- 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 unpack         = unpack
local ngx            = ngx
local thread_spawn   = ngx.thread.spawn
local thread_kill    = ngx.thread.kill
local worker_exiting = ngx.worker.exiting
local resty_signal   = require("resty.signal")
local core           = require("apisix.core")
local pipe           = require("ngx.pipe")

local mcp_server_wrapper  = require("apisix.plugins.mcp.server_wrapper")

local schema = {
    type = "object",
    properties = {
        base_uri = {
            type = "string",
            minLength = 1,
            default = "",
        },
        command = {
            type = "string",
            minLength = 1,
        },
        args = {
            type = "array",
            items = {
                type = "string",
            },
            minItems = 0,
        },
    },
    required = {
        "command"
    },
}

local plugin_name = "mcp-bridge"

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


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


local function on_connect(conf, ctx)
    return function(additional)
        local proc, err = pipe.spawn({conf.command, unpack(conf.args or {})})
        if not proc then
            core.log.error("failed to spawn mcp process: ", err)
            return 500
        end
        proc:set_timeouts(nil, 100, 100)
        ctx.mcp_bridge_proc = proc

        local server = additional.server

        -- ngx_pipe is a yield operation, so we no longer need
        -- to explicitly yield to other threads by ngx_sleep
        ctx.mcp_bridge_proc_event_loop = thread_spawn(function ()
            local stdout_partial, stderr_partial, need_exit
            while not worker_exiting() do
                -- read all the messages in stdout's pipe, line by line
                -- if there is an incomplete message it is buffered and
                -- spliced before the next message
                repeat
                    local line, _
                    line, _, stdout_partial = proc:stdout_read_line()
                    if line then
                        local ok, err = server.transport:send(
                            stdout_partial and stdout_partial .. line or line
                        )
                        if not ok then
                            core.log.info("session ", server.session_id,
                                          " exit, failed to send response message: ", err)
                            need_exit = true
                            break
                        end
                        stdout_partial = nil -- luacheck: ignore
                    end
                until not line
                if need_exit then
                    break
                end

                repeat
                    local line, _
                    line, _, stderr_partial = proc:stderr_read_line()
                    if line then
                        local ok, err = server.transport:send(
                           '{"jsonrpc":"2.0","method":"notifications/stderr","params":{"content":"'
                            .. (stderr_partial and stderr_partial .. line or line) .. '"}}')
                        if not ok then
                            core.log.info("session ", server.session_id,
                                          " exit, failed to send response message: ", err)
                            need_exit = true
                            break
                        end
                        stderr_partial = "" -- luacheck: ignore
                    end
                until not line
                if need_exit then
                    break
                end
            end
        end)
    end
end


local function on_client_message(conf, ctx)
    return function(message, additional)
        core.log.info("session ", additional.server.session_id,
                      " send message to mcp server: ", additional.raw)
        ctx.mcp_bridge_proc:write(additional.raw .. "\n")
    end
end


local function on_disconnect(conf, ctx)
    return function()
        if ctx.mcp_bridge_proc_event_loop then
            thread_kill(ctx.mcp_bridge_proc_event_loop)
            ctx.mcp_bridge_proc_event_loop = nil
        end

        local proc = ctx.mcp_bridge_proc
        if proc then
            proc:shutdown("stdin")
            proc:wait()
            local _, err = proc:wait() -- check if process not exited then kill it
            if err ~= "exited" then
                proc:kill(resty_signal.signum("KILL") or 9)
            end
        end
    end
end


function _M.access(conf, ctx)
    return mcp_server_wrapper.access(conf, ctx, {
        event_handler = {
            on_connect = on_connect(conf, ctx),
            on_client_message = on_client_message(conf, ctx),
            on_disconnect = on_disconnect(conf, ctx),
        },
    })
end


return _M
