#include "server.h"

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

#include <algorithm>
#include <filesystem>
#include <iostream>
#include <sstream>

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());
}

// ---- MediaHttpEvent ----

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

// 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));
        }
        if (state.bytes_written >= state.content_length || state.session->done()) {
            bool ok = state.session->ok();
            std::string errmsg = ok ? "" : state.session->error_message();
            if (errmsg.empty() && !ok) errmsg = "import failed";
            active_imports_.erase(it);
            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);
        }
        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();
            if (clen == 0) {
                // 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)) {
                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;
            }

            // Create incremental import session
            auto session = app_.begin_import();
            if (!session) {
                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);
                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));
            }

            if (state.bytes_written >= clen || state.session->done()) {
                bool ok = state.session->ok();
                std::string errmsg = ok ? "" : state.session->error_message();
                if (errmsg.empty() && !ok) errmsg = "import failed";
                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 {
    HttpRequest mreq = convert_request(curreq);

    // 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 * 16;
    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 import session (client disconnected mid-upload)
    auto it = active_imports_.find(static_cast<netplus::con*>(&curreq));
    if (it != active_imports_.end()) {
        active_imports_.erase(it);
        app_.finish_import(false, "client disconnected");
    }
}

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 qs = raw_url.substr(qpos + 1);
    std::istringstream stream(qs);
    std::string pair;
    while (std::getline(stream, pair, '&')) {
        auto eq = pair.find('=');
        if (eq != std::string::npos) {
            result[pair.substr(0, eq)] = pair.substr(eq + 1);
        } else {
            result[pair] = "";
        }
    }
    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;
}

bool MediaHttpEvent::onH2StreamHeaders(libhttppp::HttpRequest &conn, uint32_t streamId,
                                       const std::vector<libhttppp::hpack::HeaderField> &headers) {
    // 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;
    }

    if (endStream || state.session->done()) {
        bool ok = state.session->ok();
        std::string errmsg = ok ? "" : state.session->error_message();
        if (errmsg.empty() && !ok) errmsg = "import failed";
        h2_imports_.erase(it);
        app_.finish_import(ok, errmsg);

        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) {
    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;
    if (data && len > 0) {
        state.session->feed(data, len);
        state.bytes_written += len;
    }

    if (fin || state.session->done()) {
        bool ok = state.session->ok();
        std::string errmsg = ok ? "" : state.session->error_message();
        if (errmsg.empty() && !ok) errmsg = "import failed";
        h3_imports_.erase(it);
        app_.finish_import(ok, errmsg);

        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)
    : 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) {}

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;
            }
            (void)freopen("/dev/null", "r", stdin);
            (void)freopen("/dev/null", "w", stdout);
            (void)freopen("/dev/null", "w", stderr);
        } else {
            std::cout << "mediadb started on " << proto << "://" << addr_ << ":" << port_ << "\n";
        }

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

        MediaHttpEvent event(app_, sockets);
        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 (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)) {
                    (void)chown(entry.path().c_str(), pw->pw_uid, pw->pw_gid);
                }
                (void)chown(db_dir.c_str(), pw->pw_uid, pw->pw_gid);
            } catch (...) {
                // best-effort; directory may not exist yet
            }
        }
        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
