// sse_client.cpp — Server-Sent Events client via curl -N (fork+exec direct).
//
// Transport: background thread fork+exec curl DIRECTLY (no /bin/sh wrapper)
// so that the child PID is the curl process itself. This lets stop() call
// kill(curl_pid, SIGTERM) and have fgets() return NULL immediately.
//
// Using /bin/sh -c "curl ..." hides the real PID (it's the grandchild) and
// kill(sh_pid) leaves curl orphaned → fgets() never returns → join() hangs.
//
// No link-time libcurl. curl must be in PATH at runtime.
// Same transport strategy as http_request_cpp_core and llm_anthropic_cpp_core.
#include "core/sse_client.h"

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

#ifndef _WIN32
#include <csignal>
#include <fcntl.h>
#include <sys/wait.h>
#include <unistd.h>
#else
#  ifndef NOMINMAX
#    define NOMINMAX
#  endif
#  include <windows.h>
#  include <io.h>
#  include <fcntl.h>
#endif

namespace fn_sse {

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

namespace {

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

void sleep_ms(int ms) {
    std::this_thread::sleep_for(std::chrono::milliseconds(ms));
}

// Strip trailing \r and \n.
void rstrip_newline(std::string& s) {
    while (!s.empty() && (s.back() == '\r' || s.back() == '\n'))
        s.pop_back();
}

int parse_int_field(const std::string& s) {
    size_t i = 0;
    while (i < s.size() && (s[i] == ' ' || s[i] == '\t')) ++i;
    if (i == s.size()) return 0;
    char* end = nullptr;
    long  v   = std::strtol(s.c_str() + i, &end, 10);
    return (end && end != s.c_str() + i) ? (int)v : 0;
}

// Parse one SSE line into the event buffer.
void parse_sse_line(const std::string& line, Event& buf, bool& should_flush) {
    should_flush = false;

    if (line.empty()) {
        should_flush = true;
        return;
    }

    if (line[0] == ':') return;  // comment

    size_t colon = line.find(':');
    std::string field, value;
    if (colon == std::string::npos) {
        field = line;
        value = "";
    } else {
        field = line.substr(0, colon);
        value = line.substr(colon + 1);
        if (!value.empty() && value[0] == ' ') value = value.substr(1);
    }

    if (field == "event") {
        buf.event = value;
    } else if (field == "data") {
        if (!buf.data.empty()) buf.data += '\n';
        buf.data += value;
    } else if (field == "id") {
        if (value.find('\0') == std::string::npos)
            buf.id = value;
    } else if (field == "retry") {
        int ms = parse_int_field(value);
        if (ms > 0) buf.retry_ms = ms;
    }
}

// Dispatch buffered event, reset mutable fields.
void flush_event(Event& buf, const Client::EventHandler& cb) {
    if (buf.data.empty()) {
        buf.event = "";
        return;
    }
    if (buf.event.empty()) buf.event = "message";
    if (!buf.data.empty() && buf.data.back() == '\n') buf.data.pop_back();
    if (cb) cb(buf);
    std::string saved_id  = buf.id;
    int         saved_rms = buf.retry_ms;
    buf          = Event{};
    buf.id       = saved_id;
    buf.retry_ms = saved_rms;
}

#ifndef _WIN32
// Build argv vector for exec. Returns vector of char* pointing into `strs`.
// The last element is nullptr.
struct CurlArgs {
    std::vector<std::string>  strs;
    std::vector<const char*>  argv;

    void add(const std::string& s) { strs.push_back(s); }

    void build() {
        argv.clear();
        for (const auto& s : strs) argv.push_back(s.c_str());
        argv.push_back(nullptr);
    }
};
#endif

} // anon

// ---------------------------------------------------------------------------
// Impl
// ---------------------------------------------------------------------------

struct Client::Impl {
    std::thread       thread_;
    std::atomic<bool> stop_requested_{false};
    std::atomic<bool> running_{false};

#ifndef _WIN32
    std::atomic<pid_t> curl_pid_{0};
#else
    // HANDLEs to the curl child process (and its primary thread) so stop()
    // can TerminateProcess and the cleanup block can close them. void* to
    // avoid leaking <windows.h> into the header struct via friend lookup.
    void* curl_proc_handle_   = nullptr;  // HANDLE
    void* curl_proc_thread_   = nullptr;  // HANDLE
#endif

