/*
 * SessionServerRpc.cpp
 *
 * Copyright (C) 2022 by Posit Software, PBC
 *
 * Unless you have received this program directly from Posit Software pursuant
 * to the terms of a commercial license agreement with Posit Software, then
 * this program is licensed to you under the terms of version 3 of the
 * GNU Affero General Public License. This program is distributed WITHOUT
 * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
 * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
 * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
 *
 */

#include "session/SessionServerRpc.hpp"

#include <string>

#include <core/Thread.hpp>

#include <r/RExec.hpp>
#include <r/RRoutines.hpp>

#include "SessionRpc.hpp"

using namespace rstudio::core;

namespace rstudio {
namespace session {
namespace server_rpc {

namespace overlay {
   bool useHttp();

   Error invokeServerRpc(
      const std::string& endpoint,
      const json::Object& request,
      json::Value* pResult);

   void invokeServerRpcAsync(
      boost::asio::io_context& ioContext,
      const std::string& endpoint,
      const json::Object& request,
      const socket_rpc::RpcResultHandler& onResult,
      const socket_rpc::RpcErrorHandler& onError);
}

namespace {

// invoke an rserver HTTP RPC directly from R.
SEXP rs_invokeServerRpc(SEXP name, SEXP args)
{
   // generate RPC request from this R command
   json::JsonRpcRequest request;
   rpc::formatRpcRequest(name, args, &request);

   // call RPC
   json::Value rpcResult;
   Error error = invokeServerRpc(request.method, request.toJsonObject(), &rpcResult);
   if (error)
   {
      r::exec::error(log::errorAsLogEntry(error));
   }

   // convert result to rpc response
   core::json::JsonRpcResponse response;
   bool success = json::JsonRpcResponse::parse(rpcResult, &response);
   if (!success)
   {
      r::exec::error("Could not parse RPC response");
   }

   // emit formatted response if enabled
   if (!core::system::getenv("RSTUDIO_SESSION_RPC_DEBUG").empty())
   {
      std::cout << "<<<" << std::endl;
      response.getRawResponse().writeFormatted(std::cout);
      std::cout << std::endl;
   }

   // convert JSON response back to R
   SEXP result = R_NilValue;
   r::sexp::Protect protect;
   result = r::sexp::create(rpcResult, &protect);

   // log an R error if the RPC returns an error
   if (response.error().isObject())
   {
      r::exec::error(response.error().write());
   }

   return result;
}

// once flag for lazy initializing async RPC thread
boost::once_flag s_threadOnce = BOOST_ONCE_INIT;

// io_context for performing RPC work on the thread.
//
// Intentionally heap-allocated and never freed. stop() below stops the context
// and joins the worker on the clean exit path, but if a request is wedged in a
// blocking operation that io_context::stop() cannot unblock, joinOrAbandonThread
// detaches the worker after a timeout and lets shutdown proceed. Were this a
// normal global, ~io_context() would then run during static destruction while
// the detached worker is still inside run() -- a documented Windows-IOCP hang
// (asio issue #869, rstudio#18018). Never destroying it removes that teardown
// entirely: the OS reclaims the io_context (and force-terminates the detached
// worker) at process exit, which is well-defined. This is the standard "no
// exit-time destructor" idiom for a process-lifetime global with a non-trivial,
// thread-interacting destructor. The only difference from a normal global is at
// the instant of exit; steady-state behavior is unchanged.
boost::asio::io_context& s_ioContext = *new boost::asio::io_context();

// the RPC worker thread. retained as a joinable handle (rather than launched
// detached) so that stop() can wait for run() to return before the process
// tears s_ioContext down at exit -- see stop().
boost::thread s_rpcWorkerThread;

void rpcWorkerThreadFunc()
{
   auto work = boost::asio::make_work_guard(s_ioContext);
   s_ioContext.run();
}

void ensureRpcWorkerThreadRunning()
{
   // start RPC worker thread if it hasn't already been started
   boost::call_once(s_threadOnce,
                    boost::bind(core::thread::safeLaunchThread,
                                rpcWorkerThreadFunc,
                                &s_rpcWorkerThread));
}

} // anonymous namespace

boost::asio::io_context& ioContext()
{
   ensureRpcWorkerThreadRunning();
   return s_ioContext;
}

void stop()
{
   // The worker thread runs s_ioContext.run() behind a work guard, so it never
   // returns on its own. stop() forces run() to return; we then wait for the
   // thread to actually leave run(). stop() declines to start any further queued
   // work, so when nothing is mid-flight (the common exit case) the worker leaves
   // run() and joins almost immediately. joinOrAbandonThread bounds the wait so
   // we never trade a clean shutdown for an indefinite join hang, and is a no-op
   // if the thread was never started (no async RPC was ever issued).
   //
   // If a request is wedged in a blocking operation that stop() cannot unblock,
   // the join times out and the worker is detached. s_ioContext is never
   // destroyed (see its declaration), so ~io_context() cannot run while that
   // detached worker is still inside run() -- the Windows-IOCP teardown hang
   // this whole dance exists to avoid (rstudio#18018, asio issue #869).
   s_ioContext.stop();
   core::thread::joinOrAbandonThread(
      s_rpcWorkerThread,
      "server RPC worker thread",
      false); // released via s_ioContext.stop() above, not interruptible
}

Error invokeServerRpc(const json::JsonRpcRequest& request, json::JsonRpcResponse* pResponse)
{
   json::Value result;
   Error error = invokeServerRpc(request.method, request.toJsonObject(), &result);
   if (error)
      return error;

   bool success = json::JsonRpcResponse::parse(result, pResponse);
   if (!success)
   {
      error = Error(json::errc::ParseError, ERROR_LOCATION);
      error.addProperty(
         "description",
         "Unable to parse the response for RPC request: " + request.toJsonObject().write());
      error.addProperty("response", result.write());
   }

   return error;
}

Error invokeServerRpc(const std::string& endpoint,
                      const json::Object& request,
                      json::Value* pResult)
{
   if (overlay::useHttp())
   {
      return overlay::invokeServerRpc(endpoint, request, pResult);
   }
   else
   {
#ifdef _WIN32
       return systemError(boost::system::errc::not_supported, ERROR_LOCATION);
#else
      FilePath rpcSocket(core::system::getenv(kServerRpcSocketPathEnvVar));
      LOG_DEBUG_MESSAGE("Invoking rserver RPC '" + endpoint + "' on socket " + 
            rpcSocket.getAbsolutePath());
      return socket_rpc::invokeRpc(rpcSocket, endpoint, request, pResult);
#endif
   }
}

void invokeServerRpcAsync(const std::string& endpoint,
                          const json::Object& request,
                          const socket_rpc::RpcResultHandler& onResult,
                          const socket_rpc::RpcErrorHandler& onError)
{
   // start RPC worker thread if it hasn't already been started
   ensureRpcWorkerThreadRunning();
   if (overlay::useHttp())
   {
      overlay::invokeServerRpcAsync(s_ioContext, endpoint, request, onResult, onError);
   }
   else
   {
#ifdef _WIN32
       onError(systemError(boost::system::errc::not_supported, ERROR_LOCATION));
       return;
#else
      FilePath rpcSocket(core::system::getenv(kServerRpcSocketPathEnvVar));
      LOG_DEBUG_MESSAGE("Invoking rserver async RPC '" + endpoint + "' on socket " + 
            rpcSocket.getAbsolutePath());
      socket_rpc::invokeRpcAsync(s_ioContext,
                                 rpcSocket,
                                 endpoint,
                                 request,
                                 onResult,
                                 onError);
#endif
   }
}

Error initialize()
{
   RS_REGISTER_CALL_METHOD(rs_invokeServerRpc);

   return Success();
}

} // namespace server_rpc
} // namespace session
} // namespace rstudio

