// http_request.cpp — generic HTTP via cURL CLI (popen).
//
// Issue 0110. Same transport strategy as cpp/functions/core/llm_anthropic.cpp
// (curl in PATH, request body written to tmp file, response captured to tmp
// file, headers + body separated by a marker). No link-time libcurl.
#include "core/http_request.h"

#include <chrono>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <sstream>
#include <string>
#include <vector>

#ifdef _WIN32
#  ifndef NOMINMAX
#    define NOMINMAX
#  endif
#  include <windows.h>
#  include <io.h>
#  include <fcntl.h>
#else
#  include <unistd.h>
#endif

namespace fn_http {

namespace {

// Shell-escape single argument. POSIX uses single quotes; Windows uses
// double quotes since cmd.exe doesn't interpret single quotes.
#ifdef _WIN32
std::string sh_q(const std::string& s) {
    std::string o;
    o.reserve(s.size() + 2);
    o += '"';
    for (char c : s) {
        if (c == '"' || c == '\\') o += '\\';
        o += c;
    }
    o += '"';
    return o;
}
#else
std::string sh_q(const std::string& s) {
    std::string o;
    o.reserve(s.size() + 2);
    o += '\'';
    for (char c : s) {
        if (c == '\'') o += "'\\''";
        else o += c;
    }
    o += '\'';
    return o;
}
#endif

// Make a cross-platform temp file path. On Windows uses GetTempPathW +
// GetTempFileNameW (creates a 0-byte file in %TEMP%). On POSIX uses mkstemp.
std::string make_tmp_path(const char* prefix) {
#ifdef _WIN32
    wchar_t dir_w[MAX_PATH];
    DWORD   n = GetTempPathW(MAX_PATH, dir_w);
    if (n == 0 || n > MAX_PATH) return std::string(prefix) + "_fallback.tmp";

    wchar_t path_w[MAX_PATH];
    UINT    u = GetTempFileNameW(dir_w, L"fn_", 0, path_w);
    if (u == 0) return std::string(prefix) + "_fallback.tmp";

    int need = WideCharToMultiByte(CP_UTF8, 0, path_w, -1,
                                   nullptr, 0, nullptr, nullptr);
    if (need <= 0) return std::string(prefix) + "_fallback.tmp";
    std::string out(need - 1, '\0');
    WideCharToMultiByte(CP_UTF8, 0, path_w, -1, out.data(), need,
                        nullptr, nullptr);
    return out;
#else
    (void)prefix;
    char tmpl[] = "/tmp/fn_XXXXXX";
    int  fd     = mkstemp(tmpl);
    if (fd >= 0) ::close(fd);
    return std::string(tmpl);
#endif
}

#ifdef _WIN32
// Run a command line without spawning a visible console window. Captures
// stdout+stderr combined into out_combined. Returns the child's exit code,
// or -1 on spawn failure.
int run_no_console(const std::string& cmdline_utf8, std::string& out_combined) {
    int  need = MultiByteToWideChar(CP_UTF8, 0, cmdline_utf8.c_str(), -1,
                                    nullptr, 0);
    if (need <= 0) return -1;
    std::wstring wcmd(need, L'\0');
    MultiByteToWideChar(CP_UTF8, 0, cmdline_utf8.c_str(), -1,
                        wcmd.data(), need);

    SECURITY_ATTRIBUTES sa{};
    sa.nLength        = sizeof(sa);
    sa.bInheritHandle = TRUE;

    HANDLE rd = nullptr, wr = nullptr;
    if (!CreatePipe(&rd, &wr, &sa, 0)) return -1;
    SetHandleInformation(rd, HANDLE_FLAG_INHERIT, 0);

    STARTUPINFOW si{};
    si.cb         = sizeof(si);
    si.dwFlags    = STARTF_USESTDHANDLES;
    si.hStdOutput = wr;
    si.hStdError  = wr;
    si.hStdInput  = GetStdHandle(STD_INPUT_HANDLE);

    PROCESS_INFORMATION pi{};
    BOOL ok = CreateProcessW(nullptr, wcmd.data(), nullptr, nullptr,
                             TRUE, CREATE_NO_WINDOW, nullptr, nullptr,
                             &si, &pi);
    CloseHandle(wr);
    if (!ok) {
        CloseHandle(rd);
        return -1;
    }

    char  buf[4096];
    DWORD got = 0;
    while (ReadFile(rd, buf, sizeof(buf), &got, nullptr) && got > 0)
        out_combined.append(buf, buf + got);
    CloseHandle(rd);

    WaitForSingleObject(pi.hProcess, INFINITE);
    DWORD ec = 0;
    GetExitCodeProcess(pi.hProcess, &ec);
    CloseHandle(pi.hProcess);
    CloseHandle(pi.hThread);
    return (int)ec;
}
#endif

// Read entire file into string. Empty on missing.
std::string slurp(const std::string& path) {
    std::string out;
    FILE* f = std::fopen(path.c_str(), "rb");
    if (!f) return out;
    char buf[4096];
    size_t n;
    while ((n = std::fread(buf, 1, sizeof(buf), f)) > 0) out.append(buf, n);
    std::fclose(f);
    return out;
}

// Parse the headers block written by `curl -D <file>`. Each non-empty line
// is "Key: Value". The first line is the HTTP status line ("HTTP/1.1 200 OK")
// which we skip. Folded headers (RFC 7230 obsolete) are not handled — modern
// servers never emit them.
std::vector<std::pair<std::string, std::string>>
parse_headers(const std::string& raw) {
    std::vector<std::pair<std::string, std::string>> out;
    size_t i = 0;
    while (i < raw.size()) {
        size_t eol = raw.find('\n', i);
        if (eol == std::string::npos) eol = raw.size();
        std::string line = raw.substr(i, eol - i);
        // strip trailing \r
        while (!line.empty() && (line.back() == '\r' || line.back() == '\n')) line.pop_back();
        i = eol + 1;
        if (line.empty()) continue;
        // Skip status line(s) (curl emits one per response when following redirects).
        if (line.size() >= 5 && line.compare(0, 5, "HTTP/") == 0) continue;
        size_t colon = line.find(':');
        if (colon == std::string::npos) continue;
        std::string k = line.substr(0, colon);
        std::string v = line.substr(colon + 1);
        // strip leading spaces in value
        size_t vs = 0;
        while (vs < v.size() && (v[vs] == ' ' || v[vs] == '\t')) ++vs;
        v = v.substr(vs);
        out.emplace_back(std::move(k), std::move(v));
    }
    return out;
}

// Extract status from the LAST "HTTP/x.y NNN ..." line in the headers dump.
int parse_status(const std::string& raw) {
    int code = 0;
    size_t i = 0;
    while (i < raw.size()) {
        size_t eol = raw.find('\n', i);
        if (eol == std::string::npos) eol = raw.size();
        if (raw.compare(i, 5, "HTTP/") == 0) {
            size_t sp = raw.find(' ', i);
            if (sp != std::string::npos && sp < eol) {
                int v = std::atoi(raw.c_str() + sp + 1);
                if (v > 0) code = v;
            }
        }
        i = eol + 1;
    }
    return code;
}

// Compute now-ms.
int64_t now_ms() {
    using clock = std::chrono::steady_clock;
    return std::chrono::duration_cast<std::chrono::milliseconds>(
               clock::now().time_since_epoch())
        .count();
}

} // anon

Response request(const Request& req) {
    Response r;
    int64_t  t0 = now_ms();

    if (req.url.empty()) {
        r.error = "http_request: url is empty";
        return r;
    }

    std::string method = req.method.empty() ? std::string("GET") : req.method;

    // Tmp files: request body + response body + response headers.
    std::string tmp_body_in  = make_tmp_path("body_in");
    std::string tmp_body_out = make_tmp_path("body_out");
    std::string tmp_hdr_out  = make_tmp_path("hdr");

    bool have_body = !req.body.empty();
    if (have_body) {
        FILE* f = std::fopen(tmp_body_in.c_str(), "wb");
        if (!f) {
            r.error = "http_request: cannot write tmp body file";
            return r;
        }
        std::fwrite(req.body.data(), 1, req.body.size(), f);
        std::fclose(f);
    }

    // Build curl command.
    //  -sS: silent + show errors
    //  -X <METHOD>
    //  -D <hdr_file>: dump response headers
    //  --max-time <s>: hard timeout (rounded up from ms; min 1s)
    //  -k: insecure (only if requested)
    std::ostringstream cmd;
    cmd << "curl -sS -X " << sh_q(method)
        << " -D " << sh_q(tmp_hdr_out);
    int timeout_s = (req.timeout_ms + 999) / 1000;
    if (timeout_s < 1) timeout_s = 1;
    cmd << " --max-time " << timeout_s;
    if (req.insecure) cmd << " -k";

    // Headers — explicit ones first; shortcuts after so they don't override.
    for (const auto& h : req.headers) {
        std::string line = h.first + ": " + h.second;
        cmd << " -H " << sh_q(line);
    }
    if (!req.bearer_token.empty()) {
        std::string line = "Authorization: Bearer " + req.bearer_token;
        cmd << " -H " << sh_q(line);
    }
    if (!req.basic_user.empty()) {
        // Let curl base64-encode for us via --user.
        std::string up = req.basic_user + ":" + req.basic_pass;
        cmd << " --user " << sh_q(up);
    }

    if (have_body) {
        cmd << " --data-binary @" << sh_q(tmp_body_in);
    }

    cmd << ' ' << sh_q(req.url)
        << " -o " << sh_q(tmp_body_out);

    // On POSIX we go through /bin/sh -c via popen, so `2>&1` is a shell redirect.
    // On Windows we use CreateProcessW (no shell): `2>&1` would be passed as an
    // extra positional arg to curl, which treats it as a second URL → "Bad
    // hostname" (exit 3). stderr is already merged via STARTUPINFOW.hStdError.
#ifndef _WIN32
    cmd << " 2>&1";
#endif

    if (std::getenv("FN_HTTP_DEBUG")) {
        fprintf(stderr, "[fn_http debug] cmdline: %s\n", cmd.str().c_str());
        fprintf(stderr, "[fn_http debug] req.url=[%s] len=%zu\n",
                req.url.c_str(), req.url.size());
    }

    // Capture stderr (curl prints transport errors to stderr with -sS).
    std::string curl_stderr;
    int         rc;
#ifdef _WIN32
    // Use CreateProcessW with CREATE_NO_WINDOW: no console pop-up per call.
    rc = run_no_console(cmd.str(), curl_stderr);
#else
    FILE* p = popen(cmd.str().c_str(), "r");
    if (p) {
        char buf[1024];
        while (fgets(buf, sizeof(buf), p)) curl_stderr.append(buf);
    }
    rc = p ? pclose(p) : -1;
#endif

    // Read response files.
    r.body = slurp(tmp_body_out);
    std::string hdr_raw = slurp(tmp_hdr_out);

    // Cleanup.
    if (have_body) std::remove(tmp_body_in.c_str());
    std::remove(tmp_body_out.c_str());
    std::remove(tmp_hdr_out.c_str());

    r.duration_ms = now_ms() - t0;

    if (rc != 0) {
        // Trim trailing newline of stderr.
        while (!curl_stderr.empty() &&
               (curl_stderr.back() == '\n' || curl_stderr.back() == '\r')) {
            curl_stderr.pop_back();
        }
        r.error = "curl exit " + std::to_string(rc);
        if (!curl_stderr.empty()) r.error += ": " + curl_stderr;
        // status stays 0 -> transport error.
        return r;
    }

    r.status  = parse_status(hdr_raw);
    r.headers = parse_headers(hdr_raw);
    if (r.status == 0) {
        // curl exit 0 but no parseable status line — treat as transport-ish.
        r.error = "http_request: no status line in headers";
    }
    return r;
}

} // namespace fn_http
