
#include "http_server.h"
#include <chrono>
#include <exception>
#include <oxenmq/base64.h>
#include <boost/endian/conversion.hpp>
#include <oxenmq/variant.h>
#include "common/command_line.h"
#include "common/string_util.h"
#include "cryptonote_config.h"
#include "cryptonote_core/cryptonote_core.h"
#include "epee/net/jsonrpc_structs.h"
#include "rpc/core_rpc_server_commands_defs.h"
#include "rpc/rpc_args.h"
#include "version.h"

#undef OXEN_DEFAULT_LOG_CATEGORY
#define OXEN_DEFAULT_LOG_CATEGORY "daemon.rpc"

namespace cryptonote::rpc {

  const command_line::arg_descriptor<std::vector<std::string>> http_server::arg_rpc_public{
    "rpc-public",
    "Specifies an IP:PORT to listen on for public (restricted) RPC requests; can be specified multiple times."
  };

  const command_line::arg_descriptor<std::vector<std::string>, false, true, 2> http_server::arg_rpc_admin{
    "rpc-admin",
    "Specifies an IP:PORT to listen on for admin (unrestricted) RPC requests; can be specified multiple times. Specify \"none\" to disable.",
    {"127.0.0.1:" + std::to_string(config::RPC_DEFAULT_PORT), "[::1]:" + std::to_string(config::RPC_DEFAULT_PORT)},
    {{ &cryptonote::arg_testnet_on, &cryptonote::arg_devnet_on }},
    [](std::array<bool, 2> testnet_devnet, bool defaulted, std::vector<std::string> val) {
      auto& [testnet, devnet] = testnet_devnet;
      if (defaulted && (testnet || devnet)) {
        auto port = std::to_string(testnet ? config::testnet::RPC_DEFAULT_PORT : config::devnet::RPC_DEFAULT_PORT);
        val = {"127.0.0.1:" + port, "[::1]:" + port};
      }
      return val;
    }
  };

  const command_line::arg_descriptor<uint16_t> http_server::arg_rpc_bind_port = {
      "rpc-bind-port",
      "Port for RPC server; deprecated, use --rpc-public or --rpc-admin instead.",
      0
  };

  const command_line::arg_descriptor<uint16_t> http_server::arg_rpc_restricted_bind_port = {
      "rpc-restricted-bind-port"
    , "Port for restricted RPC server; deprecated, use --rpc-public instead"
    , 0
    };

  const command_line::arg_descriptor<bool> http_server::arg_restricted_rpc = {
      "restricted-rpc"
    , "Deprecated, use --rpc-public instead"
    , false
    };

  // This option doesn't do anything anymore, but keep it here for now in case people added it to
  // config files/startup flags.
  const command_line::arg_descriptor<bool> http_server::arg_public_node = {
      "public-node"
    , "Deprecated; use --rpc-public option instead"
    , false
    };

  namespace { void long_poll_trigger(cryptonote::tx_memory_pool&); }

  //-----------------------------------------------------------------------------------
  void http_server::init_options(boost::program_options::options_description& desc, boost::program_options::options_description& hidden)
  {
    command_line::add_arg(desc, arg_rpc_public);
    command_line::add_arg(desc, arg_rpc_admin);

    command_line::add_arg(hidden, arg_rpc_bind_port);
    command_line::add_arg(hidden, arg_rpc_restricted_bind_port);
    command_line::add_arg(hidden, arg_restricted_rpc);
    command_line::add_arg(hidden, arg_public_node);

    cryptonote::long_poll_trigger = long_poll_trigger;
  }

