/*
 * httpclient_native.cpp — libcurl-multi backend.
 *
 * Single CURLM handle owned by this module. http_post_json / http_get queue
 * an easy handle; http_pump() runs curl_multi_perform and drains finished
 * transfers, firing user callbacks exactly once.
 *
 * Failure modes (DNS, connect refused, timeout, etc.) are surfaced as
 * status=0 in the response — caller doesn't have to distinguish curl error
 * codes from HTTP errors.
 */
#include "httpclient.h"
#include "kobolog.h"

#include <curl/curl.h>
#include <string>
#include <unordered_map>

namespace {

CURLM *g_multi = nullptr;

struct pending_t {
    http_callback_t cb;
    void *user;
    std::string body;
    curl_slist *headers = nullptr;
};

// Keyed by CURL* easy handle so curl_multi_info_read results can be routed.
std::unordered_map<CURL *, pending_t *> g_pending;

size_t write_cb(char *ptr, size_t size, size_t nmemb, void *userdata)
{
    pending_t *p = static_cast<pending_t *>(userdata);
    p->body.append(ptr, size * nmemb);
    return size * nmemb;
}

void start_request(CURL *easy, pending_t *pending)
{
    curl_easy_setopt(easy, CURLOPT_WRITEFUNCTION, write_cb);
    curl_easy_setopt(easy, CURLOPT_WRITEDATA, pending);
    curl_easy_setopt(easy, CURLOPT_TIMEOUT, 10L);
    curl_easy_setopt(easy, CURLOPT_CONNECTTIMEOUT, 5L);
    curl_easy_setopt(easy, CURLOPT_FOLLOWLOCATION, 1L);
    curl_easy_setopt(easy, CURLOPT_USERAGENT, "kobodl/0.5.1");

    g_pending[easy] = pending;
    CURLMcode rc = curl_multi_add_handle(g_multi, easy);
    if(rc != CURLM_OK)
    {
        log_printf(ELOG, "http: curl_multi_add_handle failed: %s\n",
                   curl_multi_strerror(rc));
        // Synthesize a transport failure callback.
        http_response_t resp{0, "", 0};
        pending->cb(&resp, pending->user);
        g_pending.erase(easy);
        if(pending->headers) curl_slist_free_all(pending->headers);
        curl_easy_cleanup(easy);
        delete pending;
    }
}

} // namespace

extern "C" void http_init(void)
{
    if(g_multi) return;
    curl_global_init(CURL_GLOBAL_DEFAULT);
    g_multi = curl_multi_init();
}

extern "C" void http_shutdown(void)
{
    if(!g_multi) return;
    // Abandon any in-flight handles — we're shutting down, callbacks are
    // moot. Free the resources to avoid leak reports.
    for(auto &kv : g_pending)
    {
        curl_multi_remove_handle(g_multi, kv.first);
        if(kv.second->headers) curl_slist_free_all(kv.second->headers);
        curl_easy_cleanup(kv.first);
        delete kv.second;
    }
    g_pending.clear();
    curl_multi_cleanup(g_multi);
    g_multi = nullptr;
    curl_global_cleanup();
}

extern "C" void http_post_json(const char *url, const char *body,
                               http_callback_t cb, void *user)
{
    if(!g_multi || !cb) return;
    CURL *easy = curl_easy_init();
    if(!easy)
    {
        http_response_t resp{0, "", 0};
        cb(&resp, user);
        return;
    }
    auto *pending = new pending_t;
    pending->cb = cb;
    pending->user = user;
    pending->headers = curl_slist_append(nullptr, "Content-Type: application/json");

    curl_easy_setopt(easy, CURLOPT_URL, url);
    curl_easy_setopt(easy, CURLOPT_POST, 1L);
    curl_easy_setopt(easy, CURLOPT_COPYPOSTFIELDS, body ? body : "");
    curl_easy_setopt(easy, CURLOPT_HTTPHEADER, pending->headers);
    start_request(easy, pending);
}

extern "C" void http_get(const char *url, http_callback_t cb, void *user)
{
    if(!g_multi || !cb) return;
    CURL *easy = curl_easy_init();
    if(!easy)
    {
        http_response_t resp{0, "", 0};
        cb(&resp, user);
        return;
    }
    auto *pending = new pending_t;
    pending->cb = cb;
    pending->user = user;

    curl_easy_setopt(easy, CURLOPT_URL, url);
    curl_easy_setopt(easy, CURLOPT_HTTPGET, 1L);
    start_request(easy, pending);
}

extern "C" void http_pump(void)
{
    if(!g_multi) return;
    int running = 0;
    curl_multi_perform(g_multi, &running);

    CURLMsg *msg;
    int msgs_left = 0;
    while((msg = curl_multi_info_read(g_multi, &msgs_left)) != nullptr)
    {
        if(msg->msg != CURLMSG_DONE) continue;

        CURL *easy = msg->easy_handle;
        auto it = g_pending.find(easy);
        if(it == g_pending.end())
        {
            curl_multi_remove_handle(g_multi, easy);
            curl_easy_cleanup(easy);
            continue;
        }
        pending_t *p = it->second;

        long status = 0;
        if(msg->data.result == CURLE_OK)
            curl_easy_getinfo(easy, CURLINFO_RESPONSE_CODE, &status);
        else
            log_printf(DLOG, "http: transport error: %s\n",
                       curl_easy_strerror(msg->data.result));

        http_response_t resp;
        resp.status = (int)status;
        resp.body   = p->body.c_str();
        resp.len    = p->body.size();
        p->cb(&resp, p->user);

        curl_multi_remove_handle(g_multi, easy);
        if(p->headers) curl_slist_free_all(p->headers);
        curl_easy_cleanup(easy);
        g_pending.erase(it);
        delete p;
    }
}