    void run(Config cfg, EventHandler on_event, StatusHandler on_status) {
        running_ = true;

        int         backoff_ms = cfg.reconnect_initial_ms;
        std::string last_id;

        auto emit_status = [&](const std::string& s) {
            if (on_status) on_status(s);
        };

        while (!stop_requested_) {
            emit_status("connecting");

#ifndef _WIN32
            // Build curl args directly — no shell wrapper, so PID == curl PID.
            CurlArgs args;
            args.add("curl");
            args.add("-N");              // no output buffering
            args.add("-sS");             // silent + show errors
            args.add("--max-time");
            args.add("0");               // no global timeout (stream is indefinite)
            args.add("--connect-timeout");
            args.add(std::to_string(cfg.connect_timeout_ms / 1000 + 1));
            args.add("-H"); args.add("Accept: text/event-stream");
            args.add("-H"); args.add("Cache-Control: no-cache");

            if (!cfg.bearer_token.empty()) {
                args.add("-H");
                args.add("Authorization: Bearer " + cfg.bearer_token);
            }
            if (!last_id.empty()) {
                args.add("-H");
                args.add("Last-Event-ID: " + last_id);
            }
            args.add(cfg.url);
            args.build();

            // Fork+pipe: child exec's curl, parent reads stdout.
            int pipefd[2];
            if (::pipe(pipefd) != 0) {
                emit_status("error: pipe() failed");
                if (!cfg.auto_reconnect || stop_requested_) break;
                sleep_ms(backoff_ms);
                backoff_ms = std::min(backoff_ms * 2, cfg.reconnect_max_ms);
                continue;
            }

            pid_t child = ::fork();
            if (child < 0) {
                ::close(pipefd[0]);
                ::close(pipefd[1]);
                emit_status("error: fork() failed");
                if (!cfg.auto_reconnect || stop_requested_) break;
                sleep_ms(backoff_ms);
                backoff_ms = std::min(backoff_ms * 2, cfg.reconnect_max_ms);
                continue;
            }

            if (child == 0) {
                // Child: redirect stdout to pipe write-end, stderr to /dev/null.
                ::close(pipefd[0]);
                ::dup2(pipefd[1], STDOUT_FILENO);
                ::close(pipefd[1]);
                int devnull = ::open("/dev/null", O_WRONLY);
                if (devnull >= 0) {
                    ::dup2(devnull, STDERR_FILENO);
                    ::close(devnull);
                }
                // exec curl directly — no sh wrapper.
                ::execvp("curl", const_cast<char* const*>(args.argv.data()));
                ::_exit(127);  // exec failed
            }

            // Parent: close write-end, keep read-end.
            ::close(pipefd[1]);
            curl_pid_ = child;
            FILE* pipe_file = ::fdopen(pipefd[0], "r");

            if (!pipe_file) {
                ::close(pipefd[0]);
                ::kill(child, SIGTERM);
                int st; ::waitpid(child, &st, 0);
                curl_pid_ = 0;
                emit_status("error: fdopen() failed");
                if (!cfg.auto_reconnect || stop_requested_) break;
                sleep_ms(backoff_ms);
                backoff_ms = std::min(backoff_ms * 2, cfg.reconnect_max_ms);
                continue;
            }
#else
            // Windows: CreateProcessW with CREATE_NO_WINDOW so no console
            // pop-up per call. We keep a HANDLE to the child for stop().
            std::ostringstream cmd_n;
            cmd_n << "curl.exe -N -sS --max-time 0 --connect-timeout "
                  << (cfg.connect_timeout_ms / 1000 + 1)
                  << " -H \"Accept: text/event-stream\""
                  << " -H \"Cache-Control: no-cache\"";
            if (!cfg.bearer_token.empty())
                cmd_n << " -H \"Authorization: Bearer " << cfg.bearer_token << "\"";
            if (!last_id.empty())
                cmd_n << " -H \"Last-Event-ID: " << last_id << "\"";
            cmd_n << " \"" << cfg.url << "\"";

            std::string cmdu = cmd_n.str();
            int         needw = MultiByteToWideChar(CP_UTF8, 0, cmdu.c_str(),
                                                   -1, nullptr, 0);
            if (needw <= 0) {
                emit_status("error: MultiByteToWideChar failed");
                if (!cfg.auto_reconnect || stop_requested_) break;
                sleep_ms(backoff_ms);
                backoff_ms = std::min(backoff_ms * 2, cfg.reconnect_max_ms);
                continue;
            }
            std::wstring wcmd(needw, L'\0');
            MultiByteToWideChar(CP_UTF8, 0, cmdu.c_str(), -1,
                                wcmd.data(), needw);

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

            HANDLE rd = nullptr, wr = nullptr;
            if (!CreatePipe(&rd, &wr, &sa, 0)) {
                emit_status("error: CreatePipe failed");
                if (!cfg.auto_reconnect || stop_requested_) break;
                sleep_ms(backoff_ms);
                backoff_ms = std::min(backoff_ms * 2, cfg.reconnect_max_ms);
                continue;
            }
            SetHandleInformation(rd, HANDLE_FLAG_INHERIT, 0);

            STARTUPINFOW si{};
            si.cb         = sizeof(si);
            si.dwFlags    = STARTF_USESTDHANDLES;
            si.hStdOutput = wr;
            // Discard stderr to /dev/null equivalent: route to our own
            // STDERR (parent process) which on Windows GUI apps is detached.
            si.hStdError  = GetStdHandle(STD_ERROR_HANDLE);
            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);
                emit_status("error: CreateProcessW failed");
                if (!cfg.auto_reconnect || stop_requested_) break;
                sleep_ms(backoff_ms);
                backoff_ms = std::min(backoff_ms * 2, cfg.reconnect_max_ms);
                continue;
            }