  //------------------------------------------------------------------------------------------------------------------------------
  http_server::http_server(
      core_rpc_server& server,
      rpc_args rpc_config,
      bool restricted,
      std::vector<std::tuple<std::string, uint16_t, bool>> bind)
    : m_server{server}, m_restricted{restricted}
  {
    // uWS is designed to work from a single thread, which is good (we pull off the requests and
    // then stick them into the LMQ job queue to be scheduled along with other jobs).  But as a
    // consequence, we need to create everything inside that thread.  We *also* need to get the
    // (thread local) event loop pointer back from the thread so that we can shut it down later
    // (injecting a callback into it is one of the few thread-safe things we can do across threads).
    //
    // Things we need in the owning thread, fulfilled from the http thread:

    // - the uWS::Loop* for the event loop thread (which is thread_local).  We can get this during
    //   thread startup, after the thread does basic initialization.
    std::promise<uWS::Loop*> loop_promise;
    auto loop_future = loop_promise.get_future();

    // - the us_listen_socket_t* on which the server is listening.  We can't get this until we
    //   actually start listening, so wait until `start()` for it.  (We also double-purpose it to
    //   send back an exception if one fires during startup).
    std::promise<std::vector<us_listen_socket_t*>> startup_success_promise;
    m_startup_success = startup_success_promise.get_future();

    // Things we need to send from the owning thread to the event loop thread:
    // - a signal when the thread should bind to the port and start the event loop (when we call
    //   start()).
    //m_startup_promise

    m_rpc_thread = std::thread{[this, rpc_config=std::move(rpc_config), bind=std::move(bind)] (
        std::promise<uWS::Loop*> loop_promise,
        std::future<bool> startup_future,
        std::promise<std::vector<us_listen_socket_t*>> startup_success) {
      uWS::App http;
      try {
        create_rpc_endpoints(http);
      } catch (...) {
        loop_promise.set_exception(std::current_exception());
        return;
      }
      loop_promise.set_value(uWS::Loop::get());
      if (!startup_future.get())
        // False means cancel, i.e. we got destroyed/shutdown without start() being called
        return;

      m_login = rpc_config.login;

      m_cors = {rpc_config.access_control_origins.begin(), rpc_config.access_control_origins.end()};

      std::vector<us_listen_socket_t*> listening;
      try {
        bool required_bind_failed = false;
        for (const auto& [addr, port, required] : bind)
          http.listen(addr, port, [&listening, req=required, &required_bind_failed](us_listen_socket_t* sock) {
            if (sock) listening.push_back(sock);
            else if (req) required_bind_failed = true;
          });

        if (listening.empty() || required_bind_failed) {
          std::ostringstream error;
          error << "RPC HTTP server failed to bind; ";
          if (listening.empty()) error << "no valid bind address(es) given";
          error << "tried to bind to:";
          for (const auto& [addr, port, required] : bind)
            error << ' ' << addr << ':' << port;
          throw std::runtime_error(error.str());
        }
      } catch (...) {
        startup_success.set_exception(std::current_exception());
        return;
      }
      startup_success.set_value(std::move(listening));

      http.run();
    }, std::move(loop_promise), m_startup_promise.get_future(), std::move(startup_success_promise)};

    m_loop = loop_future.get();
  }

  void http_server::create_rpc_endpoints(uWS::App& http)
  {
    auto access_denied = [this](HttpResponse* res, HttpRequest* req) {
      MINFO("Forbidden HTTP request for restricted endpoint " << req->getMethod() << " " << req->getUrl());
      error_response(*res, HTTP_FORBIDDEN);
    };

    for (auto& [name, call] : rpc_commands) {
      if (call->is_legacy || call->is_binary) {
        if (!call->is_public && m_restricted)
          http.any("/" + name, access_denied);
        else
          http.any("/" + name, [this, &call=*call](HttpResponse* res, HttpRequest* req) {
            if (m_login && !check_auth(*req, *res))
              return;
            handle_base_request(*res, *req, call);
          });
      }
    }
    http.post("/json_rpc", [this](HttpResponse* res, HttpRequest* req) {
      if (m_login && !check_auth(*req, *res))
        return;
      handle_json_rpc_request(*res, *req);
    });

    // Fallback to send a 404 for anything else:
    http.any("/*", [this](HttpResponse* res, HttpRequest* req) {
      if (m_login && !check_auth(*req, *res))
        return;
      MINFO("Invalid HTTP request for " << req->getMethod() << " " << req->getUrl());
      error_response(*res, HTTP_NOT_FOUND);
    });
  }

  namespace {

  struct call_data {
    http_server& http;
    core_rpc_server& core_rpc;
    HttpResponse& res;
    std::string uri;
    const rpc_command* call{nullptr};
    rpc_request request{};
    bool aborted{false};
    bool replied{false};
    bool jsonrpc{false};
    std::string jsonrpc_id; // pre-formatted json value
    std::vector<std::pair<std::string, std::string>> extra_headers; // Extra headers to send

