#include "server.h"
#include "cluster.h"

#ifndef _WIN32
#include <grp.h>
#include <pwd.h>
#include <unistd.h>
#endif

#include <algorithm>
#include <filesystem>
#include <iostream>
#include <string_view>

namespace mediadb {

// ---- MediaConExt helpers ----

MediaConExt* getMediaExt(libhttppp::HttpRequest& req) {
    return static_cast<MediaConExt*>(req.extension.get());
}

MediaConExt* ensureMediaExt(libhttppp::HttpRequest& req) {
    if (!req.extension) {
        req.extension = {new MediaConExt{}, +[](void* p){ delete static_cast<MediaConExt*>(p); }};
    }
    return static_cast<MediaConExt*>(req.extension.get());
}

static bool drain_import_deferred(ImportState &state, std::string &error);
static bool should_finalize_import(ImportState &state, bool input_finished,
                                   bool &ok_out, std::string &errmsg_out);

// ---- MediaHttpEvent ----

MediaHttpEvent::MediaHttpEvent(App& app, std::vector<netplus::socket*> sockets,
                               const std::string &http_max_version)
    : libhttppp::HttpEvent(std::move(sockets)), app_(app), http_max_version_(http_max_version) {}

int MediaHttpEvent::version_rank(const std::string &v) {
    if (v == "HTTP/1.0" || v == "HTTP/1.1") return 1;
    if (v == "HTTP/2") return 2;
    if (v == "HTTP/3") return 3;
    return 0;
}

bool MediaHttpEvent::is_version_allowed(const std::string &req_version) const {
    const int req = version_rank(req_version);
    const int maxv = version_rank(http_max_version_);
    if (req == 0 || maxv == 0) return false;
    return req <= maxv;
}

// Override the low-level RequestEvent(con&) to intercept PUT /api/import
// before the base class buffers the entire body in RecvData.
// HTTP/2 and HTTP/3 are handled by the base class (frame-based parsing);
// we only intercept HTTP/1.x connections for streaming import.
void MediaHttpEvent::RequestEvent(netplus::con &curcon, const int tid, ULONG_PTR args) {
    auto &cureq = dynamic_cast<libhttppp::HttpRequest&>(curcon);

    // HTTP/2 and HTTP/3: delegate immediately to base class.
    // The base class detects QUIC (H3) and ALPN "h2" (H2) at the top
    // and routes to their respective frame parsers.
    if (dynamic_cast<netplus::quic*>(curcon.slots[0].csock.get())) {
        libhttppp::HttpEvent::RequestEvent(curcon, tid, args);
        return;
    }
    if (auto *sslsock = dynamic_cast<netplus::ssl*>(curcon.slots[0].csock.get())) {
        if (sslsock->getSelectedAlpn() == "h2") {
            libhttppp::HttpEvent::RequestEvent(curcon, tid, args);
            return;
        }
    }

    // HTTP/1.x only from here.

    // Active import session? Feed incoming data.
    auto it = active_imports_.find(&curcon);
    if (it != active_imports_.end()) {
        auto& state = it->second;
        if (!cureq.RecvData.empty()) {
            size_t n = std::min<size_t>(cureq.RecvData.size(),
                                        state.content_length - state.bytes_written);
            if (n > 0) {
                state.session->feed(cureq.RecvData.data(), n);
                state.bytes_written += n;
                cureq.RecvData.erase(cureq.RecvData.begin(),
                                     cureq.RecvData.begin() + static_cast<std::ptrdiff_t>(n));
            }
        } else {
            // No data available yet — yield CPU to avoid busy-loop
            std::this_thread::sleep_for(std::chrono::milliseconds(1));
        }
        bool ok = false;
        std::string errmsg;
        bool input_finished = (state.bytes_written >= state.content_length);
        if (should_finalize_import(state, input_finished, ok, errmsg)) {
            app_.finish_import(ok, errmsg);
            active_imports_.erase(it);
            if (ok) {
                send_response(cureq, HttpResponse::json(200,
                    [](){ auto*j=json_object_new_object();
                          json_object_object_add(j, "status",
                              json_object_new_string("ok"));
                          return j; }()));
            } else {
                send_response(cureq, HttpResponse::json(500,
                    [&errmsg](){ auto*j=json_object_new_object();
                          json_object_object_add(j, "error",
                              json_object_new_string(errmsg.c_str()));
                          return j; }()));
            }
            cureq.setRequestType(PARSEREQUEST);
        }
        return;
    }

    // For PARSEREQUEST: call parse() ourselves to get headers, then check
    // if this is PUT /api/import before the base class buffers the body.
    if (cureq.getRequestType() == PARSEREQUEST) {
        if (cureq.parse() == 0)
            return; // incomplete headers, wait for more data
        // Headers parsed — check if this is an import we need to intercept
        if (cureq.getRequestType() == PUTREQUEST &&
            cureq.getRequestURL() == "/api/import") {

            size_t clen = cureq.getContentLength();
            std::cerr << "[IMPORT-STREAM] intercepted PUT /api/import, content_length=" << clen << "\n";
            if (clen == 0) {
                std::cerr << "[IMPORT-STREAM] content_length=0, falling through to base class\n";
                // Empty import — let base class handle it
                libhttppp::HttpEvent::RequestEvent(curcon, tid, args);
                return;
            }

            // Auth check from headers
            HttpRequest mreq = convert_request(cureq, /*skip_body=*/true);
            if (!app_.is_authorized(mreq)) {
                std::cerr << "[IMPORT-STREAM] rejected: unauthorized\n";
                send_response(cureq, HttpResponse::json(401,
                    [](){ auto*j=json_object_new_object();
                          json_object_object_add(j, "error",
                              json_object_new_string("unauthorized"));
                          return j; }()));
                cureq.RecvData.clear();
                cureq.setRequestType(PARSEREQUEST);
                return;
            }

            std::cerr << "[IMPORT-STREAM] auth OK, creating import session\n";
            // Create incremental import session
            auto session = app_.begin_import();
            if (!session) {
                std::cerr << "[IMPORT-STREAM] begin_import returned null\n";
                send_response(cureq, HttpResponse::json(500,
                    [](){ auto*j=json_object_new_object();
                          json_object_object_add(j, "error",
                              json_object_new_string("import not available"));
                          return j; }()));
                cureq.RecvData.clear();
                cureq.setRequestType(PARSEREQUEST);
                return;
            }

            ImportState state;
            state.session = std::move(session);
            state.content_length = clen;
            // Feed any body data already in RecvData
            if (!cureq.RecvData.empty()) {
                size_t n = std::min<size_t>(cureq.RecvData.size(), clen);
                std::cerr << "[IMPORT-STREAM] initial feed: RecvData.size=" << cureq.RecvData.size()
                          << " feeding=" << n << " clen=" << clen << "\n";
                state.session->feed(cureq.RecvData.data(), n);
                state.bytes_written = n;
                cureq.RecvData.erase(cureq.RecvData.begin(),
                                     cureq.RecvData.begin() + static_cast<std::ptrdiff_t>(n));
            } else {
                std::cerr << "[IMPORT-STREAM] initial feed: RecvData is EMPTY, clen=" << clen << "\n";
            }

            std::cerr << "[IMPORT-STREAM] after initial feed: bytes_written=" << state.bytes_written
                      << " clen=" << clen << " done=" << state.session->done()
                      << " ok=" << state.session->ok() << "\n";
            bool ok = false;
            std::string errmsg;
            bool input_finished = (state.bytes_written >= clen);
            if (should_finalize_import(state, input_finished, ok, errmsg)) {
                app_.finish_import(ok, errmsg);
                if (ok) {
                    send_response(cureq, HttpResponse::json(200,
                        [](){ auto*j=json_object_new_object();
                              json_object_object_add(j, "status",
                                  json_object_new_string("ok"));
                              return j; }()));
                } else {
                    send_response(cureq, HttpResponse::json(500,
                        [&errmsg](){ auto*j=json_object_new_object();
                              json_object_object_add(j, "error",
                                  json_object_new_string(errmsg.c_str()));
                              return j; }()));
                }
                cureq.setRequestType(PARSEREQUEST);
            } else {
                active_imports_[&curcon] = std::move(state);
            }
            return;
        }
        // Not an import — fall through to base class which will handle
        // the already-parsed request (requestType is already set by parse())
    }

    // Everything else: delegate to base class
    libhttppp::HttpEvent::RequestEvent(curcon, tid, args);
}

void MediaHttpEvent::RequestEvent(libhttppp::HttpRequest& curreq, const int /*tid*/, ULONG_PTR /*args*/) {
  try {
    if (!is_version_allowed(curreq.getRequestVersion())) {
        send_response(curreq, HttpResponse::json(505,
            [](){ auto*j=json_object_new_object();
                  json_object_object_add(j, "error",
                      json_object_new_string("HTTP version not supported by server policy"));
                  return j; }()));
        return;
    }

    HttpRequest mreq = convert_request(curreq);
    if (mreq.method == "PUT" && mreq.path == "/api/import") {
        std::cerr << "[IMPORT-H2] RequestEvent(HttpRequest&) handling PUT /api/import, body_size=" << mreq.body.size() << "\n";
    }

    // For upload endpoint: PUT with metadata in headers, or POST with multipart/form-data
    if ((mreq.method == "PUT" || mreq.method == "POST") && mreq.path == "/api/upload") {
        try {
            UploadData upload;

            auto ct_it = mreq.headers.find("content-type");
            std::string ct = ct_it != mreq.headers.end() ? ct_it->second : "";

            if (ct.find("multipart/form-data") != std::string::npos) {
                std::string boundary = extract_boundary(ct);
                auto parts = parse_multipart(mreq.body, boundary);
                for (const auto& part : parts) {
                    if (part.name == "album_id") {
                        upload.album_id = part.data;
                    } else if (part.name == "file") {
                        upload.filename = part.filename;
                        upload.file_data.assign(
                            reinterpret_cast<const std::uint8_t*>(part.data.data()),
                            reinterpret_cast<const std::uint8_t*>(part.data.data()) + part.data.size());
                    }
                }
            } else {
                auto it_album = mreq.headers.find("x-album-id");
                if (it_album != mreq.headers.end())
                    upload.album_id = it_album->second;

                auto it_fname = mreq.headers.find("x-filename");
                if (it_fname != mreq.headers.end())
                    upload.filename = it_fname->second;

                if (!curreq.RecvData.empty()) {
                    const char* p = curreq.RecvData.data();
                    size_t sz = curreq.RecvData.size();
                    upload.file_data.assign(
                        reinterpret_cast<const std::uint8_t*>(p),
                        reinterpret_cast<const std::uint8_t*>(p) + sz);
                } else if (!mreq.body.empty()) {
                    upload.file_data.assign(
                        reinterpret_cast<const std::uint8_t*>(mreq.body.data()),
                        reinterpret_cast<const std::uint8_t*>(mreq.body.data()) + mreq.body.size());
                }
            }

            HttpResponse mresp = app_.handle_upload_form(mreq, std::move(upload));
            send_response(curreq, mresp);
        } catch (libhttppp::HTTPException& e) {
            std::cerr << "upload form parse error: " << e.what() << "\n";
            HttpResponse mresp = HttpResponse::text(400, "multipart parse error");
            send_response(curreq, mresp);
        } catch (std::exception& e) {
            std::cerr << "upload error: " << e.what() << "\n";
            HttpResponse mresp = HttpResponse::text(500, std::string("upload error: ") + e.what());
            send_response(curreq, mresp);
        }
        return;
    }

    HttpResponse mresp = app_.handle(mreq);
    send_response(curreq, mresp);
  } catch (netplus::NetException& e) {
    std::cerr << "RequestEvent net error: " << e.what() << "\n";
    try {
        HttpResponse mresp = HttpResponse::text(500, "internal server error");
        send_response(curreq, mresp);
    } catch (...) {}
  } catch (libhttppp::HTTPException& e) {
    std::cerr << "RequestEvent http error: " << e.what() << "\n";
    try {
        HttpResponse mresp = HttpResponse::text(500, "internal server error");
        send_response(curreq, mresp);
    } catch (...) {}
  } catch (std::exception& e) {
    std::cerr << "RequestEvent unhandled error: " << e.what() << "\n";
    try {
        HttpResponse mresp = HttpResponse::text(500, "internal server error");
        send_response(curreq, mresp);
    } catch (...) {}
  } catch (...) {
    std::cerr << "RequestEvent unknown error" << "\n";
    try {
        HttpResponse mresp = HttpResponse::text(500, "internal server error");
        send_response(curreq, mresp);
    } catch (...) {}
  }
}

void MediaHttpEvent::ResponseEvent(libhttppp::HttpRequest& curreq, const int /*tid*/, ULONG_PTR /*args*/) {
    auto* ext = getMediaExt(curreq);
    if (!ext || ext->send_size == 0) return;
    if (ext->produced >= ext->send_size) return;

    static constexpr size_t MAX_FILL = BLOCKSIZE * 64;
    size_t filled = 0;
    while (filled < MAX_FILL && ext->produced < ext->send_size) {
        const size_t remaining = ext->send_size - ext->produced;
        const size_t chunk = remaining < BLOCKSIZE ? remaining : BLOCKSIZE;
        curreq.SendData.append(
            reinterpret_cast<const char*>(ext->data.data() + ext->produced), chunk);
        ext->produced += chunk;
        filled += chunk;
    }

    if (ext->produced >= ext->send_size) {
        ext->data.clear();
        ext->data.shrink_to_fit();
    }
}

void MediaHttpEvent::ConnectEvent(libhttppp::HttpRequest& /*curreq*/, const int /*tid*/, ULONG_PTR /*args*/) {}

void MediaHttpEvent::DisconnectEvent(libhttppp::HttpRequest& curreq, const int /*tid*/, ULONG_PTR /*args*/) {
    // Clean up any orphaned H1 import session (client disconnected mid-upload)
    auto it = active_imports_.find(static_cast<netplus::con*>(&curreq));
    if (it != active_imports_.end()) {
        app_.finish_import(false, "client disconnected");
        active_imports_.erase(it);
        return;
    }

    // Clean up any orphaned H2 import sessions on this connection
    for (auto h2it = h2_imports_.begin(); h2it != h2_imports_.end(); ) {
        if (h2it->first.conn == &curreq) {
            app_.finish_import(false, "client disconnected");
            h2it = h2_imports_.erase(h2it);
            return;
        } else {
            ++h2it;
        }
    }
}

std::unordered_map<std::string, std::string> MediaHttpEvent::parse_query(const std::string& raw_url) {
    std::unordered_map<std::string, std::string> result;
    auto qpos = raw_url.find('?');
    if (qpos == std::string::npos) return result;
    std::string_view qs(raw_url);
    qs.remove_prefix(qpos + 1);
    while (!qs.empty()) {
        auto amp = qs.find('&');
        auto seg = qs.substr(0, amp);
        auto eq = seg.find('=');
        if (eq != std::string_view::npos)
            result[std::string(seg.substr(0, eq))] = std::string(seg.substr(eq + 1));
        else if (!seg.empty())
            result[std::string(seg)] = "";
        if (amp == std::string_view::npos) break;
        qs.remove_prefix(amp + 1);
    }
    return result;
}

HttpRequest MediaHttpEvent::convert_request(libhttppp::HttpRequest& curreq, bool skip_body) {
    HttpRequest mreq;

    switch (curreq.getRequestType()) {
        case GETREQUEST:    mreq.method = "GET";    break;
        case POSTREQUEST:   mreq.method = "POST";   break;
        case PUTREQUEST:    mreq.method = "PUT";    break;
        case DELETEREQUEST: mreq.method = "DELETE"; break;
        default:            mreq.method = "GET";    break;
    }

    mreq.path = curreq.getRequestURL();
    mreq.query = parse_query(curreq.getRequest());

    for (auto* hd = curreq.getfirstHeaderData(); hd != nullptr; hd = hd->nextHeaderData()) {
        std::string key = hd->getkey();
        std::transform(key.begin(), key.end(), key.begin(),
                       [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
        auto* val = hd->getfirstValue();
        if (val != nullptr) {
            std::string joined = val->getvalue();
            for (auto* v = val->nextvalue(); v; v = v->nextvalue()) {
                joined += "; ";
                joined += v->getvalue();
            }
            mreq.headers[key] = std::move(joined);
        }
    }

    if (!skip_body && (curreq.getRequestType() == POSTREQUEST || curreq.getRequestType() == PUTREQUEST)) {
        size_t content_length = curreq.getContentLength();
        if (content_length > 0 && curreq.RecvData.size() >= content_length) {
            const char* data = curreq.RecvData.data() + (curreq.RecvData.size() - content_length);
            mreq.body.assign(data, content_length);
        } else if (!curreq.RecvData.empty()) {
            mreq.body.assign(curreq.RecvData.data(), curreq.RecvData.size());
        }
    }

    return mreq;
}

std::string MediaHttpEvent::status_to_http_state(int status) {
    switch (status) {
        case 200: return HTTP200;
        case 201: return HTTP201;
        case 202: return HTTP202;
        case 204: return HTTP204;
        case 206: return HTTP206;
        case 400: return HTTP400;
        case 401: return HTTP401;
        case 403: return HTTP403;
        case 404: return HTTP404;
        case 405: return HTTP405;
        case 409: return HTTP409;
        case 416: return HTTP416;
        case 422: return HTTP422;
        case 500: return HTTP500;
        case 503: return HTTP503;
        default:  return HTTP500;
    }
}

void MediaHttpEvent::send_response(libhttppp::HttpRequest& curreq, const HttpResponse& mresp) {
    libhttppp::HttpResponse resp;
    resp.setState(status_to_http_state(mresp.status));
    resp.setContentType(mresp.content_type);
    resp.setContentLength(mresp.body.size());
    resp.setConnection("keep-alive");

    for (const auto& [key, value] : mresp.headers) {
        auto* hd = resp.setHeaderData(key);
        hd->push_back(value);
    }

    const std::string empty_body;
    resp.send(curreq, empty_body, -1);
    if (!mresp.body.empty()) {
        curreq.SendData.append(reinterpret_cast<const char*>(mresp.body.data()), mresp.body.size());
    }
}

// ---- H2/H3 streaming import callbacks ----

// Helper: build a mediadb::HttpRequest from hpack HeaderField vector for auth check
static HttpRequest make_auth_request(const std::vector<libhttppp::hpack::HeaderField> &headers) {
    HttpRequest mreq;
    for (auto &hf : headers) {
        if (hf.name == ":method") mreq.method = hf.value;
        else if (hf.name == ":path") mreq.path = hf.value;
        else mreq.headers[hf.name] = hf.value;
    }
    return mreq;
}

// Helper: build a mediadb::HttpRequest from qpack HeaderField vector for auth check
static HttpRequest make_auth_request(const std::vector<libhttppp::qpack::HeaderField> &headers) {
    HttpRequest mreq;
    for (auto &hf : headers) {
        if (hf.name == ":method") mreq.method = hf.value;
        else if (hf.name == ":path") mreq.path = hf.value;
        else mreq.headers[hf.name] = hf.value;
    }
    return mreq;
}

// Helper: JSON error body
static std::string json_err(const char *msg) {
    json_object *j = json_object_new_object();
    json_object_object_add(j, "error", json_object_new_string(msg));
    const char *s = json_object_to_json_string(j);
    std::string out(s);
    json_object_put(j);
    return out;
}

// Helper: JSON success body
static std::string json_ok() {
    json_object *j = json_object_new_object();
    json_object_object_add(j, "status", json_object_new_string("ok"));
    const char *s = json_object_to_json_string(j);
    std::string out(s);
    json_object_put(j);
    return out;
}

static bool drain_import_deferred(ImportState &state, std::string &error) {
    constexpr int kMaxRounds = 2048;
    for (int round = 0; round < kMaxRounds; ++round) {
        if (state.session->done()) return true;
        auto ops = state.session->take_deferred();
        if (ops.empty()) return state.session->done();
        for (auto &op : ops) {
            try {
                op();
            } catch (const std::exception &e) {
                error = std::string("import deferred op failed: ") + e.what();
                return false;
            } catch (...) {
                error = "import deferred op failed (unknown)";
                return false;
            }
        }
    }
    error = "import deferred op drain limit reached";
    return false;
}

static bool should_finalize_import(ImportState &state, bool input_finished,
                                   bool &ok_out, std::string &errmsg_out) {
    ok_out = false;
    errmsg_out.clear();

    if (!state.session) {
        errmsg_out = "import session missing";
        return true;
    }

    if (input_finished && !state.session->done()) {
        std::string drain_error;
        (void)drain_import_deferred(state, drain_error);
        if (!drain_error.empty()) errmsg_out = drain_error;
    }

    if (!state.session->done() && !input_finished) {
        return false;
    }

    ok_out = state.session->ok();
    if (!ok_out) {
        if (errmsg_out.empty()) errmsg_out = state.session->error_message();
        if (errmsg_out.empty()) {
            errmsg_out = input_finished
                ? "import stream finished before parser completed"
                : "import failed";
        }
    }
    return true;
}

bool MediaHttpEvent::onH2StreamHeaders(libhttppp::HttpRequest &conn, uint32_t streamId,
                                       const std::vector<libhttppp::hpack::HeaderField> &headers) {
    if (!is_version_allowed("HTTP/2")) {
        std::string body = json_err("HTTP version not supported by server policy");
        std::string dummy;
        sendH2StreamResponse(dummy, streamId, 505, "application/json", body);
        conn.SendData.append(dummy.data(), dummy.size());
        return true;
    }

    // Check if this is PUT /api/import
    std::string method, path;
    for (auto &hf : headers) {
        if (hf.name == ":method") method = hf.value;
        else if (hf.name == ":path") path = hf.value;
    }
    if (method != "PUT" || path != "/api/import") return false;

    // Auth check
    HttpRequest mreq = make_auth_request(headers);
    if (!app_.is_authorized(mreq)) {
        std::string body = json_err("unauthorized");
        std::string dummy;
        sendH2StreamResponse(dummy, streamId, 401, "application/json", body);
        conn.SendData.append(dummy.data(), dummy.size());
        return true; // claim the stream so DATA frames are consumed
    }

    auto session = app_.begin_import();
    if (!session) {
        std::string body = json_err("import not available");
        std::string dummy;
        sendH2StreamResponse(dummy, streamId, 500, "application/json", body);
        conn.SendData.append(dummy.data(), dummy.size());
        return true;
    }

    ImportState state;
    state.session = std::move(session);
    h2_imports_[{&conn, streamId}] = std::move(state);
    return true;
}

void MediaHttpEvent::onH2DataChunk(libhttppp::HttpRequest &conn, uint32_t streamId,
                                   const char *data, size_t len, bool endStream,
                                   std::string &h2out, const int /*tid*/, ULONG_PTR /*args*/) {
    H2StreamKey key{&conn, streamId};
    auto it = h2_imports_.find(key);
    if (it == h2_imports_.end()) return;

    auto &state = it->second;
    if (data && len > 0) {
        state.session->feed(data, len);
        state.bytes_written += len;
    }

    bool ok = false;
    std::string errmsg;
    if (should_finalize_import(state, endStream, ok, errmsg)) {
        app_.finish_import(ok, errmsg);
        h2_imports_.erase(it);

        std::string body = ok ? json_ok() : json_err(errmsg.c_str());
        uint16_t status = ok ? 200 : 500;
        sendH2StreamResponse(h2out, streamId, status, "application/json", body);
    }
}

bool MediaHttpEvent::onH3StreamHeaders(netplus::socket *sock, uint64_t streamId,
                                       const std::vector<libhttppp::qpack::HeaderField> &headers) {
    if (!is_version_allowed("HTTP/3")) {
        sendH3StreamResponse(sock, streamId, 505, "application/json",
                             json_err("HTTP version not supported by server policy"));
        return true;
    }

    // Recover from orphaned H3 imports (e.g. client disconnect mid-upload).
    // H1/H2 are cleaned in DisconnectEvent; H3 needs explicit stale cleanup.
    {
        constexpr auto kStaleTimeout = std::chrono::seconds(120);
        auto now = std::chrono::steady_clock::now();
        for (auto it = h3_imports_.begin(); it != h3_imports_.end();) {
            bool stale = (now - it->second.last_activity) > kStaleTimeout;
            bool done = it->second.session && it->second.session->done();
            if (stale || done) {
                app_.finish_import(false, stale ? "stale h3 import session"
                                                : "h3 import session finished unexpectedly");
                it = h3_imports_.erase(it);
            } else {
                ++it;
            }
        }
    }

    std::string method, path;
    for (auto &hf : headers) {
        if (hf.name == ":method") method = hf.value;
        else if (hf.name == ":path") path = hf.value;
    }
    if (method != "PUT" || path != "/api/import") return false;

    HttpRequest mreq = make_auth_request(headers);
    if (!app_.is_authorized(mreq)) {
        sendH3StreamResponse(sock, streamId, 401, "application/json", json_err("unauthorized"));
        return true;
    }

    auto session = app_.begin_import();
    if (!session) {
        sendH3StreamResponse(sock, streamId, 500, "application/json", json_err("import not available"));
        return true;
    }

    ImportState state;
    state.session = std::move(session);
    h3_imports_[streamId] = std::move(state);
    return true;
}

void MediaHttpEvent::onH3DataChunk(netplus::socket *sock, uint64_t streamId,
                                   const char *data, size_t len, bool fin) {
    auto it = h3_imports_.find(streamId);
    if (it == h3_imports_.end()) return;

    auto &state = it->second;
    state.last_activity = std::chrono::steady_clock::now();
    if (data && len > 0) {
        state.session->feed(data, len);
        state.bytes_written += len;
    }

    bool ok = false;
    std::string errmsg;
    if (should_finalize_import(state, fin, ok, errmsg)) {
        app_.finish_import(ok, errmsg);
        h3_imports_.erase(it);

        std::string body = ok ? json_ok() : json_err(errmsg.c_str());
        uint16_t status = ok ? 200 : 500;
        sendH3StreamResponse(sock, streamId, status, "application/json", body);
    }
}

// ---- HttpServer ----

HttpServer::HttpServer(App& app, const std::string& addr, int port, int max_connections,
                       const std::string& cert_path, const std::string& key_path,
                       bool foreground, const std::string& run_user,
                                             const std::string& db_dir,
                                             const std::string& http_max_version)
    : app_(app), addr_(addr), port_(port), max_connections_(max_connections),
      cert_path_(cert_path), key_path_(key_path),
            foreground_(foreground), run_user_(run_user), db_dir_(db_dir),
            http_max_version_(http_max_version) {}

int HttpServer::run() {
    try {
        libhttppp::HttpD httpd(addr_, port_, max_connections_, cert_path_, key_path_);
        auto sockets = httpd.getServerSockets();

        const char* proto = (!cert_path_.empty() && !key_path_.empty()) ? "https" : "http";

        if (!foreground_) {
            pid_t pid = fork();
            if (pid < 0) {
                std::cerr << "fork() failed: " << strerror(errno) << "\n";
                return 1;
            }
            if (pid > 0) {
                std::cout << "mediadb started on " << proto << "://" << addr_ << ":" << port_
                          << " (pid " << pid << ")\n";
                _exit(0);
            }
            if (setsid() < 0) {
                std::cerr << "setsid() failed: " << strerror(errno) << "\n";
                return 1;
            }
            if (!freopen("/dev/null", "r", stdin))
                std::cerr << "freopen stdin failed\n";
            if (!freopen("/dev/null", "w", stdout))
                std::cerr << "freopen stdout failed\n";
            if (!freopen("/dev/null", "w", stderr)) {}
        } else {
            std::cout << "mediadb started on " << proto << "://" << addr_ << ":" << port_ << "\n";
        }

        // Bind sockets BEFORE starting the cluster so we fail fast if the
        // port is already in use.  This avoids orphaning cluster threads
        // (which causes "terminate called without an active exception").
        // The event constructor will skip bind/listen because _Bound/_Listening
        // are already set.
        for (auto* s : sockets) {
            if (s) {
                s->bind();
                s->setNonBlock();
                s->listen();
            }
        }

        // Invoke post-fork callback (cluster start, etc.) — safe in child process
        if (post_fork_callback) {
            post_fork_callback();
        }

        MediaHttpEvent event(app_, sockets, http_max_version_);
        event.threads = std::max(event.threads * 4, 16);

        // Drop privileges after socket creation but before spawning worker
        // threads so all threads inherit correct UID/GID (musl setuid is per-thread)
        if (!run_user_.empty()) {
            drop_privileges(run_user_, db_dir_);
        }

        event.runEventloop();
    } catch (netplus::NetException& e) {
        std::cerr << "Network error: " << e.what() << "\n";
        return 1;
    } catch (libhttppp::HTTPException& e) {
        std::cerr << "HTTP error: " << e.what() << "\n";
        return 1;
    } catch (std::exception& e) {
        std::cerr << "Error: " << e.what() << "\n";
        return 1;
    }
    return 0;
}

void HttpServer::drop_privileges(const std::string& username, const std::string& db_dir) {
    if (getuid() == 0) {
        struct passwd* pw = getpwnam(username.c_str());
        if (pw == nullptr) {
            throw std::runtime_error("User '" + username + "' not found");
        }
        // Chown data directory (and contents) to target user before dropping.
        // BinDb::BinDb() creates the directory as root; after delete_store +
        // reimport the recreated dir would remain root-owned.
        if (!db_dir.empty()) {
            namespace fs = std::filesystem;
            try {
                for (auto& entry : fs::recursive_directory_iterator(db_dir)) {
                    if (chown(entry.path().c_str(), pw->pw_uid, pw->pw_gid) != 0)
                        std::cerr << "chown failed: " << entry.path() << "\n";
                }
                if (chown(db_dir.c_str(), pw->pw_uid, pw->pw_gid) != 0)
                    std::cerr << "chown failed: " << db_dir << "\n";
            } catch (...) {
                // best-effort; directory may not exist yet
            }
        }
        // Also chown cluster store path if it exists
        if (g_Cluster && !g_Cluster->getConfig().store_path.empty()) {
            namespace fs = std::filesystem;
            const auto& cpath = g_Cluster->getConfig().store_path;
            try {
                for (auto& entry : fs::recursive_directory_iterator(cpath)) {
                    if (chown(entry.path().c_str(), pw->pw_uid, pw->pw_gid) != 0)
                        std::cerr << "chown failed: " << entry.path() << "\n";
                }
                if (chown(cpath.c_str(), pw->pw_uid, pw->pw_gid) != 0)
                    std::cerr << "chown failed: " << cpath << "\n";
            } catch (...) {
                // best-effort
            }
        }
        if (initgroups(username.c_str(), pw->pw_gid) != 0) {
            throw std::runtime_error("initgroups() failed: " + std::string(strerror(errno)));
        }
        if (setgid(pw->pw_gid) != 0) {
            throw std::runtime_error("setgid() failed: " + std::string(strerror(errno)));
        }
        if (setuid(pw->pw_uid) != 0) {
            throw std::runtime_error("setuid() failed: " + std::string(strerror(errno)));
        }
    } else {
        std::cerr << "Warning: not running as root, cannot drop privileges to '" << username << "'\n";
    }
}

} // namespace mediadb