            curl_proc_handle_ = pi.hProcess;
            curl_proc_thread_ = pi.hThread;

            // Wrap read-end HANDLE as a FILE* so the existing fgets-based
            // SSE parser loop keeps working unchanged.
            int   fd = _open_osfhandle((intptr_t)rd, _O_RDONLY);
            FILE* pipe_file = (fd >= 0) ? _fdopen(fd, "r") : nullptr;
            if (!pipe_file) {
                if (fd >= 0) _close(fd);
                else         CloseHandle(rd);
                TerminateProcess(pi.hProcess, 1);
                CloseHandle(pi.hProcess);
                CloseHandle(pi.hThread);
                curl_proc_handle_ = nullptr;
                curl_proc_thread_ = nullptr;
                emit_status("error: _fdopen failed");
                if (!cfg.auto_reconnect || stop_requested_) break;
                sleep_ms(backoff_ms);
                backoff_ms = std::min(backoff_ms * 2, cfg.reconnect_max_ms);
                continue;
            }
#endif

            // ---------------------------------------------------------------
            // Read loop — parse SSE lines as they arrive from curl.
            // ---------------------------------------------------------------
            Event buf;
            buf.id = last_id;
            bool connected_emitted = false;
            char line_buf[8192];

            while (!stop_requested_) {
                if (!fgets(line_buf, sizeof(line_buf), pipe_file)) break;

                if (!connected_emitted) {
                    emit_status("connected");
                    connected_emitted = true;
                    backoff_ms = cfg.reconnect_initial_ms;  // reset on success
                }

                std::string line(line_buf);
                rstrip_newline(line);

                bool flush = false;
                parse_sse_line(line, buf, flush);

                if (flush) {
                    if (buf.retry_ms > 0) cfg.reconnect_initial_ms = buf.retry_ms;
                    if (!buf.id.empty()) last_id = buf.id;
                    flush_event(buf, on_event);
                }
            }

            // ---------------------------------------------------------------
            // Cleanup: kill child (in case we exited due to stop_requested_)
            // then wait to avoid zombie.
            // ---------------------------------------------------------------
#ifndef _WIN32
            {
                pid_t p = curl_pid_.exchange(0);
                if (p > 0) {
                    ::kill(p, SIGTERM);
                    // Drain to let curl finish writing and avoid SIGPIPE.
                    char drain[256];
                    while (fgets(drain, sizeof(drain), pipe_file)) {}
                }
                fclose(pipe_file);
                if (p > 0) {
                    int st = 0;
                    ::waitpid(p, &st, 0);
                }
            }
#else
            if (curl_proc_handle_) {
                TerminateProcess((HANDLE)curl_proc_handle_, 0);
                WaitForSingleObject((HANDLE)curl_proc_handle_, 1000);
                CloseHandle((HANDLE)curl_proc_handle_);
                CloseHandle((HANDLE)curl_proc_thread_);
                curl_proc_handle_ = nullptr;
                curl_proc_thread_ = nullptr;
            }
            fclose(pipe_file);  // also closes the underlying HANDLE via fd
#endif

            if (stop_requested_) break;

            emit_status("disconnected");

            if (!cfg.auto_reconnect) break;

            int64_t wait_until = now_ms() + backoff_ms;
            while (!stop_requested_ && now_ms() < wait_until)
                sleep_ms(50);

            backoff_ms = std::min(backoff_ms * 2, cfg.reconnect_max_ms);
        }

        emit_status("disconnected");
        running_ = false;
    }
};

// ---------------------------------------------------------------------------
// Client
// ---------------------------------------------------------------------------

Client::Client() : impl_(std::make_unique<Impl>()) {}
Client::~Client() { stop(); }

void Client::start(const Config& cfg,
                   EventHandler  on_event,
                   StatusHandler on_status) {
    if (impl_->running_) return;
    impl_->stop_requested_ = false;
    impl_->thread_ = std::thread([this, cfg, on_event, on_status]() {
        impl_->run(cfg, on_event, on_status);
    });
}

void Client::stop() {
    impl_->stop_requested_ = true;
#ifndef _WIN32
    pid_t p = impl_->curl_pid_.exchange(0);
    if (p > 0) ::kill(p, SIGTERM);
#else
    // TerminateProcess so the blocking ReadFile/fgets returns and the
    // worker thread can exit promptly. The cleanup block closes handles.
    if (impl_->curl_proc_handle_)
        TerminateProcess((HANDLE)impl_->curl_proc_handle_, 0);
#endif
    if (impl_->thread_.joinable())
        impl_->thread_.join();
}

bool Client::is_running() const {
    return impl_->running_.load();
}

} // namespace fn_sse