    // If we have to drop the request because we are overloaded we want to reply with an error (so
    // that we close the connection instead of leaking it and leaving it hanging).  We don't do
    // this, of course, if the request got aborted and replied to.
    ~call_data() {
      if (replied || aborted) return;
      http.loop_defer([&http=http, &res=res, jsonrpc=jsonrpc] {
        if (jsonrpc)
          http.jsonrpc_error_response(res, -32003, "Server busy, try again later");
        else
          http.error_response(res, http_server::HTTP_SERVICE_UNAVAILABLE, "Server busy, try again later");
      });
    }

    call_data(const call_data&) = delete;
    call_data(call_data&&) = delete;
    call_data& operator=(const call_data&) = delete;
    call_data& operator=(call_data&&) = delete;

    // Wrappers around .http.jsonrpc_error_response and .http.error_response that do nothing if the
    // request is already replied to, and otherwise set `replied` and forward everything passed in
    // to http.<method>(...).
    template <typename... T>
    auto jsonrpc_error_response(T&&... args) {
        if (replied || aborted) return;
        replied = true;
        return http.jsonrpc_error_response(std::forward<T>(args)...);
    }
    template <typename... T>
    auto error_response(T&&... args) {
        if (replied || aborted) return;
        replied = true;
        return http.error_response(std::forward<T>(args)...);
    }
  };

  // Queues a response for the HTTP thread to handle; the response can be in multiple string pieces
  // to be concatenated together.
  void queue_response(std::shared_ptr<call_data> data, std::vector<std::string> body)
  {
    auto& http = data->http;
    data->replied = true;
    http.loop_defer([data=std::move(data), body=std::move(body)] {
      if (data->aborted)
        return;
      data->res.cork([data=std::move(data), body=std::move(body)] {
        auto& res = data->res;
        res.writeHeader("Server", data->http.server_header());
        res.writeHeader("Content-Type", data->call->is_binary ? "application/octet-stream"sv : "application/json"sv);
        if (data->http.closing()) res.writeHeader("Connection", "close");
        for (const auto& [name, value] : data->extra_headers)
          res.writeHeader(name, value);

        for (const auto& piece : body)
          res.write(piece);

        res.end();
        if (data->http.closing()) res.close();
      });
    });
  }

  // Wrapper around the above that takes a single string
  void queue_response(std::shared_ptr<call_data> data, std::string body)
  {
    std::vector<std::string> b;
    b.push_back(std::move(body));
    queue_response(std::move(data), std::move(b));
  }

  void invoke_txpool_hashes_bin(std::shared_ptr<call_data> data);

