#include "app.h"
#include "cluster.h"

#include <iostream>
#include <memory>
#include <sstream>
#include <unordered_set>

namespace mediadb {

// ---- MIME by extension ----
std::string mime_for_ext(const std::string& ext_in) {
    std::string ext = ext_in;
    std::transform(ext.begin(), ext.end(), ext.begin(),
                   [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
    if (ext == ".html" || ext == ".htm") return "text/html; charset=utf-8";
    if (ext == ".css")  return "text/css; charset=utf-8";
    if (ext == ".js")   return "application/javascript; charset=utf-8";
    if (ext == ".json") return "application/json";
    if (ext == ".png")  return "image/png";
    if (ext == ".jpg" || ext == ".jpeg") return "image/jpeg";
    if (ext == ".webp") return "image/webp";
    if (ext == ".avif") return "image/avif";
    if (ext == ".svg")  return "image/svg+xml";
    if (ext == ".ico")  return "image/x-icon";
    if (ext == ".gif")  return "image/gif";
    if (ext == ".mp4" || ext == ".m4v") return "video/mp4";
    if (ext == ".webm") return "video/webm";
    if (ext == ".mkv")  return "video/x-matroska";
    if (ext == ".mov")  return "video/quicktime";
    if (ext == ".avi")  return "video/x-msvideo";
    if (ext == ".ts")   return "video/mp2t";
    if (ext == ".flv")  return "video/x-flv";
    if (ext == ".mp3")  return "audio/mpeg";
    if (ext == ".flac") return "audio/flac";
    if (ext == ".wav")  return "audio/wav";
    if (ext == ".ogg")  return "audio/ogg";
    if (ext == ".aac")  return "audio/aac";
    if (ext == ".m4a")  return "audio/mp4";
    if (ext == ".opus") return "audio/opus";
    if (ext == ".woff2") return "font/woff2";
    return "application/octet-stream";
}

// ---- multipart parser utilities ----

std::string extract_param(const std::string& header, const std::string& param) {
    auto key = param + "=";
    auto pos = header.find(key);
    if (pos == std::string::npos) return {};
    pos += key.size();
    if (pos < header.size() && header[pos] == '"') {
        ++pos;
        auto end = header.find('"', pos);
        return end == std::string::npos ? std::string{} : header.substr(pos, end - pos);
    }
    auto end = header.find_first_of("; \r\n", pos);
    return header.substr(pos, end - pos);
}

std::vector<MultipartPart> parse_multipart(const std::string& body, const std::string& boundary) {
    std::vector<MultipartPart> parts;
    const std::string delim = "--" + boundary;
    const std::string end_delim = delim + "--";

    std::string::size_type pos = body.find(delim);
    if (pos == std::string::npos) return parts;
    pos += delim.size();

    while (pos < body.size()) {
        if (pos + 1 < body.size() && body[pos] == '\r' && body[pos + 1] == '\n') pos += 2;
        else if (pos < body.size() && body[pos] == '\n') pos += 1;

        auto hdr_end = body.find("\r\n\r\n", pos);
        std::string::size_type body_start;
        if (hdr_end != std::string::npos) {
            body_start = hdr_end + 4;
        } else {
            hdr_end = body.find("\n\n", pos);
            if (hdr_end == std::string::npos) break;
            body_start = hdr_end + 2;
        }

        std::string headers_block = body.substr(pos, hdr_end - pos);

        auto next = body.find(delim, body_start);
        if (next == std::string::npos) break;

        auto part_end = next;
        if (part_end >= 2 && body[part_end - 2] == '\r' && body[part_end - 1] == '\n') part_end -= 2;
        else if (part_end >= 1 && body[part_end - 1] == '\n') part_end -= 1;

        MultipartPart part;
        part.data = body.substr(body_start, part_end - body_start);

        std::istringstream hs(headers_block);
        std::string line;
        while (std::getline(hs, line)) {
            if (!line.empty() && line.back() == '\r') line.pop_back();
            std::string lower = line;
            std::transform(lower.begin(), lower.end(), lower.begin(),
                           [](unsigned char c){ return static_cast<char>(std::tolower(c)); });
            if (lower.rfind("content-disposition:", 0) == 0) {
                part.name = extract_param(line, "name");
                part.filename = extract_param(line, "filename");
            } else if (lower.rfind("content-type:", 0) == 0) {
                part.content_type = line.substr(14);
                while (!part.content_type.empty() && part.content_type.front() == ' ')
                    part.content_type.erase(part.content_type.begin());
            }
        }

        parts.push_back(std::move(part));

        pos = next + delim.size();
        if (pos + 1 < body.size() && body[pos] == '-' && body[pos + 1] == '-') break;
    }
    return parts;
}

std::string extract_boundary(const std::string& content_type) {
    auto pos = content_type.find("boundary=");
    if (pos == std::string::npos) return {};
    pos += 9;
    if (pos < content_type.size() && content_type[pos] == '"') {
        ++pos;
        auto end = content_type.find('"', pos);
        return end == std::string::npos ? std::string{} : content_type.substr(pos, end - pos);
    }
    auto end = content_type.find_first_of("; \r\n", pos);
    return content_type.substr(pos, end - pos);
}

// ==================== App ====================

App::App(MediaBackendApi& db,
         const std::string& authdb_url,
         const std::string& authdb_client_name,
         const std::string& authdb_client_secret,
         const std::string& html_dir,
         std::size_t cache_size_mb)
    : db_(db),
      auth_(authdb_url, authdb_client_name, authdb_client_secret),
      cache_(cache_size_mb * 1024U * 1024U),
      previews_(cache_, db_),
      html_dir_(html_dir) {}

App::~App() {
    if (import_thread_.joinable()) import_thread_.join();
}

bool App::is_authorized(const HttpRequest& req) const {
    return auth_.is_authorized(req);
}

// ---- helpers ----

bool App::starts_with(const std::string& s, const std::string& prefix) { return s.rfind(prefix, 0) == 0; }
bool App::ends_with(const std::string& s, const std::string& suffix) {
    return s.size() >= suffix.size() && s.compare(s.size() - suffix.size(), suffix.size(), suffix) == 0;
}
bool App::contains(const std::string& s, const std::string& sub) { return s.find(sub) != std::string::npos; }
std::string App::segment(const std::string& path, std::size_t prefix_len) {
    auto rest = path.substr(prefix_len);
    auto slash = rest.find('/');
    return slash == std::string::npos ? rest : rest.substr(0, slash);
}

// ---- session tracking helpers ----

void App::evict_expired_sessions() {
    // caller must hold session_mutex_
    auto now = std::chrono::steady_clock::now();
    for (auto it = sessions_.begin(); it != sessions_.end(); ) {
        if (now - it->second.created_at > std::chrono::seconds(SESSION_TTL_SECONDS))
            it = sessions_.erase(it);
        else
            ++it;
    }
}

void App::store_session(const std::string& token, const std::string& username,
                         const std::string& domain_label,
                         const std::vector<std::string>& group_ids) {
    std::lock_guard<std::mutex> lk(session_mutex_);
    if (sessions_.size() > MAX_SESSIONS) {
        evict_expired_sessions();
    }
    sessions_[token] = SessionInfo{username, domain_label, group_ids, std::chrono::steady_clock::now()};
}

std::string App::get_session_username(const HttpRequest& req) const {
    auto it = req.headers.find("authorization");
    if (it == req.headers.end()) return {};
    const std::string& value = it->second;
    const std::string prefix = "Bearer ";
    if (value.size() <= prefix.size() || value.compare(0, prefix.size(), prefix) != 0) return {};
    const std::string token = value.substr(prefix.size());
    std::lock_guard<std::mutex> lk(session_mutex_);
    auto sit = sessions_.find(token);
    return sit != sessions_.end() ? sit->second.username : std::string{};
}

std::string App::get_session_domain(const HttpRequest& req) const {
    auto it = req.headers.find("authorization");
    if (it == req.headers.end()) return {};
    const std::string& value = it->second;
    const std::string prefix = "Bearer ";
    if (value.size() <= prefix.size() || value.compare(0, prefix.size(), prefix) != 0) return {};
    const std::string token = value.substr(prefix.size());
    std::lock_guard<std::mutex> lk(session_mutex_);
    auto sit = sessions_.find(token);
    return sit != sessions_.end() ? sit->second.domain_label : std::string{};
}

std::vector<std::string> App::get_session_groups(const HttpRequest& req) const {
    auto it = req.headers.find("authorization");
    if (it == req.headers.end()) return {};
    const std::string& value = it->second;
    const std::string prefix = "Bearer ";
    if (value.size() <= prefix.size() || value.compare(0, prefix.size(), prefix) != 0) return {};
    const std::string token = value.substr(prefix.size());
    std::lock_guard<std::mutex> lk(session_mutex_);
    auto sit = sessions_.find(token);
    return sit != sessions_.end() ? sit->second.group_ids : std::vector<std::string>{};
}

std::vector<std::string> App::resolve_session_groups(authdb::client::Client& client) {
    std::vector<std::string> matched;
    auto all_gids = db_.all_acl_group_ids();
    if (all_gids.empty()) return matched;
    try {
        authdb::SessionData sdat;
        client.SessionInfo(sdat);
        for (const auto& gid_str : all_gids) {
            try {
                uuid::uuid gid(gid_str);
                if (sdat.isMemberOf(gid)) {
                    matched.push_back(gid_str);
                }
            } catch (...) {}
        }
    } catch (...) {}
    return matched;
}

void App::repopulate_session(const HttpRequest& req) {
    auto it = req.headers.find("authorization");
    if (it == req.headers.end()) return;
    const std::string& value = it->second;
    const std::string prefix = "Bearer ";
    if (value.size() <= prefix.size() || value.compare(0, prefix.size(), prefix) != 0) return;
    const std::string token = value.substr(prefix.size());
    try {
        uuid::uuid session_id(token);
        authdb::client::ClientConnection con;
        con.setUrl(auth_.url());
        con.setClientName(auth_.client_name());
        con.setClientSecret(auth_.client_secret());
        con.setSessionID(session_id);
        authdb::client::Client client(con);
        if (!client.ClientAuth()) return;
        authdb::SessionData sdat;
        client.SessionInfo(sdat);
        std::string username = sdat.getUsername() ? sdat.getUsername() : "";
        auto grps = resolve_session_groups(client);
        store_session(token, username, {}, grps);
    } catch (...) {
        store_session(token, "");
    }
}

bool App::is_session_known(const HttpRequest& req) const {
    auto it = req.headers.find("authorization");
    if (it == req.headers.end()) return false;
    const std::string& value = it->second;
    const std::string prefix = "Bearer ";
    if (value.size() <= prefix.size() || value.compare(0, prefix.size(), prefix) != 0) return false;
    const std::string token = value.substr(prefix.size());
    std::lock_guard<std::mutex> lk(session_mutex_);
    return sessions_.find(token) != sessions_.end();
}

bool App::is_any_authorized(const HttpRequest& req) {
    if (is_session_known(req)) return true;
    if (auth_.is_authorized(req)) {
        repopulate_session(req);
        return true;
    }
    return false;
}

bool App::has_store_access(const HttpRequest& req, const std::string& store_id) const {
    const std::string domain = get_session_domain(req);
    if (domain.empty()) return true;

    auto acls = db_.list_store_acls(store_id);
    if (!acls.empty()) {
        auto grps = get_session_groups(req);
        for (const auto& acl : acls) {
            if (!acl.can_read) continue;
            for (const auto& gid : grps) {
                if (gid == acl.group_id) return true;
            }
        }
        return false;
    }

    auto domains = db_.list_store_domains(store_id);
    for (const auto& d : domains) {
        if (d.label == domain) return true;
    }
    return false;
}

bool App::has_store_write_access(const HttpRequest& req, const std::string& store_id) const {
    const std::string domain = get_session_domain(req);
    if (domain.empty()) return true;

    auto acls = db_.list_store_acls(store_id);
    if (!acls.empty()) {
        auto grps = get_session_groups(req);
        for (const auto& acl : acls) {
            if (!acl.can_write) continue;
            for (const auto& gid : grps) {
                if (gid == acl.group_id) return true;
            }
        }
        return false;
    }

    auto domains = db_.list_store_domains(store_id);
    for (const auto& d : domains) {
        if (d.label == domain) return true;
    }
    return false;
}

// ---- JSON serializers ----

json_object* App::store_to_json(const StoreRecord& s) {
    json_object* j = json_object_new_object();
    json_object_object_add(j, "id", json_object_new_string(s.id.c_str()));
    json_object_object_add(j, "name", json_object_new_string(s.name.c_str()));
    json_object_object_add(j, "created_at", json_object_new_string(s.created_at.c_str()));
    json_object_object_add(j, "album_count", json_object_new_double(static_cast<double>(s.album_ids.size())));
    json_object_object_add(j, "domain_count", json_object_new_double(static_cast<double>(s.auth_domains.size())));
    json_object_object_add(j, "acl_count", json_object_new_double(static_cast<double>(s.acls.size())));
    return j;
}

json_object* App::album_to_json(const AlbumRecord& a) {
    json_object* j = json_object_new_object();
    json_object_object_add(j, "id", json_object_new_string(a.id.c_str()));
    json_object_object_add(j, "name", json_object_new_string(a.name.c_str()));
    json_object_object_add(j, "store_id", json_object_new_string(a.store_id.c_str()));
    json_object_object_add(j, "created_at", json_object_new_string(a.created_at.c_str()));
    json_object_object_add(j, "is_public", json_object_new_boolean(a.is_public));
    json_object_object_add(j, "media_count", json_object_new_double(static_cast<double>(a.media_ids.size())));
    return j;
}

json_object* App::media_to_json(const MediaRecord& m) {
    json_object* j = json_object_new_object();
    json_object_object_add(j, "id", json_object_new_string(m.id.c_str()));
    json_object_object_add(j, "filename", json_object_new_string(m.original_filename.c_str()));
    json_object_object_add(j, "kind", json_object_new_string(m.media_kind.c_str()));
    json_object_object_add(j, "content_type", json_object_new_string(m.content_type.c_str()));
    json_object_object_add(j, "album_id", json_object_new_string(m.album_id.c_str()));
    json_object_object_add(j, "store_id", json_object_new_string(m.store_id.c_str()));
    json_object_object_add(j, "created_at", json_object_new_string(m.created_at.c_str()));
    json_object_object_add(j, "size_bytes", json_object_new_double(static_cast<double>(m.size_bytes)));
    return j;
}

json_object* App::domain_to_json(const AuthDomain& d, bool hide_secret) {
    json_object* j = json_object_new_object();
    json_object_object_add(j, "id", json_object_new_string(d.id.c_str()));
    json_object_object_add(j, "label", json_object_new_string(d.label.c_str()));
    json_object_object_add(j, "url", json_object_new_string(d.url.c_str()));
    json_object_object_add(j, "client_name", json_object_new_string(d.client_name.c_str()));
    if (!hide_secret)
        json_object_object_add(j, "client_secret", json_object_new_string(d.client_secret.c_str()));
    return j;
}

json_object* App::acl_to_json(const StoreACL& acl) {
    json_object* j = json_object_new_object();
    json_object_object_add(j, "id", json_object_new_string(acl.id.c_str()));
    json_object_object_add(j, "group_id", json_object_new_string(acl.group_id.c_str()));
    json_object_object_add(j, "group_name", json_object_new_string(acl.group_name.c_str()));
    json_object_object_add(j, "can_read", json_object_new_boolean(acl.can_read));
    json_object_object_add(j, "can_write", json_object_new_boolean(acl.can_write));
    return j;
}

json_object* App::msg_json(const std::string& msg) {
    json_object* j = json_object_new_object();
    json_object_object_add(j, "message", json_object_new_string(msg.c_str()));
    return j;
}

std::string App::normalize_fmt(const std::string& fmt) {
    if (fmt == "jpg") return "jpeg";
    return fmt;
}

int App::parse_int(const std::unordered_map<std::string, std::string>& query, const std::string& key, int fallback) {
    auto it = query.find(key);
    if (it == query.end()) return fallback;
    try { return std::stoi(it->second); } catch (...) { return fallback; }
}

std::string App::parse_string(const std::unordered_map<std::string, std::string>& query, const std::string& key,
                               const std::string& fallback) {
    auto it = query.find(key);
    return it == query.end() ? fallback : it->second;
}

HttpResponse App::error_json(int status, const std::string& message) {
    json_object* json = json_object_new_object();
    json_object_object_add(json, "error", json_object_new_string(message.c_str()));
    return HttpResponse::json(status, json);
}

// ---- import ----

HttpResponse App::start_import(const std::uint8_t* data, std::size_t len) {
    if (!data || len == 0) return error_json(400, "empty body");
    if (import_running_.load()) return error_json(409, "import already in progress");

    // Run synchronously — the HTTP connection stays open, providing
    // natural backpressure.  Each store is parsed and replicated before
    // the next one, so memory stays bounded and replication is confirmed
    // before the client gets a response.
    {
        std::lock_guard<std::mutex> lk(import_mutex_);
        import_running_.store(true);
        import_ok_.store(false);
        import_error_.clear();
    }

    std::cerr << "[import] starting synchronous import (" << len << " bytes)" << "\n";
    bool ok = db_.import_db_from_buffer(data, len);
    {
        std::lock_guard<std::mutex> lk(import_mutex_);
        import_ok_.store(ok);
        if (!ok) import_error_ = "invalid archive format or replication failed";
        import_running_.store(false);
    }
    std::cerr << "[import] finished ok=" << ok << "\n";

    if (ok) {
        json_object* j = json_object_new_object();
        json_object_object_add(j, "message", json_object_new_string("import complete"));
        json_object_object_add(j, "status", json_object_new_string("done"));
        return HttpResponse::json(200, j);
    } else {
        return error_json(500, import_error_.empty() ? "import failed" : import_error_);
    }
}

HttpResponse App::start_import_file(const std::string& path) {
    if (import_running_.load()) return error_json(409, "import already in progress");
    {
        std::lock_guard<std::mutex> lk(import_mutex_);
        import_running_.store(true);
        import_ok_.store(false);
        import_error_.clear();
    }

    std::cerr << "[import] starting file-based import from " << path << "\n";
    bool ok = db_.import_db(path);
    {
        std::lock_guard<std::mutex> lk(import_mutex_);
        import_ok_.store(ok);
        if (!ok) import_error_ = "invalid archive format or replication failed";
        import_running_.store(false);
    }
    std::cerr << "[import] finished ok=" << ok << "\n";

    if (ok) {
        json_object* j = json_object_new_object();
        json_object_object_add(j, "message", json_object_new_string("import complete"));
        json_object_object_add(j, "status", json_object_new_string("done"));
        return HttpResponse::json(200, j);
    } else {
        return error_json(500, import_error_.empty() ? "import failed" : import_error_);
    }
}

std::unique_ptr<ImportSession> App::begin_import() {
    if (import_running_.load()) return nullptr;
    {
        std::lock_guard<std::mutex> lk(import_mutex_);
        import_running_.store(true);
        import_ok_.store(false);
        import_error_.clear();
    }
    auto session = db_.begin_import();
    if (!session) {
        import_running_.store(false);
    }
    return session;
}

void App::finish_import(bool ok, const std::string& error) {
    std::lock_guard<std::mutex> lk(import_mutex_);
    import_ok_.store(ok);
    if (!ok && !error.empty()) import_error_ = error;
    import_running_.store(false);
}

// ---- main router ----

HttpResponse App::handle(const HttpRequest& req) {
    if (req.method == "POST" && req.path == "/api/login")
        return handle_login(req);
    if (req.method == "PUT" && req.path == "/api/upload")
        return error_json(400, "upload must go through RequestEvent");
    if (req.method == "GET" && req.path == "/api/export")
        return handle_export(req);
    if (req.method == "PUT" && req.path == "/api/import")
        return handle_import(req);
    if (req.method == "GET" && req.path == "/api/import/status")
        return handle_import_status(req);
    if (req.method == "POST" && req.path == "/api/vacuum")
        return handle_vacuum(req);
    if (req.method == "GET" && req.path == "/api/cluster/status")
        return handle_cluster_status(req);
    if (req.method == "GET" && req.path == "/api/cluster/stores")
        return handle_cluster_stores(req);
    if (req.method == "POST" && req.path == "/api/cluster/sync")
        return handle_cluster_sync(req);
    if (req.method == "POST" && req.path == "/api/cluster/scrub")
        return handle_cluster_scrub(req);
    if (req.method == "POST" && req.path == "/api/cluster/rebalance")
        return handle_cluster_rebalance(req);
    if (req.method == "GET" && req.path == "/api/groups")
        return handle_list_groups(req);

    if (req.method == "GET" && req.path == "/public/albums")
        return handle_list_public_albums(req);

    if (req.method == "POST" && req.path == "/stores")
        return handle_create_store(req);
    if (req.method == "GET"  && req.path == "/stores")
        return handle_list_stores(req);

    if (starts_with(req.path, "/stores/") && ends_with(req.path, "/domains"))
        if (req.method == "GET" || req.method == "POST")
            return req.method == "GET" ? handle_list_store_domains(req) : handle_add_store_domain(req);
    if (req.method == "DELETE" && starts_with(req.path, "/stores/") && contains(req.path, "/domains/"))
        return handle_remove_store_domain(req);
    if (starts_with(req.path, "/stores/") && ends_with(req.path, "/acls"))
        if (req.method == "GET" || req.method == "POST")
            return req.method == "GET" ? handle_list_store_acls(req) : handle_add_store_acl(req);
    if (req.method == "DELETE" && starts_with(req.path, "/stores/") && contains(req.path, "/acls/"))
        return handle_remove_store_acl(req);
    if (req.method == "POST" && starts_with(req.path, "/stores/") && ends_with(req.path, "/login"))
        return handle_store_login(req);

    if (req.method == "GET"  && starts_with(req.path, "/stores/") && !contains(req.path.substr(8), "/"))
        return handle_get_store(req);
    if (req.method == "DELETE" && starts_with(req.path, "/stores/") && !contains(req.path.substr(8), "/"))
        return handle_delete_store(req);
    if (req.method == "PUT" && starts_with(req.path, "/stores/") && !contains(req.path.substr(8), "/"))
        return handle_rename_store(req);

    if (req.method == "POST" && starts_with(req.path, "/stores/") && ends_with(req.path, "/albums"))
        return handle_create_album(req);
    if (req.method == "GET"  && starts_with(req.path, "/stores/") && ends_with(req.path, "/albums"))
        return handle_list_albums(req);
    if (req.method == "GET"  && starts_with(req.path, "/albums/") && !contains(req.path.substr(8), "/"))
        return handle_get_album(req);
    if (req.method == "DELETE" && starts_with(req.path, "/albums/") && !contains(req.path.substr(8), "/"))
        return handle_delete_album(req);
    if (req.method == "PUT" && starts_with(req.path, "/albums/") && ends_with(req.path, "/public"))
        return handle_set_album_public(req);

    if (req.method == "POST" && starts_with(req.path, "/albums/") && ends_with(req.path, "/media"))
        return handle_add_media(req);
    if (req.method == "GET"  && starts_with(req.path, "/albums/") && ends_with(req.path, "/media"))
        return handle_list_media(req);
    if (req.method == "GET"  && starts_with(req.path, "/media/") && ends_with(req.path, "/preview"))
        return handle_preview(req);
    if (req.method == "GET"  && starts_with(req.path, "/media/") && ends_with(req.path, "/raw"))
        return handle_get_media_raw(req);
    if (req.method == "GET"  && starts_with(req.path, "/media/") && !contains(req.path.substr(7), "/"))
        return handle_get_media(req);
    if (req.method == "DELETE" && starts_with(req.path, "/media/") && !contains(req.path.substr(7), "/"))
        return handle_delete_media(req);

    if (req.method == "GET")
        return handle_static(req);

    return HttpResponse::text(404, "not found\n");
}

// ---- upload form handler ----

HttpResponse App::handle_upload_form(const HttpRequest& req, UploadData upload) {
    if (!is_any_authorized(req)) return error_json(401, "unauthorized");
    if (!auth_.check_gpo(req, GPO_MEDIA_ADD)) if (!is_session_known(req)) return error_json(403, "no permission: media.add");

    if (upload.album_id.empty()) return error_json(400, "field 'album_id' missing");
    if (upload.filename.empty() || upload.file_data.empty()) return error_json(400, "field 'file' missing");

    auto album = db_.get_album(upload.album_id);
    if (!album) return error_json(404, "album not found");
    if (!has_store_write_access(req, album->store_id)) return error_json(403, "no write access to this store");

    const fs::path fname(upload.filename);
    const std::string ct = mime_for_ext(fname.extension().string());

    auto m = db_.add_media(upload.album_id, upload.filename, ct, std::move(upload.file_data));
    if (!m) return error_json(500, "failed to register media");
    return HttpResponse::json(201, media_to_json(*m));
}

// ---- import form handler ----

HttpResponse App::handle_import_form(const HttpRequest& req, std::vector<std::uint8_t> file_data) {
    if (!auth_.is_authorized(req)) return error_json(401, "unauthorized");
    if (!auth_.check_gpo(req, GPO_IMPORT)) return error_json(403, "no permission: db.import");
    if (file_data.empty()) return error_json(400, "field 'file' missing");
    if (import_running_.load()) return error_json(409, "import already in progress");

    return start_import(file_data.data(), file_data.size());
}

HttpResponse App::handle_import_status(const HttpRequest& req) {
    if (!auth_.is_authorized(req)) return error_json(401, "unauthorized");
    json_object* j = json_object_new_object();
    if (import_running_.load()) {
        json_object_object_add(j, "status", json_object_new_string("running"));
    } else if (import_ok_.load()) {
        json_object_object_add(j, "status", json_object_new_string("done"));
        json_object_object_add(j, "message", json_object_new_string("import successful"));
    } else {
        std::lock_guard<std::mutex> lk(import_mutex_);
        json_object_object_add(j, "status", json_object_new_string("failed"));
        json_object_object_add(j, "error", json_object_new_string(import_error_.c_str()));
    }
    return HttpResponse::json(200, j);
}

// ---- vacuum handler ----

HttpResponse App::handle_vacuum(const HttpRequest& req) {
    if (!auth_.is_authorized(req)) return error_json(401, "unauthorized");
    if (!auth_.check_gpo(req, GPO_EXPORT)) return error_json(403, "no permission: db.export");

    // Optional: vacuum specific store via query parameter ?store=<id>
    auto sit = req.query.find("store");
    if (sit != req.query.end() && !sit->second.empty()) {
        db_.vacuum_store(sit->second);
        json_object* j = json_object_new_object();
        json_object_object_add(j, "message", json_object_new_string("vacuum completed"));
        json_object_object_add(j, "store", json_object_new_string(sit->second.c_str()));
        return HttpResponse::json(200, j);
    }

    db_.vacuum_all_stores();
    json_object* j = json_object_new_object();
    json_object_object_add(j, "message", json_object_new_string("vacuum completed for all stores"));
    return HttpResponse::json(200, j);
}

// ---- cluster handlers ----

HttpResponse App::handle_cluster_status(const HttpRequest& req) {
    if (!auth_.is_authorized(req)) return error_json(401, "unauthorized");
    json_object* j = json_object_new_object();

    if (!g_Cluster || !g_Cluster->isRunning()) {
        json_object_object_add(j, "enabled", json_object_new_boolean(false));
        return HttpResponse::json(200, j);
    }

    json_object_object_add(j, "enabled", json_object_new_boolean(true));

    int self_idx = g_Cluster->getSelfNodeIndex();
    size_t total_peers = g_Cluster->getTotalPeers();

    auto& pclient = g_Cluster->getClient();
    if (pclient) {
        decltype(pclient->get_cluster_status()) health;
        try {
            health = pclient->get_cluster_status();
        } catch (...) {
            json_object_object_add(j, "status", json_object_new_string("UNAVAILABLE"));
            json_object_object_add(j, "nodes_total", json_object_new_int(0));
            json_object_object_add(j, "nodes_online", json_object_new_int(0));
            return HttpResponse::json(200, j);
        }

        // Self is now included in pclient via set_local_node, no separate +1
        uint32_t nodes_online = health.nodes_online;
        uint32_t nodes_total = static_cast<uint32_t>(total_peers);

        const char* statusStr = "OK";
        if (nodes_online < nodes_total) {
            statusStr = "DEGRADED";
        }
        if (health.overall == paritypp::node_status::REBUILD) {
            statusStr = "REBUILD";
        }

        json_object_object_add(j, "status", json_object_new_string(statusStr));
        json_object_object_add(j, "nodes_total", json_object_new_int(nodes_total));
        json_object_object_add(j, "nodes_online", json_object_new_int(nodes_online));

        json_object* jnodes = json_object_new_array();

        // Build reverse map: pclient node idx -> config peer idx
        std::unordered_map<size_t, size_t> client_to_peer;
        for (const auto& [peer_idx, client_idx] : g_Cluster->getPeerToClientIndex()) {
            client_to_peer[client_idx] = peer_idx;
        }

        for (const auto& [idx, info] : health.node_statuses) {
            json_object* jn = json_object_new_object();
            auto cit = client_to_peer.find(idx);
            int peer_idx = (cit != client_to_peer.end()) ? static_cast<int>(cit->second) : static_cast<int>(idx);
            json_object_object_add(jn, "index", json_object_new_int(peer_idx));
            const char* nstat = "OK";
            if (info.status == paritypp::node_status::DEGRADED) nstat = "DEGRADED";
            else if (info.status == paritypp::node_status::REBUILD) nstat = "REBUILD";
            json_object_object_add(jn, "status", json_object_new_string(nstat));
            json_object_object_add(jn, "blocks", json_object_new_int(info.block_count));
            json_object_object_add(jn, "groups", json_object_new_int(info.group_count));
            json_object_object_add(jn, "peers_total", json_object_new_int(info.peers_total));
            json_object_object_add(jn, "peers_online", json_object_new_int(info.peers_online));
            json_object_object_add(jn, "scrub_running", json_object_new_boolean(info.scrub_running));
            json_object_array_add(jnodes, jn);
        }
        json_object_object_add(j, "nodes", jnodes);
    }

    json_object_object_add(j, "scrub_running",
        json_object_new_boolean(g_Cluster->isScrubRunning()));

    return HttpResponse::json(200, j);
}

HttpResponse App::handle_cluster_stores(const HttpRequest& req) {
    if (!auth_.is_authorized(req)) return error_json(401, "unauthorized");

    json_object* j = json_object_new_object();

    if (!g_Cluster || !g_Cluster->isRunning()) {
        json_object_object_add(j, "enabled", json_object_new_boolean(false));
        return HttpResponse::json(200, j);
    }

    json_object_object_add(j, "enabled", json_object_new_boolean(true));

    auto stores = db_.list_stores();

    struct StoreGroupInfo {
        std::string store_id;
        std::string store_name;
        uint64_t group_id;
    };
    std::vector<StoreGroupInfo> store_groups;
    store_groups.reserve(stores.size() + 1);
    store_groups.push_back({"__index__", "Index (master)", cluster_group_id("index")});
    for (const auto& s : stores) {
        store_groups.push_back({s.id, s.name, cluster_group_id("store:" + s.id)});
    }

    auto peer_groups = g_Cluster->list_peer_groups();

    std::vector<std::unordered_set<uint64_t>> node_group_sets(peer_groups.size());
    for (size_t i = 0; i < peer_groups.size(); ++i) {
        node_group_sets[i].insert(peer_groups[i].groups.begin(),
                                  peer_groups[i].groups.end());
    }

    json_object* jnodes = json_object_new_array();
    for (const auto& ng : peer_groups) {
        json_object* jn = json_object_new_object();
        json_object_object_add(jn, "index", json_object_new_int(static_cast<int>(ng.index)));
        json_object_object_add(jn, "address", json_object_new_string(ng.address.c_str()));
        json_object_object_add(jn, "port", json_object_new_int(ng.port));
        json_object_object_add(jn, "online", json_object_new_boolean(ng.online));
        json_object_array_add(jnodes, jn);
    }
    json_object_object_add(j, "nodes", jnodes);

    json_object* jstores = json_object_new_array();
    for (const auto& sg : store_groups) {
        json_object* js = json_object_new_object();
        json_object_object_add(js, "store_id", json_object_new_string(sg.store_id.c_str()));
        json_object_object_add(js, "store_name", json_object_new_string(sg.store_name.c_str()));

        json_object* jrep = json_object_new_array();
        for (size_t i = 0; i < peer_groups.size(); ++i) {
            json_object* jr = json_object_new_object();
            json_object_object_add(jr, "node_index", json_object_new_int(static_cast<int>(i)));
            bool replicated = peer_groups[i].online &&
                              node_group_sets[i].count(sg.group_id) > 0;
            json_object_object_add(jr, "replicated", json_object_new_boolean(replicated));
            json_object_array_add(jrep, jr);
        }
        json_object_object_add(js, "nodes", jrep);

        int replicated_count = 0;
        for (size_t i = 0; i < peer_groups.size(); ++i) {
            if (peer_groups[i].online && node_group_sets[i].count(sg.group_id) > 0)
                ++replicated_count;
        }
        int total_nodes = static_cast<int>(peer_groups.size());
        json_object_object_add(js, "replicated_count", json_object_new_int(replicated_count));
        json_object_object_add(js, "total_nodes", json_object_new_int(total_nodes));

        json_object_array_add(jstores, js);
    }
    json_object_object_add(j, "stores", jstores);

    return HttpResponse::json(200, j);
}

HttpResponse App::handle_cluster_sync(const HttpRequest& req) {
    if (!auth_.is_authorized(req)) return error_json(401, "unauthorized");

    if (!g_Cluster || !g_Cluster->isRunning()) {
        return error_json(400, "cluster not enabled");
    }

    auto* cluster_backend = dynamic_cast<ClusterMediaBackend*>(&db_);
    if (!cluster_backend) {
        return error_json(500, "backend does not support cluster sync");
    }

    try {
        cluster_backend->sync_from_cluster();
    } catch (const std::exception& e) {
        return error_json(500, std::string("sync failed: ") + e.what());
    }

    json_object* j = json_object_new_object();
    json_object_object_add(j, "message", json_object_new_string("sync completed"));
    return HttpResponse::json(200, j);
}

HttpResponse App::handle_cluster_scrub(const HttpRequest& req) {
    if (!auth_.is_authorized(req)) return error_json(401, "unauthorized");

    if (!g_Cluster || !g_Cluster->isRunning()) {
        return error_json(400, "cluster not enabled");
    }

    if (g_Cluster->isCritical()) {
        return error_json(503, "cluster is critical — need more nodes online");
    }

    auto result = g_Cluster->scrub();

    json_object* j = json_object_new_object();
    json_object_object_add(j, "status", json_object_new_string("ok"));
    json_object_object_add(j, "groups_checked", json_object_new_int(static_cast<int>(result.groups_checked)));
    json_object_object_add(j, "groups_repaired", json_object_new_int(static_cast<int>(result.groups_repaired)));
    json_object_object_add(j, "groups_failed", json_object_new_int(static_cast<int>(result.groups_failed)));
    return HttpResponse::json(200, j);
}

HttpResponse App::handle_cluster_rebalance(const HttpRequest& req) {
    if (!auth_.is_authorized(req)) return error_json(401, "unauthorized");

    if (!g_Cluster || !g_Cluster->isRunning()) {
        return error_json(400, "cluster not enabled");
    }

    if (g_Cluster->isCritical()) {
        return error_json(503, "cluster is critical — need more nodes online");
    }

    auto& client = g_Cluster->getClient();
    if (!client) {
        return error_json(500, "no parity client");
    }

    auto scrub_lk = g_Cluster->tryScrubLock();
    if (!scrub_lk.owns_lock()) {
        return error_json(409, "scrub or rebalance already running");
    }

    auto rb = client->rebalance();

    json_object* j = json_object_new_object();
    json_object_object_add(j, "status", json_object_new_string("ok"));
    json_object_object_add(j, "rebalanced", json_object_new_int(static_cast<int>(rb.rebalanced)));
    json_object_object_add(j, "already_ok", json_object_new_int(static_cast<int>(rb.already_ok)));
    json_object_object_add(j, "failed", json_object_new_int(static_cast<int>(rb.failed)));
    return HttpResponse::json(200, j);
}

// ---- store handlers ----

HttpResponse App::handle_create_store(const HttpRequest& req) {
    if (!auth_.is_authorized(req)) return error_json(401, "unauthorized");
    if (!auth_.check_gpo(req, GPO_STORE_CREATE)) return error_json(403, "no permission: store.create");
    json_object* root = json_tokener_parse(req.body.c_str());
    if (!root) return error_json(400, "invalid json");
    json_object* jname = json_object_object_get(root, "name");
    if (!json_object_is_type(jname, json_type_string) || !json_object_get_string(jname)) { json_object_put(root); return error_json(400, "field 'name' missing"); }
    std::string name(json_object_get_string(jname));
    json_object_put(root);
    auto s = db_.create_store(name);
    return HttpResponse::json(201, store_to_json(s));
}

HttpResponse App::handle_list_stores(const HttpRequest& req) {
    if (!is_any_authorized(req)) return error_json(401, "unauthorized");
    auto stores = db_.list_stores();
    json_object* arr = json_object_new_array();
    for (auto& s : stores) {
        if (!has_store_access(req, s.id)) continue;
        json_object_array_add(arr, store_to_json(s));
    }
    json_object* wrap = json_object_new_object();
    json_object_object_add(wrap, "stores", arr);
    return HttpResponse::json(200, wrap);
}

HttpResponse App::handle_get_store(const HttpRequest& req) {
    if (!is_any_authorized(req)) return error_json(401, "unauthorized");
    auto id = segment(req.path, 8);
    if (!has_store_access(req, id)) return error_json(403, "no access to this store");
    auto s = db_.get_store(id);
    if (!s) return error_json(404, "store not found");
    return HttpResponse::json(200, store_to_json(*s));
}

HttpResponse App::handle_delete_store(const HttpRequest& req) {
    if (!auth_.is_authorized(req)) return error_json(401, "unauthorized");
    if (!auth_.check_gpo(req, GPO_STORE_DELETE)) return error_json(403, "no permission: store.delete");
    auto id = segment(req.path, 8);
    if (!has_store_access(req, id)) return error_json(403, "no access to this store");
    if (!db_.delete_store(id)) return error_json(404, "store not found");
    return HttpResponse::json(200, msg_json("deleted"));
}

HttpResponse App::handle_rename_store(const HttpRequest& req) {
    if (!auth_.is_authorized(req)) return error_json(401, "unauthorized");
    if (!auth_.check_gpo(req, GPO_STORE_EDIT)) return error_json(403, "no permission: store.edit");
    auto id = segment(req.path, 8);
    json_object* root = json_tokener_parse(req.body.c_str());
    if (!root) return error_json(400, "invalid json");
    json_object* jname = json_object_object_get(root, "name");
    if (!json_object_is_type(jname, json_type_string) || !json_object_get_string(jname)) { json_object_put(root); return error_json(400, "field 'name' missing"); }
    std::string name(json_object_get_string(jname));
    json_object_put(root);
    if (!db_.rename_store(id, name)) return error_json(404, "store not found");
    auto s = db_.get_store(id);
    if (!s) return error_json(404, "store not found");
    return HttpResponse::json(200, store_to_json(*s));
}

// ---- store auth domain handlers ----

HttpResponse App::handle_list_store_domains(const HttpRequest& req) {
    if (!auth_.is_authorized(req)) return error_json(401, "unauthorized");
    auto store_id = segment(req.path, 8);
    if (!db_.get_store(store_id)) return error_json(404, "store not found");
    auto domains = db_.list_store_domains(store_id);
    json_object* arr = json_object_new_array();
    for (const auto& d : domains) json_object_array_add(arr, domain_to_json(d));
    json_object* wrap = json_object_new_object();
    json_object_object_add(wrap, "domains", arr);
    return HttpResponse::json(200, wrap);
}

HttpResponse App::handle_add_store_domain(const HttpRequest& req) {
    if (!auth_.is_authorized(req)) return error_json(401, "unauthorized");
    if (!auth_.check_gpo(req, GPO_STORE_EDIT)) return error_json(403, "no permission: store.edit");
    auto store_id = segment(req.path, 8);
    if (!db_.get_store(store_id)) return error_json(404, "store not found");
    json_object* root = json_tokener_parse(req.body.c_str());
    if (!root) return error_json(400, "invalid json");
    json_object* jlabel = json_object_object_get(root, "label");
    json_object* jurl   = json_object_object_get(root, "url");
    json_object* jname  = json_object_object_get(root, "client_name");
    json_object* jsec   = json_object_object_get(root, "client_secret");
    if (!json_object_is_type(jlabel, json_type_string) || !json_object_is_type(jurl, json_type_string) ||
        !json_object_is_type(jname, json_type_string) || !json_object_is_type(jsec, json_type_string)) {
        json_object_put(root);
        return error_json(400, "fields label, url, client_name, client_secret required");
    }
    AuthDomain dom;
    dom.label = json_object_get_string(jlabel);
    dom.url = json_object_get_string(jurl);
    dom.client_name = json_object_get_string(jname);
    dom.client_secret = json_object_get_string(jsec);
    json_object_put(root);
    if (dom.label.empty() || dom.url.empty() || dom.client_name.empty())
        return error_json(400, "label, url, client_name must not be empty");
    std::string did = db_.add_store_domain(store_id, std::move(dom));
    if (did.empty()) return error_json(404, "store not found");
    json_object* j = json_object_new_object();
    json_object_object_add(j, "message", json_object_new_string("domain added"));
    json_object_object_add(j, "id", json_object_new_string(did.c_str()));
    return HttpResponse::json(201, j);
}

HttpResponse App::handle_remove_store_domain(const HttpRequest& req) {
    if (!auth_.is_authorized(req)) return error_json(401, "unauthorized");
    if (!auth_.check_gpo(req, GPO_STORE_EDIT)) return error_json(403, "no permission: store.edit");
    auto store_id = segment(req.path, 8);
    const std::string domains_prefix = "/domains/";
    auto pos = req.path.find(domains_prefix);
    if (pos == std::string::npos) return error_json(400, "invalid path");
    std::string domain_id = req.path.substr(pos + domains_prefix.size());
    if (domain_id.empty()) return error_json(400, "domain id missing");
    if (!db_.remove_store_domain(store_id, domain_id)) return error_json(404, "store or domain not found");
    return HttpResponse::json(200, msg_json("domain removed"));
}

// ---- store ACL handlers ----

HttpResponse App::handle_list_store_acls(const HttpRequest& req) {
    if (!auth_.is_authorized(req)) return error_json(401, "unauthorized");
    auto store_id = segment(req.path, 8);
    if (!db_.get_store(store_id)) return error_json(404, "store not found");
    auto acls = db_.list_store_acls(store_id);
    json_object* arr = json_object_new_array();
    for (const auto& a : acls) json_object_array_add(arr, acl_to_json(a));
    json_object* wrap = json_object_new_object();
    json_object_object_add(wrap, "acls", arr);
    return HttpResponse::json(200, wrap);
}

HttpResponse App::handle_add_store_acl(const HttpRequest& req) {
    if (!auth_.is_authorized(req)) return error_json(401, "unauthorized");
    if (!auth_.check_gpo(req, GPO_STORE_EDIT)) return error_json(403, "no permission: store.edit");
    auto store_id = segment(req.path, 8);
    if (!db_.get_store(store_id)) return error_json(404, "store not found");
    json_object* root = json_tokener_parse(req.body.c_str());
    if (!root) return error_json(400, "invalid json");
    json_object* jgid  = json_object_object_get(root, "group_id");
    json_object* jname = json_object_object_get(root, "group_name");
    json_object* jr    = json_object_object_get(root, "can_read");
    json_object* jw    = json_object_object_get(root, "can_write");
    if (!json_object_is_type(jgid, json_type_string) || !json_object_is_type(jname, json_type_string)) {
        json_object_put(root);
        return error_json(400, "fields group_id, group_name required");
    }
    StoreACL acl;
    acl.group_id   = json_object_get_string(jgid);
    acl.group_name = json_object_get_string(jname);
    acl.can_read   = json_object_is_type(jr, json_type_boolean) ? json_object_get_boolean(jr) : false;
    acl.can_write  = json_object_is_type(jw, json_type_boolean) ? json_object_get_boolean(jw) : false;
    json_object_put(root);
    if (acl.group_id.empty() || acl.group_name.empty())
        return error_json(400, "group_id and group_name must not be empty");
    std::string aid = db_.add_store_acl(store_id, std::move(acl));
    if (aid.empty()) return error_json(404, "store not found");
    json_object* j = json_object_new_object();
    json_object_object_add(j, "message", json_object_new_string("acl added"));
    json_object_object_add(j, "id", json_object_new_string(aid.c_str()));
    return HttpResponse::json(201, j);
}

HttpResponse App::handle_remove_store_acl(const HttpRequest& req) {
    if (!auth_.is_authorized(req)) return error_json(401, "unauthorized");
    if (!auth_.check_gpo(req, GPO_STORE_EDIT)) return error_json(403, "no permission: store.edit");
    auto store_id = segment(req.path, 8);
    const std::string acls_prefix = "/acls/";
    auto pos = req.path.find(acls_prefix);
    if (pos == std::string::npos) return error_json(400, "invalid path");
    std::string acl_id = req.path.substr(pos + acls_prefix.size());
    if (acl_id.empty()) return error_json(400, "acl id missing");
    if (!db_.remove_store_acl(store_id, acl_id)) return error_json(404, "store or acl not found");
    return HttpResponse::json(200, msg_json("acl removed"));
}

// ---- groups handler ----

HttpResponse App::handle_list_groups(const HttpRequest& req) {
    if (!auth_.is_authorized(req)) return error_json(401, "unauthorized");

    const std::string domain = get_session_domain(req);

    try {
        authdb::client::ClientConnection con;

        if (!domain.empty()) {
            auto dom = db_.find_domain_by_label(domain);
            if (!dom) return error_json(404, "unknown domain: " + domain);
            con.setUrl(dom->url);
            con.setClientName(dom->client_name);
            con.setClientSecret(dom->client_secret);
        } else {
            con.setUrl(auth_.url());
            con.setClientName(auth_.client_name());
            con.setClientSecret(auth_.client_secret());
        }

        authdb::client::Client client(con);
        if (!client.ClientAuth())
            return error_json(500, "client authentication failed");

        auto groups = client.listGroups();

        json_object* arr = json_object_new_array();
        for (const auto& g : groups) {
            json_object* jgrp = json_object_new_object();
            json_object_object_add(jgrp, "gid", json_object_new_string(g.gid.c_str()));
            json_object_object_add(jgrp, "name", json_object_new_string(g.name.c_str()));
            json_object_array_add(arr, jgrp);
        }
        json_object* wrap = json_object_new_object();
        json_object_object_add(wrap, "groups", arr);
        return HttpResponse::json(200, wrap);
    } catch (const std::exception& e) {
        return error_json(500, std::string("failed to list groups: ") + e.what());
    } catch (...) {
        return error_json(500, "failed to list groups");
    }
}

// ---- store login handler ----

HttpResponse App::handle_store_login(const HttpRequest& req) {
    auto store_id = segment(req.path, 8);
    auto store = db_.get_store(store_id);
    if (!store) return error_json(404, "store not found");

    json_object* root = json_tokener_parse(req.body.c_str());
    if (!root) return error_json(400, "invalid json");
    json_object* juser = json_object_object_get(root, "username");
    json_object* jpass = json_object_object_get(root, "password");
    json_object* jdomain = json_object_object_get(root, "domain_id");
    if (!json_object_is_type(juser, json_type_string) || !json_object_get_string(juser) ||
        !json_object_is_type(jpass, json_type_string) || !json_object_get_string(jpass)) {
        json_object_put(root);
        return error_json(400, "username and password required");
    }
    const std::string user(json_object_get_string(juser));
    const std::string pass(json_object_get_string(jpass));
    const std::string domain_id = (json_object_is_type(jdomain, json_type_string) && json_object_get_string(jdomain)) ? json_object_get_string(jdomain) : "";
    json_object_put(root);

    auto domains = db_.list_store_domains(store_id);
    if (domains.empty())
        return error_json(400, "no auth domains configured for this store");

    const AuthDomain* target = nullptr;
    if (!domain_id.empty()) {
        for (const auto& d : domains) {
            if (d.id == domain_id) { target = &d; break; }
        }
        if (!target) return error_json(404, "domain not found");
    } else {
        for (const auto& d : domains) {
            try {
                authdb::client::ClientConnection con;
                con.setUrl(d.url);
                con.setClientName(d.client_name);
                con.setClientSecret(d.client_secret);
                authdb::client::Client client(con);
                if (!client.ClientAuth()) continue;
                con.setUsername(user);
                con.setUserPassword(pass);
                if (!client.UserAuth()) continue;
                const std::string session_token = con.getSessionID().c_str();
                if (session_token.empty()) continue;
                try { auth_.try_init_policies(client, d.label); } catch (...) {}
                auto grps = resolve_session_groups(client);
                store_session(session_token, user, d.label, grps);
                json_object* j = json_object_new_object();
                json_object_object_add(j, "token", json_object_new_string(session_token.c_str()));
                json_object_object_add(j, "username", json_object_new_string(user.c_str()));
                json_object_object_add(j, "domain", json_object_new_string(d.label.c_str()));
                return HttpResponse::json(200, j);
            } catch (...) { continue; }
        }
        return error_json(401, "invalid credentials for all configured domains");
    }

    try {
        authdb::client::ClientConnection con;
        con.setUrl(target->url);
        con.setClientName(target->client_name);
        con.setClientSecret(target->client_secret);
        authdb::client::Client client(con);
        if (!client.ClientAuth())
            return error_json(500, "client authentication failed for domain");
        con.setUsername(user);
        con.setUserPassword(pass);
        if (!client.UserAuth())
            return error_json(401, "invalid credentials");
        const std::string session_token = con.getSessionID().c_str();
        if (session_token.empty())
            return error_json(500, "login succeeded but no session token returned");
        try { auth_.try_init_policies(client, target->label); } catch (...) {}
        auto grps = resolve_session_groups(client);
        store_session(session_token, user, target->label, grps);
        json_object* j = json_object_new_object();
        json_object_object_add(j, "token", json_object_new_string(session_token.c_str()));
        json_object_object_add(j, "username", json_object_new_string(user.c_str()));
        json_object_object_add(j, "domain", json_object_new_string(target->label.c_str()));
        return HttpResponse::json(200, j);
    } catch (const authdb::AuthBackendError& e) {
        return error_json(401, std::string("authentication failed: ") + e.what());
    } catch (const std::exception& e) {
        return error_json(500, std::string("auth service error: ") + e.what());
    } catch (...) {
        return error_json(500, "auth service error");
    }
}

// ---- album handlers ----

HttpResponse App::handle_create_album(const HttpRequest& req) {
    if (!is_any_authorized(req)) return error_json(401, "unauthorized");
    if (!auth_.check_gpo(req, GPO_ALBUM_CREATE)) if (!is_session_known(req)) return error_json(403, "no permission: album.create");
    auto store_id = segment(req.path, 8);
    if (!has_store_write_access(req, store_id)) return error_json(403, "no write access to this store");
    json_object* root = json_tokener_parse(req.body.c_str());
    if (!root) return error_json(400, "invalid json");
    json_object* jname = json_object_object_get(root, "name");
    if (!json_object_is_type(jname, json_type_string) || !json_object_get_string(jname)) { json_object_put(root); return error_json(400, "field 'name' missing"); }
    std::string name(json_object_get_string(jname));
    json_object_put(root);
    auto a = db_.create_album(store_id, name);
    if (!a) return error_json(404, "store not found");
    return HttpResponse::json(201, album_to_json(*a));
}

HttpResponse App::handle_list_albums(const HttpRequest& req) {
    if (!is_any_authorized(req)) return error_json(401, "unauthorized");
    auto store_id = segment(req.path, 8);
    if (!has_store_access(req, store_id)) return error_json(403, "no access to this store");
    auto albums = db_.list_albums(store_id);
    json_object* arr = json_object_new_array();
    for (auto& a : albums) json_object_array_add(arr, album_to_json(a));
    json_object* wrap = json_object_new_object();
    json_object_object_add(wrap, "albums", arr);
    return HttpResponse::json(200, wrap);
}

HttpResponse App::handle_get_album(const HttpRequest& req) {
    auto id = segment(req.path, 8);
    auto a = db_.get_album(id);
    if (!a) return error_json(404, "album not found");
    if (!a->is_public) {
        if (!is_any_authorized(req)) return error_json(401, "unauthorized");
        if (!has_store_access(req, a->store_id)) return error_json(403, "no access to this store");
    }
    return HttpResponse::json(200, album_to_json(*a));
}

HttpResponse App::handle_delete_album(const HttpRequest& req) {
    if (!is_any_authorized(req)) return error_json(401, "unauthorized");
    if (!auth_.check_gpo(req, GPO_ALBUM_DELETE)) if (!is_session_known(req)) return error_json(403, "no permission: album.delete");
    auto id = segment(req.path, 8);
    auto a = db_.get_album(id);
    if (!a) return error_json(404, "album not found");
    if (!has_store_write_access(req, a->store_id)) return error_json(403, "no write access to this store");
    if (!db_.delete_album(id)) return error_json(404, "album not found");
    return HttpResponse::json(200, msg_json("deleted"));
}

HttpResponse App::handle_set_album_public(const HttpRequest& req) {
    if (!auth_.is_authorized(req)) return error_json(401, "unauthorized");
    if (!auth_.check_gpo(req, GPO_ALBUM_EDIT)) if (!is_session_known(req)) return error_json(403, "no permission: album.edit");
    auto id = segment(req.path, 8);
    json_object* root = json_tokener_parse(req.body.c_str());
    if (!root) return error_json(400, "invalid json");
    json_object* jpub = json_object_object_get(root, "public");
    if (!json_object_is_type(jpub, json_type_boolean)) { json_object_put(root); return error_json(400, "field 'public' (bool) missing"); }
    bool is_public = json_object_get_boolean(jpub);
    json_object_put(root);
    if (!db_.set_album_public(id, is_public)) return error_json(404, "album not found");
    auto a = db_.get_album(id);
    if (!a) return error_json(404, "album not found");
    return HttpResponse::json(200, album_to_json(*a));
}

HttpResponse App::handle_list_public_albums(const HttpRequest& /*req*/) {
    auto albums = db_.list_public_albums();
    json_object* arr = json_object_new_array();
    for (auto& a : albums) json_object_array_add(arr, album_to_json(a));
    json_object* wrap = json_object_new_object();
    json_object_object_add(wrap, "albums", arr);
    return HttpResponse::json(200, wrap);
}

// ---- media handlers ----

HttpResponse App::handle_add_media(const HttpRequest& req) {
    if (!is_any_authorized(req)) return error_json(401, "unauthorized");
    if (!auth_.check_gpo(req, GPO_MEDIA_ADD)) if (!is_session_known(req)) return error_json(403, "no permission: media.add");
    auto album_id = segment(req.path, 8);
    auto album = db_.get_album(album_id);
    if (!album) return error_json(404, "album not found");
    if (!has_store_write_access(req, album->store_id)) return error_json(403, "no write access to this store");
    json_object* root = json_tokener_parse(req.body.c_str());
    if (!root) return error_json(400, "invalid json");
    json_object* jpath = json_object_object_get(root, "path");
    if (!json_object_is_type(jpath, json_type_string) || !json_object_get_string(jpath)) { json_object_put(root); return error_json(400, "field 'path' missing"); }
    std::string path(json_object_get_string(jpath));
    json_object_put(root);
    if (!fs::exists(path)) return error_json(404, "source file not found");
    const auto fsize = fs::file_size(path);
    std::vector<std::uint8_t> data(fsize);
    {
        std::ifstream in(path, std::ios::binary);
        if (!in.is_open()) return error_json(500, "cannot read source file");
        in.read(reinterpret_cast<char*>(data.data()), static_cast<std::streamsize>(fsize));
    }
    const fs::path fp(path);
    const std::string ct = mime_for_ext(fp.extension().string());
    auto m = db_.add_media(album_id, fp.filename().string(), ct, std::move(data));
    if (!m) return error_json(404, "album not found");
    return HttpResponse::json(201, media_to_json(*m));
}

HttpResponse App::handle_list_media(const HttpRequest& req) {
    auto album_id = segment(req.path, 8);
    if (!db_.is_album_public(album_id) && !is_any_authorized(req)) return error_json(401, "unauthorized");
    if (is_any_authorized(req)) {
        auto album = db_.get_album(album_id);
        if (album && !has_store_access(req, album->store_id)) return error_json(403, "no access to this store");
    }
    auto items = db_.list_media(album_id);
    json_object* arr = json_object_new_array();
    for (auto& m : items) json_object_array_add(arr, media_to_json(m));
    json_object* wrap = json_object_new_object();
    json_object_object_add(wrap, "media", arr);
    return HttpResponse::json(200, wrap);
}

HttpResponse App::handle_get_media(const HttpRequest& req) {
    auto id = segment(req.path, 7);
    auto m = db_.get_media(id);
    if (!m) return error_json(404, "media not found");
    if (!db_.is_media_public(id)) {
        if (!is_any_authorized(req)) return error_json(401, "unauthorized");
        if (!has_store_access(req, m->store_id)) return error_json(403, "no access to this store");
    }
    return HttpResponse::json(200, media_to_json(*m));
}

HttpResponse App::handle_delete_media(const HttpRequest& req) {
    if (!is_any_authorized(req)) return error_json(401, "unauthorized");
    if (!auth_.check_gpo(req, GPO_MEDIA_DELETE)) if (!is_session_known(req)) return error_json(403, "no permission: media.delete");
    auto id = segment(req.path, 7);
    auto m = db_.get_media(id);
    if (m && !has_store_write_access(req, m->store_id)) return error_json(403, "no write access to this store");
    if (!db_.delete_media(id)) return error_json(404, "media not found");
    return HttpResponse::json(200, msg_json("deleted"));
}

// ---- raw data handler ----

HttpResponse App::handle_get_media_raw(const HttpRequest& req) {
    const std::string prefix = "/media/";
    const std::string suffix = "/raw";
    const auto start = prefix.size();
    const auto end = req.path.find(suffix, start);
    if (end == std::string::npos || end <= start)
        return error_json(400, "invalid raw path");
    const std::string media_id = req.path.substr(start, end - start);

    if (!db_.is_media_public(media_id)) {
        bool authorized = is_any_authorized(req);
        if (!authorized) {
            auto tok_it = req.query.find("token");
            if (tok_it != req.query.end() && !tok_it->second.empty()) {
                HttpRequest patched = req;
                patched.headers["authorization"] = "Bearer " + tok_it->second;
                authorized = is_any_authorized(patched);
            }
        }
        if (!authorized) return error_json(401, "unauthorized");
        auto media = db_.get_media(media_id);
        if (media && !has_store_access(req, media->store_id)) return error_json(403, "no access to this store");
    }

    auto media = db_.get_media(media_id);
    if (!media) return error_json(404, "media not found");

    const std::uint64_t total_size = media->size_bytes;
    if (total_size == 0) return error_json(404, "media has no data");

    auto range_it = req.headers.find("range");
    if (range_it != req.headers.end()) {
        const std::string& range_val = range_it->second;
        std::uint64_t range_start = 0, range_end = total_size - 1;
        if (range_val.compare(0, 6, "bytes=") == 0) {
            std::string range = range_val.substr(6);
            auto dash = range.find('-');
            if (dash != std::string::npos) {
                if (dash == 0 && dash + 1 < range.size()) {
                    auto suffix_len = std::stoull(range.substr(1));
                    if (suffix_len >= total_size) {
                        range_start = 0;
                    } else {
                        range_start = total_size - suffix_len;
                    }
                    range_end = total_size - 1;
                } else {
                    if (dash > 0)
                        range_start = std::stoull(range.substr(0, dash));
                    if (dash + 1 < range.size())
                        range_end = std::stoull(range.substr(dash + 1));
                }
            }
        }

        if (range_start >= total_size) {
            HttpResponse resp;
            resp.status = 416;
            resp.content_type = media->content_type;
            resp.headers["Content-Range"] = "bytes */" + std::to_string(total_size);
            return resp;
        }

        if (range_end >= total_size) range_end = total_size - 1;

        // Cap open-ended range requests to 2 MB chunks.  Browsers send
        // "Range: bytes=0-" and expect the server to deliver a reasonable
        // chunk, not the entire file.  Without a cap the cluster backend
        // would try to fetch all stripes at once, which times out for
        // large videos.
        constexpr std::uint64_t MAX_RANGE_CHUNK = 2 * 1024 * 1024; // 2 MB
        if (range_end - range_start + 1 > MAX_RANGE_CHUNK)
            range_end = range_start + MAX_RANGE_CHUNK - 1;

        const std::uint64_t length = range_end - range_start + 1;

        auto data = db_.read_media_data_range(media_id, range_start, length);
        if (data.empty()) return error_json(500, "could not read media data range");

        HttpResponse resp;
        resp.status = 206;
        resp.content_type = media->content_type;
        resp.headers["Accept-Ranges"] = "bytes";
        resp.headers["Content-Range"] = "bytes " + std::to_string(range_start) + "-"
                                       + std::to_string(range_end) + "/" + std::to_string(total_size);
        resp.body = std::move(data);
        return resp;
    }

    auto data = db_.read_media_data(media_id);
    if (data.empty()) return error_json(500, "could not read media data");

    HttpResponse resp;
    resp.status = 200;
    resp.content_type = media->content_type;
    resp.headers["Accept-Ranges"] = "bytes";
    resp.body = std::move(data);
    return resp;
}

// ---- preview handler ----

HttpResponse App::handle_preview(const HttpRequest& req) {
    const std::string prefix = "/media/";
    const std::string suffix = "/preview";
    const auto start = prefix.size();
    const auto end = req.path.find(suffix);
    if (end == std::string::npos || end <= start)
        return error_json(400, "invalid preview path");
    const std::string media_id = req.path.substr(start, end - start);

    auto media = db_.get_media(media_id);
    if (!media) return error_json(404, "unknown media id");

    const int width      = parse_int(req.query, "w", 0);
    const int height     = parse_int(req.query, "h", 0);
    const int t_seconds  = parse_int(req.query, "t", media->media_kind == "video" ? 3 : 0);
    const std::string fmt = parse_string(req.query, "fmt", "webp");
    const int duration   = std::min(parse_int(req.query, "dur", (fmt == "mp4" || fmt == "av1" || fmt == "h265") ? 5 : 0), 30);

    if (width > 4096 || height > 4096)
        return error_json(400, "invalid dimensions");
    if (fmt != "webp" && fmt != "png" && fmt != "jpeg" && fmt != "jpg" && fmt != "avif" && fmt != "mp4" && fmt != "av1" && fmt != "h265")
        return error_json(400, "unsupported fmt");

    const std::string norm_fmt = normalize_fmt(fmt);
    PreviewKey cache_key{media_id, width, height, norm_fmt, t_seconds, duration};
    if (auto hit = previews_.cache_get(cache_key)) {
        // If-None-Match: return 304 if ETag matches
        auto inm = req.headers.find("if-none-match");
        if (inm != req.headers.end() && inm->second == hit->etag) {
            HttpResponse not_mod;
            not_mod.status = 304;
            not_mod.headers["Cache-Control"] = "public, max-age=31536000, immutable";
            not_mod.headers["ETag"] = hit->etag;
            return not_mod;
        }
        HttpResponse cached_resp;
        cached_resp.status = 200;
        cached_resp.content_type = hit->mime;
        cached_resp.headers["Cache-Control"] = "public, max-age=31536000, immutable";
        cached_resp.headers["ETag"] = hit->etag;
        cached_resp.body = hit->bytes;
        return cached_resp;
    }

    if (media->size_bytes > 512ULL * 1024 * 1024)
        return error_json(413, "media file too large for preview");

    std::string error;
    std::shared_ptr<const PreviewValue> preview;
    try {
        // For video clip formats, use streaming to avoid loading the full file
        if (norm_fmt == "mp4" || norm_fmt == "av1" || norm_fmt == "h265") {
            preview = previews_.get_or_create_streaming(*media, width, height, norm_fmt, t_seconds, duration, error);
        } else {
            auto file_data = db_.read_media_data(media_id);
            if (file_data.empty())
                return error_json(500, "could not read media data from store");
            preview = previews_.get_or_create(*media, file_data, width, height, norm_fmt, t_seconds, duration, error);
        }
    } catch (const std::exception& e) {
        return error_json(500, std::string("preview render exception: ") + e.what());
    } catch (...) {
        return error_json(500, "preview render crashed");
    }
    if (!preview) return error_json(422, error.empty() ? "preview failed" : error);

    // If-None-Match for freshly generated preview
    auto inm = req.headers.find("if-none-match");
    if (inm != req.headers.end() && inm->second == preview->etag) {
        HttpResponse not_mod;
        not_mod.status = 304;
        not_mod.headers["Cache-Control"] = "public, max-age=31536000, immutable";
        not_mod.headers["ETag"] = preview->etag;
        return not_mod;
    }
    HttpResponse resp;
    resp.status = 200;
    resp.content_type = preview->mime;
    resp.headers["Cache-Control"] = "public, max-age=31536000, immutable";
    resp.headers["ETag"] = preview->etag;
    resp.body = preview->bytes;
    return resp;
}

// ---- export handler ----

HttpResponse App::handle_export(const HttpRequest& req) {
    if (!auth_.is_authorized(req)) return error_json(401, "unauthorized");
    if (!auth_.check_gpo(req, GPO_EXPORT)) return error_json(403, "no permission: db.export");
    auto data = db_.export_db_to_buffer();
    if (data.empty()) return error_json(500, "export failed");
    HttpResponse resp;
    resp.status = 200;
    resp.content_type = "application/octet-stream";
    resp.headers["Content-Disposition"] = "attachment; filename=\"mediadb_export.mdb\"";
    resp.body = std::move(data);
    return resp;
}

// ---- import handler ----

HttpResponse App::handle_import(const HttpRequest& req) {
    if (!auth_.is_authorized(req)) return error_json(401, "unauthorized");
    if (!auth_.check_gpo(req, GPO_IMPORT)) return error_json(403, "no permission: db.import");
    if (req.body.empty()) return error_json(400, "empty body");
    if (import_running_.load()) return error_json(409, "import already in progress");

    // Copy body and run import in background thread
    auto data = std::make_shared<std::vector<uint8_t>>(
        reinterpret_cast<const uint8_t*>(req.body.data()),
        reinterpret_cast<const uint8_t*>(req.body.data()) + req.body.size());

    {
        std::lock_guard<std::mutex> lk(import_mutex_);
        import_running_.store(true);
        import_ok_.store(false);
        import_error_.clear();
    }

    if (import_thread_.joinable()) import_thread_.join();
    import_thread_ = std::thread([this, data]() {
        bool ok = db_.import_db_from_buffer(data->data(), data->size());
        std::lock_guard<std::mutex> lk(import_mutex_);
        import_ok_.store(ok);
        if (!ok) import_error_ = "import failed";
        import_running_.store(false);
    });

    json_object* j = json_object_new_object();
    json_object_object_add(j, "status", json_object_new_string("accepted"));
    json_object_object_add(j, "message", json_object_new_string("import started, poll /api/import/status"));
    return HttpResponse::json(202, j);
}

// ---- login handler ----

HttpResponse App::handle_login(const HttpRequest& req) {
    json_object* root = json_tokener_parse(req.body.c_str());
    if (!root) return error_json(400, "invalid json");
    json_object* juser = json_object_object_get(root, "username");
    json_object* jpass = json_object_object_get(root, "password");
    if (!json_object_is_type(juser, json_type_string) || !json_object_get_string(juser) ||
        !json_object_is_type(jpass, json_type_string) || !json_object_get_string(jpass)) {
        json_object_put(root);
        return error_json(400, "username and password required");
    }
    const std::string full_user(json_object_get_string(juser));
    const std::string pass(json_object_get_string(jpass));
    json_object_put(root);

    std::string user = full_user;
    std::string domain_label;
    auto at_pos = full_user.rfind('@');
    if (at_pos != std::string::npos && at_pos > 0 && at_pos < full_user.size() - 1) {
        user = full_user.substr(0, at_pos);
        domain_label = full_user.substr(at_pos + 1);
    }

    if (!domain_label.empty()) {
        auto domain = db_.find_domain_by_label(domain_label);
        if (!domain) return error_json(404, "unknown domain: " + domain_label);

        try {
            authdb::client::ClientConnection con;
            con.setUrl(domain->url);
            con.setClientName(domain->client_name);
            con.setClientSecret(domain->client_secret);
            authdb::client::Client client(con);
            if (!client.ClientAuth())
                return error_json(500, "client authentication failed for domain " + domain_label);
            con.setUsername(user);
            con.setUserPassword(pass);
            if (!client.UserAuth())
                return error_json(401, "invalid credentials");
            try { auth_.try_init_policies(client, domain_label); } catch (...) {}
            auto grps = resolve_session_groups(client);
            const std::string session_token = con.getSessionID().c_str();
            if (session_token.empty())
                return error_json(500, "login succeeded but no session token returned");
            store_session(session_token, full_user, domain_label, grps);
            json_object* j = json_object_new_object();
            json_object_object_add(j, "token", json_object_new_string(session_token.c_str()));
            json_object_object_add(j, "username", json_object_new_string(full_user.c_str()));
            json_object_object_add(j, "domain", json_object_new_string(domain_label.c_str()));
            return HttpResponse::json(200, j);
        } catch (const authdb::AuthBackendError& e) {
            return error_json(401, std::string("authentication failed: ") + e.what());
        } catch (const std::exception& e) {
            return error_json(500, std::string("auth service error: ") + e.what());
        } catch (...) {
            return error_json(500, "auth service error");
        }
    }

    try {
        authdb::client::ClientConnection con;
        con.setUrl(auth_.url());
        con.setClientName(auth_.client_name());
        con.setClientSecret(auth_.client_secret());

        authdb::client::Client client(con);

        if (!client.ClientAuth())
            return error_json(500, "client authentication failed");

        con.setUsername(user);
        con.setUserPassword(pass);

        if (!client.UserAuth())
            return error_json(401, "invalid credentials");

        try {
            auth_.try_init_policies(client);
        } catch (const std::exception& e) {
            std::cerr << "[mediadb] GPO init after login failed: " << e.what() << "\n";
        } catch (...) {
            std::cerr << "[mediadb] GPO init after login failed (unknown)" << "\n";
        }

        const std::string session_token = con.getSessionID().c_str();
        if (session_token.empty())
            return error_json(500, "login succeeded but no session token returned");

        auto grps = resolve_session_groups(client);
        store_session(session_token, user, {}, grps);

        json_object* j = json_object_new_object();
        json_object_object_add(j, "token", json_object_new_string(session_token.c_str()));
        json_object_object_add(j, "username", json_object_new_string(user.c_str()));
        return HttpResponse::json(200, j);
    } catch (const authdb::AuthBackendError& e) {
        std::cerr << "authdb login error: " << e.what() << "\n";
        return error_json(401, std::string("authentication failed: ") + e.what());
    } catch (const std::exception& e) {
        std::cerr << "login error: " << e.what() << "\n";
        return error_json(500, std::string("auth service error: ") + e.what());
    } catch (...) {
        return error_json(500, "auth service error");
    }
}

// ---- static file serving ----

HttpResponse App::handle_static(const HttpRequest& req) const {
    if (html_dir_.empty()) return HttpResponse::text(404, "not found\n");

    std::string file_path = req.path;
    if (file_path == "/" || file_path.empty()) file_path = "/login.html";

    if (file_path.find("..") != std::string::npos)
        return HttpResponse::text(403, "forbidden\n");

    const fs::path full = fs::path(html_dir_) / file_path.substr(1);
    if (!fs::exists(full) || !fs::is_regular_file(full))
        return HttpResponse::text(404, "not found\n");

    std::ifstream in(full, std::ios::binary);
    if (!in.is_open()) return HttpResponse::text(500, "read error\n");

    const auto sz = fs::file_size(full);
    std::vector<std::uint8_t> data(sz);
    in.read(reinterpret_cast<char*>(data.data()), static_cast<std::streamsize>(sz));

    HttpResponse resp;
    resp.status = 200;
    resp.content_type = mime_for_ext(full.extension().string());
    resp.body = std::move(data);
    return resp;
}

} // namespace mediadb
