#include "mtran/http_translation_client.hpp"

#include <curl/curl.h>
#include <nlohmann/json.hpp>

#include <algorithm>
#include <string>
#include <utility>

namespace mtran
{
namespace
{

size_t append_response(char * data, size_t size, size_t count, void * output)
{
  const auto byte_count = size * count;
  static_cast<std::string *>(output)->append(data, byte_count);
  return byte_count;
}

bool is_unavailable(CURLcode code)
{
  return code == CURLE_COULDNT_CONNECT ||
         code == CURLE_COULDNT_RESOLVE_HOST ||
         code == CURLE_COULDNT_RESOLVE_PROXY;
}

TranslationResult failure(ClientError error)
{
  return TranslationResult{false, "", error};
}

}  // namespace

HttpTranslationClient::HttpTranslationClient(
  std::string server_url,
  long timeout_ms,
  HttpExecutor executor)
: server_url_(std::move(server_url)),
  timeout_ms_(timeout_ms),
  executor_(std::move(executor))
{
  while (!server_url_.empty() && server_url_.back() == '/') {
    server_url_.pop_back();
  }
  if (!executor_) {
    executor_ = &HttpTranslationClient::execute_with_curl;
  }
}

bool HttpTranslationClient::health()
{
  const auto response = executor_("GET", server_url_ + "/health", "", timeout_ms_);
  return response.transport_code == CURLE_OK &&
         response.status_code >= 200 && response.status_code < 300;
}

TranslationResult HttpTranslationClient::translate(const std::string & text)
{
  const nlohmann::json request = {
    {"from", "en"},
    {"to", "zh-Hans"},
    {"text", text},
    {"html", false},
  };
  const auto response = executor_(
    "POST", server_url_ + "/translate", request.dump(), timeout_ms_);

  const auto transport_code = static_cast<CURLcode>(response.transport_code);
  if (transport_code == CURLE_OPERATION_TIMEDOUT) {
    return failure(ClientError::kTimeout);
  }
  if (is_unavailable(transport_code)) {
    return failure(ClientError::kUnavailable);
  }
  if (transport_code != CURLE_OK || response.status_code < 200 || response.status_code >= 300) {
    return failure(ClientError::kUpstream);
  }

  try {
    const auto parsed = nlohmann::json::parse(response.body);
    if (!parsed.contains("result") || !parsed.at("result").is_string()) {
      return failure(ClientError::kInvalidResponse);
    }
    return TranslationResult{true, parsed.at("result").get<std::string>(), ClientError::kNone};
  } catch (const nlohmann::json::exception &) {
    return failure(ClientError::kInvalidResponse);
  }
}

HttpResponse HttpTranslationClient::execute_with_curl(
  const std::string & method,
  const std::string & url,
  const std::string & body,
  long timeout_ms)
{
  static const auto global_init_code = curl_global_init(CURL_GLOBAL_DEFAULT);
  if (global_init_code != CURLE_OK) {
    return HttpResponse{global_init_code, 0, ""};
  }

  auto * handle = curl_easy_init();
  if (handle == nullptr) {
    return HttpResponse{CURLE_FAILED_INIT, 0, ""};
  }

  std::string response_body;
  curl_slist * headers = nullptr;
  curl_easy_setopt(handle, CURLOPT_URL, url.c_str());
  curl_easy_setopt(handle, CURLOPT_NOSIGNAL, 1L);
  curl_easy_setopt(handle, CURLOPT_CONNECTTIMEOUT_MS, std::min(timeout_ms, 5000L));
  curl_easy_setopt(handle, CURLOPT_TIMEOUT_MS, timeout_ms);
  curl_easy_setopt(handle, CURLOPT_WRITEFUNCTION, append_response);
  curl_easy_setopt(handle, CURLOPT_WRITEDATA, &response_body);

  if (method == "POST") {
    headers = curl_slist_append(headers, "Content-Type: application/json");
    curl_easy_setopt(handle, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(handle, CURLOPT_POST, 1L);
    curl_easy_setopt(handle, CURLOPT_POSTFIELDS, body.c_str());
    curl_easy_setopt(handle, CURLOPT_POSTFIELDSIZE, static_cast<long>(body.size()));
  }

  const auto transport_code = curl_easy_perform(handle);
  long status_code = 0;
  curl_easy_getinfo(handle, CURLINFO_RESPONSE_CODE, &status_code);

  if (headers != nullptr) {
    curl_slist_free_all(headers);
  }
  curl_easy_cleanup(handle);

  return HttpResponse{transport_code, status_code, std::move(response_body)};
}

}  // namespace mtran