  // Invokes the actual RPC request; this is called (via oxenmq) from some random LMQ worker thread,
  // which means we can't just write our reply; instead we have to post it to the uWS loop.
  void invoke_rpc(std::shared_ptr<call_data> dataptr)
  {
    auto& data = *dataptr;
    if (data.aborted) return;

    // Replace the default tx pool hashes callback with our own (which adds long poll support):
    if (std::string_view{data.uri}.substr(1) == rpc::GET_TRANSACTION_POOL_HASHES_BIN::names()[0])
      return invoke_txpool_hashes_bin(std::move(dataptr));

    const bool time_logging = LOG_ENABLED(Debug);
    std::chrono::steady_clock::time_point start;
    if (time_logging)
      start = std::chrono::steady_clock::now();

    std::vector<std::string> result;
    result.reserve(data.jsonrpc ? 3 : 1);
    if (data.jsonrpc)
    {
      result.emplace_back(R"({"jsonrpc":"2.0","id":)");
      result.back() += data.jsonrpc_id;
      result.back() += R"(,"result":)";
    }

    int json_error = -32603;
    std::string json_message = "Internal error";
    std::string http_message;

    try {
      result.push_back(data.call->invoke(std::move(data.request), data.core_rpc));
      json_error = 0;
    } catch (const parse_error& e) {
      // This isn't really WARNable as it's the client fault; log at info level instead.
      MINFO("HTTP RPC request '" << data.uri << "' called with invalid/unparseable data: " << e.what());
      json_error = -32602;
      http_message = "Unable to parse request: "s + e.what();
      json_message = "Invalid params";
    } catch (const rpc_error& e) {
      MWARNING("HTTP RPC request '" << data.uri << "' failed with: " << e.what());
      json_error = e.code;
      json_message = e.message;
      http_message = e.message;
    } catch (const std::exception& e) {
      MWARNING("HTTP RPC request '" << data.uri << "' raised an exception: " << e.what());
    } catch (...) {
      MWARNING("HTTP RPC request '" << data.uri << "' raised an unknown exception");
    }

    if (json_error != 0) {
      data.http.loop_defer([data=std::move(dataptr), json_error, msg=std::move(data.jsonrpc ? json_message : http_message)] {
        if (data->jsonrpc)
          data->jsonrpc_error_response(data->res, json_error, msg);
        else
          data->error_response(data->res, http_server::HTTP_ERROR, msg.empty() ? std::nullopt : std::make_optional<std::string_view>(msg));
      });
      return;
    }

    if (data.jsonrpc)
      result.emplace_back("}\n");

    std::string call_duration;
    if (time_logging)
      call_duration = " in " + tools::friendly_duration(std::chrono::steady_clock::now() - start);
    if (LOG_ENABLED(Info)) {
      size_t bytes = 0;
      for (const auto& r : result) bytes += r.size();
      MINFO("HTTP RPC " << data.uri << " [" << data.request.context.remote << "] OK (" << bytes << " bytes)" << call_duration);
    }

    queue_response(std::move(dataptr), std::move(result));
  }

  std::string pool_hashes_response(std::vector<crypto::hash>&& pool_hashes) {
    GET_TRANSACTION_POOL_HASHES_BIN::response res{};
    res.tx_hashes = std::move(pool_hashes);
    res.status = STATUS_OK;

    std::string response;
    epee::serialization::store_t_to_binary(res, response);
    return response;
  }

  std::list<std::pair<std::shared_ptr<call_data>, std::chrono::steady_clock::time_point>> long_pollers;
  std::mutex long_poll_mutex;

  // HTTP-only long-polling support for the transaction pool hashes command
  void invoke_txpool_hashes_bin(std::shared_ptr<call_data> data) {
    GET_TRANSACTION_POOL_HASHES_BIN::request req{};
    std::string_view body;
    if (auto body_sv = data->request.body_view())
      body = *body_sv;
    else
      throw parse_error{"Internal error: got unexpected request body type"};

    if (!epee::serialization::load_t_from_binary(req, body))
      throw parse_error{"Failed to parse binary data parameters"};

    std::vector<crypto::hash> pool_hashes;
    data->core_rpc.get_core().get_pool().get_transaction_hashes(pool_hashes, data->request.context.admin, req.blinked_txs_only /*include_only_blinked*/);

    if (req.long_poll)
    {
      crypto::hash checksum{};
      for (const auto& h : pool_hashes) checksum ^= h;

      if (req.tx_pool_checksum == checksum) {
        // Hashes match, which means we need to defer this request until later.
        std::lock_guard lock{long_poll_mutex};
        MTRACE("Deferring long poll request from " << data->request.context.remote << ": long polling requested and remote's checksum matches current pool (" << checksum << ")");
        long_pollers.emplace_back(std::move(data), std::chrono::steady_clock::now() + GET_TRANSACTION_POOL_HASHES_BIN::long_poll_timeout);
        return;
      }

      MTRACE("Ignoring long poll request from " << data->request.context.remote << ": pool hash mismatch (remote: " << req.tx_pool_checksum << ", local: " << checksum << ")");
    }

    // Either not a long poll request or checksum didn't match
    queue_response(std::move(data), pool_hashes_response(std::move(pool_hashes)));
  }

  // This get invoked (from cryptonote_core.cpp) whenever the mempool is added to.  We queue
  // responses for everyone currently waiting.
  void long_poll_trigger(tx_memory_pool &pool) {
    std::lock_guard lock{long_poll_mutex};
    if (long_pollers.empty())
      return;

    MDEBUG("TX pool changed; sending tx pool to " << long_pollers.size() << " pending long poll connections");

    std::optional<std::string> body_public, body_admin;

    for (auto& [dataptr, expiry]: long_pollers)
    {
      auto& data = *dataptr;
      auto& body = data.request.context.admin ? body_admin : body_public;
      if (!body)
      {
        std::vector<crypto::hash> pool_hashes;
        pool.get_transaction_hashes(pool_hashes, data.request.context.admin, true /*include_only_blinked*/);
        body = pool_hashes_response(std::move(pool_hashes));
      }
      MTRACE("Sending deferred long poll pool update to " << data.request.context.remote);
      queue_response(std::move(dataptr), *body);
    }
    long_pollers.clear();
  }

  std::string long_poll_timeout_body;

  // Called periodically to clear expired Starts up a periodic timer for checking for expired long poll requests.  We run this only once
  // a second because we don't really care if we time out at *precisely* 15 seconds.
  void long_poll_process_timeouts() {
    std::lock_guard lock{long_poll_mutex};

    if (long_pollers.empty())
      return;

    if (long_poll_timeout_body.empty())
    {
      GET_TRANSACTION_POOL_HASHES_BIN::response res{};
      res.status = STATUS_TX_LONG_POLL_TIMED_OUT;
      epee::serialization::store_t_to_binary(res, long_poll_timeout_body);
    }

    int count = 0;
    auto now = std::chrono::steady_clock::now();
    for (auto it = long_pollers.begin(); it != long_pollers.end(); )
    {
      if (it->second < now)
      {
        MTRACE("Sending long poll timeout to " << it->first->request.context.remote);
        queue_response(std::move(it->first), long_poll_timeout_body);
        it = long_pollers.erase(it);
        count++;
      }
      else
        ++it;
    }

    if (count > 0)
      MDEBUG("Timed out " << count << " long poll connections");
    else
      MTRACE("None of " << long_pollers.size() << " established long poll connections reached timeout");
  }

  } // anonymous namespace

  void http_server::handle_base_request(
        HttpResponse& res,
        HttpRequest& req,
        const rpc_command& call)
  {
    std::shared_ptr<call_data> data{new call_data{*this, m_server, res, std::string{req.getUrl()}, &call}};
    auto& request = data->request;
    request.body = ""s;
    request.context.admin = !m_restricted;
    request.context.source = rpc_source::http;
    request.context.remote = get_remote_address(res);
    handle_cors(req, data->extra_headers);
    MTRACE("Received " << req.getMethod() << " " << req.getUrl() << " request from " << request.context.remote);

    res.onAborted([data] { data->aborted = true; });
    res.onData([data=std::move(data)](std::string_view d, bool done) mutable {
      var::get<std::string>(data->request.body) += d;
      if (!done)
        return;

      auto& omq = data->core_rpc.get_core().get_omq();
      std::string cat{data->call->is_public ? "rpc" : "admin"};
      std::string cmd{"http:" + data->uri}; // Used for LMQ job logging; prefixed with http: so we can distinguish it
      std::string remote{data->request.context.remote};
      omq.inject_task(std::move(cat), std::move(cmd), std::move(remote), [data=std::move(data)] { invoke_rpc(std::move(data)); });
    });
  }

  void http_server::handle_json_rpc_request(HttpResponse& res, HttpRequest& req)
  {
    std::shared_ptr<call_data> data{new call_data{*this, m_server, res, std::string{req.getUrl()}}};
    data->jsonrpc = true;
    auto& request = data->request;
    request.context.admin = !m_restricted;
    request.context.source = rpc_source::http;
    request.context.remote = get_remote_address(res);
    handle_cors(req, data->extra_headers);

    res.onAborted([data] { data->aborted = true; });
    res.onData([buffer=""s, data, restricted=m_restricted](std::string_view d, bool done) mutable {
      if (!done) {
        buffer += d;
        return;
      }

      std::string_view body;
      if (buffer.empty())
        body = d; // bypass copying the string_view to a string
      else
        body = (buffer += d);

      auto& [ps, st_entry] = var::get<jsonrpc_params>(data->request.body = jsonrpc_params{});
      if(!ps.load_from_json(body))
        return data->jsonrpc_error_response(data->res, -32700, "Parse error");

      epee::serialization::storage_entry id{std::string{}};
      ps.get_value("id", id, nullptr);

      std::string method;
      if(!ps.get_value("method", method, nullptr))
      {
        MINFO("Invalid JSON RPC request from " << data->request.context.remote << ": no 'method' in request");
        return data->jsonrpc_error_response(data->res, -32600, "Invalid Request", id);
      }

      auto it = rpc_commands.find(method);
      if (it == rpc_commands.end() || it->second->is_binary)
      {
        MINFO("Invalid JSON RPC request from " << data->request.context.remote << ": method '" << method << "' is invalid");
        return data->jsonrpc_error_response(data->res, -32601, "Method not found", id);
      }

      data->call = it->second.get();
      if (restricted && !data->call->is_public)
      {
        MWARNING("Invalid JSON RPC request from " << data->request.context.remote << ": method '" << method << "' is restricted");
        return data->jsonrpc_error_response(data->res, 403, "Forbidden; this command is not available over public RPC", id);
      }

      MDEBUG("Incoming JSON RPC request for " << method << " from " << data->request.context.remote);

      {
        std::ostringstream o;
        epee::serialization::dump_as_json(o, id, 0 /*indent*/, false /*newlines*/);
        data->jsonrpc_id = o.str();
      }

      // Try to load "params" into a generic epee value; if it fails (because there is no "params")
      // then we replace request.body with an empty string (instead of the epee jsonrpc_params
      // alternative) to signal that no params were provided at all.
      if (!ps.get_value("params", st_entry, nullptr))
        data->request.body = ""sv;

      auto& omq = data->core_rpc.get_core().get_omq();
      std::string cat{data->call->is_public ? "rpc" : "admin"};
      std::string cmd{"jsonrpc:" + method}; // Used for LMQ job logging; prefixed with jsonrpc: so we can distinguish it
      std::string remote{data->request.context.remote};
      omq.inject_task(std::move(cat), std::move(cmd), std::move(remote), [data=std::move(data)] { invoke_rpc(std::move(data)); });
    });
  }

  static std::unordered_set<oxenmq::OxenMQ*> timer_started;

  void http_server::start()
  {
    if (m_sent_startup)
      throw std::logic_error{"Cannot call http_server::start() more than once"};

    auto net = m_server.nettype();
    m_server_header = "oxend/"s + (m_restricted ? std::to_string(OXEN_VERSION[0]) : std::string{OXEN_VERSION_FULL})
      + (net == MAINNET ? " mainnet" : net == TESTNET ? " testnet" : net == DEVNET ? " devnet" : net == FAKECHAIN ? " fakenet" : " unknown net");

    m_startup_promise.set_value(true);
    m_sent_startup = true;
    m_listen_socks = m_startup_success.get();

    auto& omq = m_server.get_core().get_omq();
    if (timer_started.insert(&omq).second)
      omq.add_timer(long_poll_process_timeouts, 1s);
  }

  void http_server::shutdown(bool join)
  {
    if (!m_rpc_thread.joinable())
      return;

    if (!m_sent_shutdown)
    {
      MTRACE("initiating shutdown");
      if (!m_sent_startup)
      {
        m_startup_promise.set_value(false);
        m_sent_startup = true;
      }
      else if (!m_listen_socks.empty())
      {
        loop_defer([this] {
          MTRACE("closing " << m_listen_socks.size() << " listening sockets");
          for (auto* s : m_listen_socks)
            us_listen_socket_close(/*ssl=*/false, s);
          m_listen_socks.clear();

          m_closing = true;

          {
            // Destroy any pending long poll connections as well
            MTRACE("closing pending long poll requests");
            std::lock_guard lock{long_poll_mutex};
            for (auto it = long_pollers.begin(); it != long_pollers.end(); )
            {
              if (&it->first->http != this)
                continue; // Belongs to some other http_server instance
              it->first->aborted = true;
              it->first->res.close();
              it = long_pollers.erase(it);
            }
          }
        });
      }
      m_sent_shutdown = true;
    }

    MTRACE("joining rpc thread");
    if (join)
      m_rpc_thread.join();
    MTRACE("done shutdown");
  }

  http_server::~http_server()
  {
    shutdown(true);
  }


} // namespace cryptonote::rpc
