/*******************************************************************************
 * Copyright (c) 2025, Jan Koester jan.koester@gmx.net
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * Redistributions of source code must retain the above copyright
 *      notice, this list of conditions and the following disclaimer.
 * Redistributions in binary form must reproduce the above copyright
 *      notice, this list of conditions and the following disclaimer in the
 *      documentation and/or other materials provided with the distribution.
 * Neither the name of the <organization> nor the
 *      names of its contributors may be used to endorse or promote products
 *      derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *******************************************************************************/

#include <algorithm>
#include <functional>
#include <iostream>
#include <fstream>
#include <cstring>

#include <json-c/json.h>
#include <json-c/json_object.h>

#include <httppp/http.h>
#include <httppp/exception.h>
#include <htmlpp/html.h>
#include <htmlpp/exception.h>
#include <uuidp.h>

#include "admin.h"
#include "authdb.h"
#include "user.h"
#include "group.h"
#include "domain.h"
#include "types.h"
#include "hash.h"
#include "hash/sha512.h"
#include "gpo.h"
#include "gpo_default.h"
#include "client.h"
#include "session.h"
#include "export.h"
#include "import.h"
#include "plugin.h"
#include "service.h"
#include "cluster.h"
#include "profile_webp.h"

namespace authdb {

    /* Hex-encode raw bytes to a lowercase hex string */
    static std::string hexEncode(const std::vector<uint8_t> &data) {
        static const char hex[] = "0123456789abcdef";
        std::string out;
        out.reserve(data.size() * 2);
        for (uint8_t b : data) {
            out.push_back(hex[b >> 4]);
            out.push_back(hex[b & 0x0F]);
        }
        return out;
    }

    static void sendJson(libhttppp::HttpRequest &curreq, json_object *jobj) {
        const char *str = json_object_to_json_string_ext(jobj, JSON_C_TO_STRING_PLAIN);
        size_t len = strlen(str);
        libhttppp::HttpResponse rep;
        rep.setState(HTTP200);
        rep.setContentType("application/json");
        rep.send(curreq, str, len);
    }

    static void sendJsonError(libhttppp::HttpRequest &curreq, const char *msg, int code = 500) {
        json_object *jobj = json_object_new_object();
        json_object_object_add(jobj, "error", json_object_new_string(msg));
        const char *str = json_object_to_json_string_ext(jobj, JSON_C_TO_STRING_PLAIN);
        size_t len = strlen(str);
        libhttppp::HttpResponse rep;
        if (code == 404)
            rep.setState(HTTP404);
        else
            rep.setState(HTTP500);
        rep.setContentType("application/json");
        rep.send(curreq, str, len);
        json_object_put(jobj);
    }

    static std::string getRequestBody(libhttppp::HttpRequest &curreq) {
        size_t bodyLen = curreq.getContentLength();
        if (bodyLen == 0)
            bodyLen = curreq.RecvData.size();
        if (bodyLen > curreq.RecvData.size())
            throw AuthBackendError("Incomplete request body!");
        return std::string(curreq.RecvData.begin(), curreq.RecvData.begin() + bodyLen);
    }

    static AuthBackend* resolveDomain(const char *cdid, AuthBackend &adminBackend, DomainBackend &dbackend) {
        if (strcmp(cdid, "admin") == 0) {
            return &adminBackend;
        }
        uuid::uuid did;
        if (did.parse(cdid) == 0) {
            return dbackend.data(did);
        }
        return nullptr;
    }

    class AdminController {
    public:
        AdminController(authdb::AuthBackend &backend, std::vector<netplus::socket*> ssock) : _AdminBackend(backend) {
            // Load admin.html
            std::ifstream adminfs(GETDATA(admin.html), std::ios::ate);
            if (adminfs.is_open()) {
                size_t sz = adminfs.tellg();
                _AdminHtml.resize(sz);
                adminfs.seekg(0);
                adminfs.read(_AdminHtml.data(), sz);
            }
            // Load login page via htmlpp for login form fallback
            _LoginElement = std::make_shared<libhtmlpp::HtmlElement>();
            _Loginpage = std::make_shared<libhtmlpp::HtmlPage>();
            try {
                _Loginpage->loadFile(*_LoginElement, GETDATA(login.html));
            } catch (libhtmlpp::HTMLException &e) {
                std::cerr << "Failed to load login.html: " << e.what() << std::endl;
            }
        };

        void setDomainResolver(std::function<AuthBackend*(const char*)> resolver) {
            _DomainResolver = std::move(resolver);
        }

        void setDomainCacheEvictor(std::function<void(const char*)> evictor) {
            _DomainCacheEvictor = std::move(evictor);
        }

        /* ---- JSON API: List Users ---- */
        void apiListUsers(libhttppp::HttpRequest &curreq, const char *cdid) {
            AuthBackend *backend = getDomain(cdid);
            if (!backend) throw AuthBackendError("listUsers: could not resolve domain!");

            json_object *jobj = json_object_new_object();
            json_object *jarr = json_object_new_array();

            User user;
            std::vector<uuid::uuid> uuids;
            user.list(*backend, uuids);

            for (const auto &uid : uuids) {
                class UserData udat(uid);
                try {
                    size_t upos = sizeof(authdb::AuthHeader);
                    user.info(*backend, udat, upos);

                    json_object *juser = json_object_new_object();
                    json_object_object_add(juser, "uid", json_object_new_string(uid.c_str()));
                    json_object_object_add(juser, "username", json_object_new_string(udat.getUsername().c_str()));
                    try { json_object_object_add(juser, "firstname", json_object_new_string(udat.getFirstname().c_str())); } catch (...) {}
                    try { json_object_object_add(juser, "lastname", json_object_new_string(udat.getLastname().c_str())); } catch (...) {}
                    try { json_object_object_add(juser, "mail", json_object_new_string(udat.getMail().c_str())); } catch (...) {}
                    json_object_array_add(jarr, juser);
                } catch (AuthBackendError &e) {
                    std::cerr << e.what() << std::endl;
                }
            }
            json_object_object_add(jobj, "users", jarr);
            sendJson(curreq, jobj);
            json_object_put(jobj);
        }

        /* ---- JSON API: Get User (for edit form) ---- */
        void apiGetUser(libhttppp::HttpRequest &curreq, const char *cdid, const char *cuid) {
            AuthBackend *backend = getDomain(cdid);
            if (!backend) throw AuthBackendError("getUser: could not resolve domain!");

            uuid::uuid uid;
            uid.parse(cuid);

            json_object *jobj = json_object_new_object();
            json_object *jfields = json_object_new_array();

            User user;
            class UserData udat(uid);
            size_t upos = sizeof(authdb::AuthHeader);
            user.info(*backend, udat, upos);

            auto addField = [&](const char *name, const std::string &val) {
                json_object *jfield = json_object_new_object();
                json_object_object_add(jfield, "name", json_object_new_string(name));
                json_object_object_add(jfield, "value", json_object_new_string(val.c_str()));
                json_object_array_add(jfields, jfield);
            };
            try { addField("username", udat.getUsername()); } catch (...) {}
            try { addField("firstname", udat.getFirstname()); } catch (...) {}
            try { addField("lastname", udat.getLastname()); } catch (...) {}
            try { addField("mail", udat.getMail()); } catch (...) {}

            json_object_object_add(jobj, "fields", jfields);
            sendJson(curreq, jobj);
            json_object_put(jobj);
        }

        /* ---- JSON API: Create User ---- */
        void apiCreateUser(libhttppp::HttpRequest &curreq, const char *cdid) {
            AuthBackend *backend = getDomain(cdid);
            if (!backend) throw AuthBackendError("createUser: could not resolve domain!");

            libhttppp::HttpForm curform;
            curform.parse(curreq);

            uuid::uuid uid;
            uid.generate();
            class UserData udat(uid);
            bool username = false;
            bool hasAvatar = false;
            std::string capturedRawPw;
            std::string capturedUsername;

            for (const auto &curformdat : curform.multipartData()) {
                if (curformdat.value.empty()) continue;

                std::string name;
                int stortype = EmptyStorage;

                for (const auto &curdispo : curformdat.dispositions) {
                    std::string tmp = curdispo.key;
                    std::transform(tmp.begin(), tmp.end(), tmp.begin(),
                                   [](unsigned char c) { return std::tolower(c); });
                    if (tmp == "name") name = curdispo.value;
                    if (tmp == "filename") stortype = BinaryStorage;
                }

                if (stortype == EmptyStorage) {
                    bool numeric = true;
                    for (auto i : curformdat.value) {
                        if (!std::isdigit(static_cast<unsigned char>(i))) { numeric = false; break; }
                    }
                    stortype = numeric ? IntStorage : TextStorage;
                }

                if (name.empty() || stortype == EmptyStorage) continue;

                struct AuthData::Record cudat;
                cudat.start = 0xFE;
                std::copy(&uid.value[0], &uid.value[16], &cudat.ruid[0]);
                cudat.type = UserData;
                cudat.storage = stortype;
                strcpy(cudat.fieldname, name.c_str());

                if (cudat.storage == BinaryStorage) {
                    if (strcmp(cudat.fieldname, "avatar") == 0) hasAvatar = true;
                    cudat.datasize = curformdat.value.size();
                    cudat.data = new char[cudat.datasize];
                    memcpy(cudat.data, curformdat.value.data(), cudat.datasize);
                    udat.append(cudat);
                    delete[] cudat.data;
                } else if (cudat.storage == TextStorage) {
                    if (strcmp(cudat.fieldname, "username") == 0) {
                        username = true;
                        capturedUsername = std::string(curformdat.value.begin(), curformdat.value.end());
                    }
                    if (strcmp(cudat.fieldname, "pwhash") == 0) {
                        Hash hash;
                        std::string pwhash, inpw;
                        std::copy(curformdat.value.begin(), curformdat.value.end(), std::back_inserter(inpw));
                        capturedRawPw = inpw;
                        hash.hash(inpw, pwhash);
                        cudat.datasize = pwhash.length() + 1;
                        cudat.data = new char[cudat.datasize];
                        memcpy(cudat.data, pwhash.c_str(), pwhash.length());
                        cudat.data[pwhash.length()] = '\0';
                        udat.append(cudat);
                        delete[] cudat.data;
                    } else {
                        std::vector<char> value = curformdat.value;
                        value.push_back('\0');
                        cudat.datasize = value.size();
                        cudat.data = new char[cudat.datasize];
                        memcpy(cudat.data, value.data(), value.size());
                        udat.append(cudat);
                        delete[] cudat.data;
                    }
                } else if (cudat.storage == IntStorage) {
                    std::vector<char> value = curformdat.value;
                    value.push_back('\0');
                    int val = atoi(value.data());
                    cudat.datasize = sizeof(int);
                    cudat.data = (char*)new int;
                    memcpy(cudat.data, &val, sizeof(int));
                    udat.append(cudat);
                    delete[] cudat.data;
                }
            }

            if (!username) throw AuthBackendError("username not set!");

            if (!hasAvatar) {
                struct AuthData::Record avat;
                avat.start = 0xFE;
                std::copy(&uid.value[0], &uid.value[16], &avat.ruid[0]);
                avat.type = UserData;
                avat.storage = BinaryStorage;
                strcpy(avat.fieldname, "avatar");
                avat.datasize = sizeof(profile_webp);
                avat.data = new char[avat.datasize];
                memcpy(avat.data, profile_webp, sizeof(profile_webp));
                udat.append(avat);
                delete[] avat.data;
            }

            /*
             * Compute Kerberos long-term key (RFC 3962 string-to-key) from
             * the raw password so that MIT / Heimdal kinit can authenticate.
             * Salt = REALM + username, where REALM = upper-case domain name.
             */
            if (!capturedRawPw.empty() && !capturedUsername.empty()
                && strcmp(cdid, "admin") != 0 && g_KdcPlugin) {
                try {
                    uuid::uuid did;
                    did.parse(cdid);
                    class DomainData ddat(did);
                    Domain dom;
                    size_t dpos = sizeof(AuthHeader);
                    dom.info(_AdminBackend, ddat, dpos);
                    std::string domainName = ddat.getDomainName();
                    std::string realm = domainName;
                    std::transform(realm.begin(), realm.end(), realm.begin(),
                                   [](unsigned char c){ return std::toupper(c); });
                    std::string salt = realm + capturedUsername;
                    auto krbKeyBytes = g_KdcPlugin->string2key(
                        capturedRawPw, salt, KDC_ETYPE_AES256_CTS_HMAC_SHA1);
                    udat.setKrbKey(hexEncode(krbKeyBytes));
                } catch (...) {
                    /* Non-fatal: user is created without krbkey */
                }
            }

            User user;
            user.create(*backend, &udat);

            json_object *jobj = json_object_new_object();
            json_object_object_add(jobj, "status", json_object_new_string("ok"));
            json_object_object_add(jobj, "uid", json_object_new_string(uid.c_str()));
            sendJson(curreq, jobj);
            json_object_put(jobj);
        }

        /* ---- JSON API: Remove User ---- */
        void apiRemoveUser(libhttppp::HttpRequest &curreq, const char *cdid, const char *cuid) {
            AuthBackend *backend = getDomain(cdid);
            if (!backend) throw AuthBackendError("removeUser: could not resolve domain!");

            User user;
            uuid::uuid uuid;
            uuid = cuid;
            user.remove(*backend, uuid);

            json_object *jobj = json_object_new_object();
            json_object_object_add(jobj, "status", json_object_new_string("ok"));
            sendJson(curreq, jobj);
            json_object_put(jobj);
        }

        /* ---- JSON API: Edit User ---- */
        void apiEditUser(libhttppp::HttpRequest &curreq, const char *cdid, const char *cuid) {
            AuthBackend *backend = getDomain(cdid);
            if (!backend) throw AuthBackendError("editUser: could not resolve domain!");

            uuid::uuid uid;
            uid.parse(cuid);

            std::shared_ptr<authdb::AuthData> editrec = std::make_shared<authdb::AuthData>(uid);
            bool data = false;
            std::string editRawPw;

            libhttppp::HttpForm curform;
            curform.parse(curreq);

            std::shared_ptr<authdb::AuthData::Record> curec = std::make_shared<authdb::AuthData::Record>();
            std::copy(&uid.value[0], &uid.value[sizeof(uuid::uuid_t)], &curec->ruid[0]);

            for (const auto &curformdat : curform.multipartData()) {
                curec->type = UserData;
                curec->storage = EmptyStorage;
                memset(curec->fieldname, 0, sizeof(curec->fieldname));

                for (const auto &curdispo : curformdat.dispositions) {
                    if (curformdat.value.empty() || curdispo.value.empty()) continue;
                    data = true;

                    std::string key = curdispo.key;
                    std::transform(key.begin(), key.end(), key.begin(),
                                   [](unsigned char c) { return std::tolower(c); });

                    if (key == "name") {
                        std::copy(curdispo.value.begin(), curdispo.value.end(), curec->fieldname);
                    }
                    if (key == "filename") curec->storage = BinaryStorage;
                }

                if (curec->storage == EmptyStorage) {
                    bool numeric = true;
                    for (auto t : curformdat.value) {
                        if (!std::isdigit(static_cast<unsigned char>(t))) { numeric = false; break; }
                    }
                    curec->storage = numeric ? IntStorage : TextStorage;
                }

                if (curec->storage == TextStorage) {
                    std::vector<char> value = curformdat.value;
                    value.push_back('\0');
                    if (strcmp(curec->fieldname, "pwhash") == 0) {
                        sha512 hash;
                        std::string pwhash, inpw;
                        inpw.assign(curformdat.value.begin(), curformdat.value.end());
                        editRawPw = inpw;
                        hash.hash(inpw, pwhash);
                        curec->datasize = pwhash.length() + 1;
                        curec->data = new char[curec->datasize];
                        std::copy(pwhash.begin(), pwhash.end(), curec->data);
                        curec->data[pwhash.length()] = '\0';
                        editrec->append(*curec);
                        delete[] curec->data;
                    } else {
                        curec->datasize = value.size();
                        curec->data = new char[curec->datasize];
                        std::copy(value.begin(), value.end(), curec->data);
                        editrec->append(*curec);
                        delete[] curec->data;
                    }
                } else if (curec->storage == BinaryStorage) {
                    curec->datasize = curformdat.value.size();
                    curec->data = new char[curec->datasize];
                    std::copy(curformdat.value.begin(), curformdat.value.end(), curec->data);
                    editrec->append(*curec);
                    delete[] curec->data;
                } else if (curec->storage == IntStorage) {
                    std::vector<char> value = curformdat.value;
                    value.push_back('\0');
                    curec->datasize = sizeof(int);
                    curec->data = new char[sizeof(int)];
                    int val = atoi(value.data());
                    std::memcpy(curec->data, &val, sizeof(int));
                    editrec->append(*curec);
                    delete[] curec->data;
                }
            }

            if (data) {
                backend->editRecord(*editrec, UserData);
            }

            /*
             * If the password was changed, recompute the Kerberos key.
             * Look up the username from the existing user record.
             */
            if (!editRawPw.empty() && strcmp(cdid, "admin") != 0) {
                try {
                    User user;
                    class UserData udat(uid);
                    size_t upos = sizeof(AuthHeader);
                    user.info(*backend, udat, upos);

                    std::string uname = udat.getUsername();
                    if (!uname.empty()) {
                        uuid::uuid did;
                        did.parse(cdid);
                        class DomainData ddat(did);
                        Domain dom;
                        size_t dpos = sizeof(AuthHeader);
                        dom.info(_AdminBackend, ddat, dpos);
                        std::string domainName = ddat.getDomainName();
                        std::string realm = domainName;
                        std::transform(realm.begin(), realm.end(), realm.begin(),
                                       [](unsigned char c){ return std::toupper(c); });
                        std::string salt = realm + uname;
                        auto krbKeyBytes = g_KdcPlugin->string2key(
                            editRawPw, salt, KDC_ETYPE_AES256_CTS_HMAC_SHA1);
                        std::string hexKey = hexEncode(krbKeyBytes);
                        udat.setKrbKey(hexKey);
                        upos = sizeof(AuthHeader);
                        user.edit(*backend, udat, upos);
                    }
                } catch (...) {
                    /* Non-fatal */
                }
            }

            json_object *jobj = json_object_new_object();
            json_object_object_add(jobj, "status", json_object_new_string("ok"));
            sendJson(curreq, jobj);
            json_object_put(jobj);
        }

        /* ---- Helper: Check if session user is SuperAdmin from admin.local ---- */
        bool isSuperAdmin(const SessionData *sdat) {
            if (!sdat) return false;

            // Get the user ID from the session
            uuid::uuid uid;
            sdat->getUid(uid);

            // Find "SuperAdmin" group in admin backend and check membership
            Group group;
            std::vector<uuid::uuid> gids;
            group.list(_AdminBackend, gids);

            for (const auto &gid : gids) {
                class GroupData gdat(gid);
                try {
                    size_t gpos = sizeof(authdb::AuthHeader);
                    group.info(_AdminBackend, gdat, gpos);
                    if (gdat.getName() == "SuperAdmin") {
                        return gdat.isMember(uid);
                    }
                } catch (AuthBackendError &e) {
                    std::cerr << e.what() << std::endl;
                }
            }
            return false;
        }

        /* ---- Helper: Check GPO permission ---- */
        void checkGPO(const SessionData *sdat, const char *gpoid) {
            if (!sdat) throw AuthBackendError("No session!");
            if (isSuperAdmin(sdat)) return;
            if (!sdat->checkGPO(uuid::uuid(gpoid)))
                throw AuthBackendError("Insufficient permissions!");
        }

        /* ---- Helper: Check domain access (non-SuperAdmins can only access their own domain) ---- */
        void checkDomainAccess(const SessionData *sdat, const char *cdid) {
            if (!sdat) throw AuthBackendError("No session!");

            // Determine session domain string
            uuid::uuid sessionDid;
            sdat->getDid(sessionDid);
            std::string sessionDomain;
            if (sessionDid.empty()) {
                sessionDomain = "admin";
            } else {
                sessionDomain = sessionDid.c_str();
            }

            // If requested domain matches session domain, allow
            if (sessionDomain == cdid) return;

            // Otherwise, must be SuperAdmin from admin.local
            if (!isSuperAdmin(sdat))
                throw AuthBackendError("Zugriff verweigert: nur SuperAdmins von admin.local duerfen andere Domains einsehen!");
        }

        /* ---- JSON API: Check Permissions ---- */
        void apiCheckPermissions(libhttppp::HttpRequest &curreq, const SessionData *sdat) {
            json_object *jobj = json_object_new_object();
            bool superadmin = isSuperAdmin(sdat);
            json_object_object_add(jobj, "superadmin", json_object_new_boolean(superadmin));

            // Return session domain so frontend knows which domain the user belongs to
            uuid::uuid sessionDid;
            sdat->getDid(sessionDid);
            if (sessionDid.empty()) {
                json_object_object_add(jobj, "sessiondomain", json_object_new_string("admin"));
            } else {
                json_object_object_add(jobj, "sessiondomain", json_object_new_string(sessionDid.c_str()));
            }

            // GPO permissions (SuperAdmins get all rights)
            json_object *jgpo = json_object_new_object();
            for (size_t i = 0; gpo_default[i][0] != nullptr; ++i) {
                bool granted = superadmin || sdat->checkGPO(uuid::uuid(gpo_default[i][0]));
                json_object_object_add(jgpo, gpo_default[i][0], json_object_new_boolean(granted));
            }
            json_object_object_add(jobj, "gpo", jgpo);

            // Expose backend type so frontend can restrict domain storage options
            // 0 = File, 2 = ClusterStore
            json_object_object_add(jobj, "backendtype", json_object_new_int(_AdminBackend.getType()));

            // Cluster info: indicate whether cluster mode is active
            bool clusterActive = (g_Cluster && g_Cluster->isRunning());
            json_object_object_add(jobj, "cluster", json_object_new_boolean(clusterActive));

            sendJson(curreq, jobj);
            json_object_put(jobj);
        }

        /* ---- JSON API: Cluster Status ---- */
        void apiClusterStatus(libhttppp::HttpRequest &curreq) {
            json_object *jobj = json_object_new_object();

            if (!g_Cluster || !g_Cluster->isRunning()) {
                json_object_object_add(jobj, "enabled", json_object_new_boolean(false));
                sendJson(curreq, jobj);
                json_object_put(jobj);
                return;
            }

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

            auto &pclient = g_Cluster->getClient();
            if (pclient) {
                decltype(pclient->get_cluster_status()) health;
                try {
                    health = pclient->get_cluster_status();
                } catch (...) {
                    json_object_object_add(jobj, "status", json_object_new_string("UNAVAILABLE"));
                    json_object_object_add(jobj, "nodes_total", json_object_new_int(0));
                    json_object_object_add(jobj, "nodes_online", json_object_new_int(0));
                    sendJson(curreq, jobj);
                    json_object_put(jobj);
                    return;
                }

                uint32_t total = health.nodes_total;
                uint32_t online = health.nodes_online;

                const char *statusStr = "OK";
                if (health.overall == paritypp::node_status::DEGRADED)
                    statusStr = "DEGRADED";
                else if (health.overall == paritypp::node_status::REBUILD)
                    statusStr = "REBUILD";

                json_object_object_add(jobj, "status", json_object_new_string(statusStr));
                json_object_object_add(jobj, "nodes_total", json_object_new_int(total));
                json_object_object_add(jobj, "nodes_online", json_object_new_int(online));

                json_object *jnodes = json_object_new_array();

                // Remote peer nodes
                for (const auto &[idx, info] : health.node_statuses) {
                    json_object *jn = json_object_new_object();
                    json_object_object_add(jn, "index", json_object_new_int(static_cast<int>(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_array_add(jnodes, jn);
                }
                json_object_object_add(jobj, "nodes", jnodes);
            }

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

            sendJson(curreq, jobj);
            json_object_put(jobj);
        }

        /* ---- JSON API: Cluster Sync ---- */
        void apiClusterSync(libhttppp::HttpRequest &curreq) {
            json_object *jobj = json_object_new_object();

            if (!g_Cluster || !g_Cluster->isRunning()) {
                json_object_object_add(jobj, "status", json_object_new_string("error"));
                json_object_object_add(jobj, "message", json_object_new_string("cluster not running"));
                sendJson(curreq, jobj);
                json_object_put(jobj);
                return;
            }

            g_Cluster->forceSync();

            json_object_object_add(jobj, "status", json_object_new_string("ok"));
            json_object_object_add(jobj, "epoch", json_object_new_int64(
                static_cast<int64_t>(g_Cluster->getRecoveryEpoch())));
            sendJson(curreq, jobj);
            json_object_put(jobj);
        }

        /* ---- JSON API: Cluster Scrub ---- */
        void apiClusterScrub(libhttppp::HttpRequest &curreq) {
            json_object *jobj = json_object_new_object();

            if (!g_Cluster || !g_Cluster->isRunning()) {
                json_object_object_add(jobj, "status", json_object_new_string("error"));
                json_object_object_add(jobj, "message", json_object_new_string("cluster not running"));
                sendJson(curreq, jobj);
                json_object_put(jobj);
                return;
            }

            if (g_Cluster->isCritical()) {
                json_object_object_add(jobj, "status", json_object_new_string("error"));
                json_object_object_add(jobj, "message", json_object_new_string("cluster is critical — need more nodes online"));
                sendJson(curreq, jobj);
                json_object_put(jobj);
                return;
            }

            auto result = g_Cluster->scrub();

            json_object_object_add(jobj, "status", json_object_new_string("ok"));
            json_object_object_add(jobj, "groups_checked", json_object_new_int(static_cast<int>(result.groups_checked)));
            json_object_object_add(jobj, "groups_repaired", json_object_new_int(static_cast<int>(result.groups_repaired)));
            json_object_object_add(jobj, "groups_failed", json_object_new_int(static_cast<int>(result.groups_failed)));
            sendJson(curreq, jobj);
            json_object_put(jobj);
        }

        /* ---- JSON API: Cluster Rebalance ---- */
        void apiClusterRebalance(libhttppp::HttpRequest &curreq) {
            json_object *jobj = json_object_new_object();

            if (!g_Cluster || !g_Cluster->isRunning()) {
                json_object_object_add(jobj, "status", json_object_new_string("error"));
                json_object_object_add(jobj, "message", json_object_new_string("cluster not running"));
                sendJson(curreq, jobj);
                json_object_put(jobj);
                return;
            }

            if (g_Cluster->isCritical()) {
                json_object_object_add(jobj, "status", json_object_new_string("error"));
                json_object_object_add(jobj, "message", json_object_new_string("cluster is critical — need more nodes online"));
                sendJson(curreq, jobj);
                json_object_put(jobj);
                return;
            }

            auto &client = g_Cluster->getClient();
            if (!client) {
                json_object_object_add(jobj, "status", json_object_new_string("error"));
                json_object_object_add(jobj, "message", json_object_new_string("no parity client"));
                sendJson(curreq, jobj);
                json_object_put(jobj);
                return;
            }

            auto scrub_lk = g_Cluster->tryScrubLock();
            if (!scrub_lk.owns_lock()) {
                json_object_object_add(jobj, "status", json_object_new_string("error"));
                json_object_object_add(jobj, "message", json_object_new_string("scrub or rebalance already running"));
                sendJson(curreq, jobj);
                json_object_put(jobj);
                return;
            }

            auto rb = client->rebalance();

            json_object_object_add(jobj, "status", json_object_new_string("ok"));
            json_object_object_add(jobj, "rebalanced", json_object_new_int(static_cast<int>(rb.rebalanced)));
            json_object_object_add(jobj, "already_ok", json_object_new_int(static_cast<int>(rb.already_ok)));
            json_object_object_add(jobj, "failed", json_object_new_int(static_cast<int>(rb.failed)));
            sendJson(curreq, jobj);
            json_object_put(jobj);
        }

        /* ---- JSON API: List Domains ---- */
        void apiListDomains(libhttppp::HttpRequest &curreq) {
            json_object *jobj = json_object_new_object();
            json_object *jarr = json_object_new_array();

            Domain domain;
            std::vector<uuid::uuid> dids;
            domain.list(_AdminBackend, dids);

            for (const auto &did : dids) {
                class DomainData ddat(did);
                try {
                    size_t dpos = sizeof(authdb::AuthHeader);
                    domain.info(_AdminBackend, ddat, dpos);

                    json_object *jdom = json_object_new_object();
                    json_object_object_add(jdom, "id", json_object_new_string(did.c_str()));
                    json_object_object_add(jdom, "name", json_object_new_string(ddat.getDomainName().c_str()));
                    json_object_object_add(jdom, "storagetype", json_object_new_int(ddat.getStorageType()));
                    json_object_object_add(jdom, "storageoptions", json_object_new_string(ddat.getStorageOptions().c_str()));
                    json_object_array_add(jarr, jdom);
                } catch (AuthBackendError &e) {
                    std::cerr << e.what() << std::endl;
                }
            }
            json_object_object_add(jobj, "domains", jarr);
            sendJson(curreq, jobj);
            json_object_put(jobj);
        }

        /* ---- JSON API: Create Domain ---- */
        void apiCreateDomain(libhttppp::HttpRequest &curreq, const SessionData *sdat) {
            if (!isSuperAdmin(sdat))
                throw AuthBackendError("createDomain: nur SuperAdmins von admin.local erlaubt!");
            std::vector<char> body;
            libhttppp::HttpForm curform;
            curform.parse(curreq);

            // Read JSON body
            std::string reqbody = getRequestBody(curreq);
            json_object *jreq = json_tokener_parse(reqbody.c_str());

            if (!jreq) throw AuthBackendError("createDomain: invalid JSON!");

            json_object *jname = json_object_object_get(jreq, "domainname");
            json_object *jstorage = json_object_object_get(jreq, "domainstorage");
            json_object *joptions = json_object_object_get(jreq, "domainoptions");

            if (!jname || !jstorage || !joptions) {
                json_object_put(jreq);
                throw AuthBackendError("createDomain: missing fields!");
            }

            uuid::uuid uid;
            json_object *jid = json_object_object_get(jreq, "domainid");
            if (jid && strlen(json_object_get_string(jid)) > 0) {
                uid = uuid::uuid(json_object_get_string(jid));
            } else {
                uid.generate();
            }
            class DomainData ddat(uid);

            // domainname
            {
                AuthData::Record dodat{};
                dodat.start = 0xFE;
                std::copy(&uid.value[0], &uid.value[16], &dodat.ruid[0]);
                dodat.type = DomainData;
                dodat.storage = TextStorage;
                strcpy(dodat.fieldname, "domainname");
                const char *val = json_object_get_string(jname);
                size_t vlen = strlen(val) + 1;
                dodat.datasize = vlen;
                dodat.data = new char[vlen];
                memcpy(dodat.data, val, vlen);
                ddat.append(dodat);
                delete[] dodat.data;
            }

            // domainstorage
            {
                AuthData::Record dodat{};
                dodat.start = 0xFE;
                std::copy(&uid.value[0], &uid.value[16], &dodat.ruid[0]);
                dodat.type = DomainData;
                dodat.storage = IntStorage;
                strcpy(dodat.fieldname, "domainstorage");
                int val = json_object_get_int(jstorage);
                dodat.datasize = sizeof(int);
                dodat.data = new char[sizeof(int)];
                memcpy(dodat.data, &val, sizeof(int));
                ddat.append(dodat);
                delete[] dodat.data;
            }

            // domainoptions
            {
                AuthData::Record dodat{};
                dodat.start = 0xFE;
                std::copy(&uid.value[0], &uid.value[16], &dodat.ruid[0]);
                dodat.type = DomainData;
                dodat.storage = TextStorage;
                strcpy(dodat.fieldname, "domainoptions");
                const char *val = json_object_get_string(joptions);
                size_t vlen = strlen(val) + 1;
                dodat.datasize = vlen;
                dodat.data = new char[vlen];
                memcpy(dodat.data, val, vlen);
                ddat.append(dodat);
                delete[] dodat.data;
            }

            Domain domain;
            domain.create(_AdminBackend, &ddat);

            // Auto-create "Domain Admins" group and admin login GPO
            {
                AuthBackend *domBackend = getDomain(uid.c_str());
                if (domBackend) {
                    AuthBackend::Guard dguard(*domBackend);

                    // Create "Domain Admins" group
                    uuid::uuid gid;
                    gid.generate();
                    class GroupData grpdat(gid);
                    grpdat.setName("Domain Admins");
                    Group grp;
                    grp.create(*domBackend, &grpdat);

                    // Create all default GPOs
                    Gpo gpo;
                    std::vector<uuid::uuid> adminAllowed;
                    adminAllowed.push_back(gid);

                    for (size_t i = 0; gpo_default[i][0] != nullptr; ++i) {
                        uuid::uuid gpoid(gpo_default[i][0]);
                        class GpoData gdat(gpoid);
                        gdat.setName(gpo_default[i][1]);
                        gdat.setDesc(gpo_default[i][2]);
                        bool defval = (strcmp(gpo_default[i][3], "true") == 0);
                        gdat.setGpoValue(defval);

                        // GPOs with default=false: grant to Domain Admins
                        if (!defval) {
                            gdat.setAllowed(adminAllowed);
                        }

                        gpo.create(*domBackend, &gdat);
                    }
                }
            }

            json_object_put(jreq);

            json_object *jobj = json_object_new_object();
            json_object_object_add(jobj, "status", json_object_new_string("ok"));
            json_object_object_add(jobj, "id", json_object_new_string(uid.c_str()));
            sendJson(curreq, jobj);
            json_object_put(jobj);
        }

        /* ---- JSON API: Remove Domain ---- */
        void apiRemoveDomain(libhttppp::HttpRequest &curreq, const char *cdid, const SessionData *sdat) {
            if (!isSuperAdmin(sdat))
                throw AuthBackendError("removeDomain: nur SuperAdmins von admin.local erlaubt!");

            // Purge domain backend cluster data before removing the domain record
            uuid::uuid uuid;
            uuid.parse(cdid);
            {
                AuthBackend *domBackend = getDomain(cdid);
                if (domBackend) {
                    AuthBackend::Guard domGuard(*domBackend);
                    domBackend->purge();
                }
            }

            Domain domain;
            domain.remove(_AdminBackend, uuid);

            // Evict from domain cache so stale backend is not reused
            if (_DomainCacheEvictor)
                _DomainCacheEvictor(cdid);

            json_object *jobj = json_object_new_object();
            json_object_object_add(jobj, "status", json_object_new_string("ok"));
            sendJson(curreq, jobj);
            json_object_put(jobj);
        }

        /* ---- JSON API: List Groups ---- */
        void apiListGroups(libhttppp::HttpRequest &curreq, const char *cdid) {
            AuthBackend *backend = getDomain(cdid);
            if (!backend) throw AuthBackendError("listGroups: could not resolve domain!");

            json_object *jobj = json_object_new_object();
            json_object *jarr = json_object_new_array();

            Group group;
            std::vector<uuid::uuid> gids;
            group.list(*backend, gids);

            for (const auto &gid : gids) {
                class GroupData gdat(gid);
                try {
                    size_t upos = sizeof(authdb::AuthHeader);
                    group.info(*backend, gdat, upos);

                    json_object *jgrp = json_object_new_object();
                    json_object_object_add(jgrp, "gid", json_object_new_string(gid.c_str()));
                    json_object_object_add(jgrp, "name", json_object_new_string(gdat.getName().c_str()));
                    json_object_array_add(jarr, jgrp);
                } catch (AuthBackendError &e) {
                    std::cerr << e.what() << std::endl;
                }
            }
            json_object_object_add(jobj, "groups", jarr);
            sendJson(curreq, jobj);
            json_object_put(jobj);
        }

        /* ---- JSON API: Get Group (for edit) ---- */
        void apiGetGroup(libhttppp::HttpRequest &curreq, const char *cdid, const char *cgid) {
            AuthBackend *backend = getDomain(cdid);
            if (!backend) throw AuthBackendError("getGroup: could not resolve domain!");

            uuid::uuid gid(cgid);
            json_object *jobj = json_object_new_object();

            // Group info
            class GroupData gdat(gid);
            Group grp;
            size_t pos = sizeof(AuthHeader);
            grp.info(*backend, gdat, pos);

            json_object_object_add(jobj, "groupname", json_object_new_string(gdat.getName().c_str()));

            // Members
            std::vector<uuid::uuid> mrecs;
            gdat.getMembers(mrecs);
            json_object *jmembers = json_object_new_array();
            for (const auto &m : mrecs) {
                json_object_array_add(jmembers, json_object_new_string(m.c_str()));
            }
            json_object_object_add(jobj, "members", jmembers);

            // All users (for selection)
            json_object *jallusers = json_object_new_array();
            User usr;
            std::vector<uuid::uuid> uids;
            usr.list(*backend, uids);
            for (const auto &uid : uids) {
                class UserData udat(uid);
                try {
                    size_t upos = sizeof(authdb::AuthHeader);
                    usr.info(*backend, udat, upos);
                    json_object *ju = json_object_new_object();
                    json_object_object_add(ju, "uid", json_object_new_string(uid.c_str()));
                    json_object_object_add(ju, "username", json_object_new_string(udat.getUsername().c_str()));
                    json_object_array_add(jallusers, ju);
                } catch (...) {}
            }
            json_object_object_add(jobj, "allusers", jallusers);

            sendJson(curreq, jobj);
            json_object_put(jobj);
        }

        /* ---- JSON API: Create Group ---- */
        void apiCreateGroup(libhttppp::HttpRequest &curreq, const char *cdid) {
            AuthBackend *backend = getDomain(cdid);
            if (!backend) throw AuthBackendError("createGroup: could not resolve domain!");

            std::string reqbody = getRequestBody(curreq);
            json_object *jreq = json_tokener_parse(reqbody.c_str());
            if (!jreq) throw AuthBackendError("createGroup: invalid JSON!");

            json_object *jname = json_object_object_get(jreq, "groupname");
            json_object *jmembers = json_object_object_get(jreq, "groupmembers");

            uuid::uuid gid;
            gid.generate();
            class GroupData grpdat(gid);

            if (jname) grpdat.setName(json_object_get_string(jname));

            if (jmembers && json_object_get_type(jmembers) == json_type_array) {
                int mlen = json_object_array_length(jmembers);
                for (int i = 0; i < mlen; i++) {
                    grpdat.addMember(uuid::uuid(json_object_get_string(json_object_array_get_idx(jmembers, i))));
                }
            }

            Group grp;
            grp.create(*backend, &grpdat);

            json_object_put(jreq);

            json_object *jobj = json_object_new_object();
            json_object_object_add(jobj, "status", json_object_new_string("ok"));
            json_object_object_add(jobj, "gid", json_object_new_string(gid.c_str()));
            sendJson(curreq, jobj);
            json_object_put(jobj);
        }

        /* ---- JSON API: Remove Group ---- */
        void apiRemoveGroup(libhttppp::HttpRequest &curreq, const char *cdid, const char *cgid) {
            AuthBackend *backend = getDomain(cdid);
            if (!backend) throw AuthBackendError("removeGroup: could not resolve domain!");

            Group group;
            uuid::uuid uuid;
            uuid = cgid;
            group.remove(*backend, uuid);

            json_object *jobj = json_object_new_object();
            json_object_object_add(jobj, "status", json_object_new_string("ok"));
            sendJson(curreq, jobj);
            json_object_put(jobj);
        }

        /* ---- JSON API: Edit Group ---- */
        void apiEditGroup(libhttppp::HttpRequest &curreq, const char *cdid, const char *cgid) {
            AuthBackend *backend = getDomain(cdid);
            if (!backend) throw AuthBackendError("editGroup: could not resolve domain!");

            uuid::uuid gid(cgid);

            std::string reqbody = getRequestBody(curreq);
            json_object *jreq = json_tokener_parse(reqbody.c_str());
            if (!jreq) throw AuthBackendError("editGroup: invalid JSON!");

            json_object *jname = json_object_object_get(jreq, "groupname");
            json_object *jmembers = json_object_object_get(jreq, "groupmembers");

            class GroupData editrec(gid);

            if (jname) editrec.setName(json_object_get_string(jname));

            if (jmembers && json_object_get_type(jmembers) == json_type_array) {
                int mlen = json_object_array_length(jmembers);
                for (int i = 0; i < mlen; i++) {
                    editrec.addMember(uuid::uuid(json_object_get_string(json_object_array_get_idx(jmembers, i))));
                }
            }

            Group grp;
            size_t pos = sizeof(AuthHeader);
            grp.edit(*backend, editrec, pos);

            json_object_put(jreq);

            json_object *jobj = json_object_new_object();
            json_object_object_add(jobj, "status", json_object_new_string("ok"));
            sendJson(curreq, jobj);
            json_object_put(jobj);
        }

        /* ---- JSON API: List Sessions ---- */
        void apiListSessions(libhttppp::HttpRequest &curreq, const char *cdid) {
            uuid::uuid did;
            if (strcmp(cdid, "admin") != 0) {
                if (did.parse(cdid) != 0)
                    throw AuthBackendError("listSessions: wrong domain id!");
            }

            json_object *jobj = json_object_new_object();
            json_object *jarr = json_object_new_array();

            std::vector<SessionInfo> sessions;
            AuthSession->listSessions(sessions);

            for (const auto &si : sessions) {
                if ((strcmp(cdid, "admin") == 0 && si.did.empty()) ||
                    (!did.empty() && did == si.did)) {
                    json_object *jsess = json_object_new_object();
                    json_object_object_add(jsess, "sid", json_object_new_string(si.sid.c_str()));
                    json_object_object_add(jsess, "did", json_object_new_string(si.did.c_str()));
                    json_object_object_add(jsess, "username", json_object_new_string(si.username.c_str()));
                    json_object_array_add(jarr, jsess);
                }
            }

            json_object_object_add(jobj, "sessions", jarr);
            sendJson(curreq, jobj);
            json_object_put(jobj);
        }

        /* ---- JSON API: Remove Session ---- */
        void apiRemoveSession(libhttppp::HttpRequest &curreq, const char *csid) {
            uuid::uuid sid;
            sid = csid;
            AuthSession->remSession(sid);

            json_object *jobj = json_object_new_object();
            json_object_object_add(jobj, "status", json_object_new_string("ok"));
            sendJson(curreq, jobj);
            json_object_put(jobj);
        }

        /* ---- JSON API: Reload Session ---- */
        void apiReloadSession(libhttppp::HttpRequest &curreq, const char *cdid, const char *csid) {
            uuid::uuid sid, did;
            sid.parse(csid);

            AuthBackend *backend = getDomain(cdid);
            if (!backend) throw AuthBackendError("reloadSession: could not resolve domain!");

            AuthSession->relSession(*backend, sid);

            json_object *jobj = json_object_new_object();
            json_object_object_add(jobj, "status", json_object_new_string("ok"));
            sendJson(curreq, jobj);
            json_object_put(jobj);
        }

        /* ---- JSON API: List GPOs ---- */
        void apiListGpos(libhttppp::HttpRequest &curreq, const char *cdid) {
            AuthBackend *backend = getDomain(cdid);
            if (!backend) throw AuthBackendError("listGpos: could not resolve domain!");

            json_object *jobj = json_object_new_object();
            json_object *jarr = json_object_new_array();

            Gpo gpo;
            std::vector<uuid::uuid> gpids;
            gpo.list(*backend, gpids);

            for (const auto &gpid : gpids) {
                class GpoData gdat(gpid);
                try {
                    size_t upos = sizeof(authdb::AuthHeader);
                    gpo.info(*backend, gdat, upos);

                    json_object *jgpo = json_object_new_object();
                    json_object_object_add(jgpo, "id", json_object_new_string(gpid.c_str()));
                    json_object_object_add(jgpo, "name", json_object_new_string(gdat.getName()));
                    try {
                        json_object_object_add(jgpo, "desc", json_object_new_string(gdat.getDesc()));
                    } catch (...) {
                        json_object_object_add(jgpo, "desc", json_object_new_string(""));
                    }
                    json_object_object_add(jgpo, "defaultvalue", json_object_new_boolean(gdat.getGpoValue()));
                    json_object_array_add(jarr, jgpo);
                } catch (AuthBackendError &e) {
                    std::cerr << e.what() << std::endl;
                }
            }
            json_object_object_add(jobj, "gpos", jarr);
            sendJson(curreq, jobj);
            json_object_put(jobj);
        }

        /* ---- JSON API: Get GPO (for edit) ---- */
        void apiGetGpo(libhttppp::HttpRequest &curreq, const char *cdid, const char *cgpid) {
            AuthBackend *backend = getDomain(cdid);
            if (!backend) throw AuthBackendError("getGpo: could not resolve domain!");

            uuid::uuid gpid;
            gpid.parse(cgpid);

            Gpo gpo;
            int ret;
            if (!gpo.exists(*backend, gpid, nullptr, ret))
                throw AuthBackendError("GPO doesn't exist!");

            class GpoData gpodat(gpid);
            size_t gppos = sizeof(authdb::AuthHeader);
            gpo.info(*backend, gpodat, gppos);

            json_object *jobj = json_object_new_object();
            json_object_object_add(jobj, "gpoid", json_object_new_string(cgpid));
            json_object_object_add(jobj, "gponame", json_object_new_string(gpodat.getName()));
            try {
                json_object_object_add(jobj, "gpodesc", json_object_new_string(gpodat.getDesc()));
            } catch (...) {
                json_object_object_add(jobj, "gpodesc", json_object_new_string(""));
            }
            json_object_object_add(jobj, "gpodefault", json_object_new_boolean(gpodat.getGpoValue()));

            // Allowed groups
            std::vector<uuid::uuid> allow;
            gpodat.getAlowed(allow);
            json_object *jallowed = json_object_new_array();
            for (const auto &a : allow) {
                json_object_array_add(jallowed, json_object_new_string(a.c_str()));
            }
            json_object_object_add(jobj, "allowed", jallowed);

            // All groups
            json_object *jallgroups = json_object_new_array();
            Group grplst;
            std::vector<uuid::uuid> grpids;
            grplst.list(*backend, grpids);
            for (const auto &gid : grpids) {
                class GroupData grpdat(gid);
                try {
                    size_t gpos = sizeof(authdb::AuthHeader);
                    grplst.info(*backend, grpdat, gpos);
                    json_object *jg = json_object_new_object();
                    json_object_object_add(jg, "gid", json_object_new_string(gid.c_str()));
                    json_object_object_add(jg, "name", json_object_new_string(grpdat.getName().c_str()));
                    json_object_array_add(jallgroups, jg);
                } catch (...) {}
            }
            json_object_object_add(jobj, "allgroups", jallgroups);

            sendJson(curreq, jobj);
            json_object_put(jobj);
        }

        /* ---- JSON API: Create GPO ---- */
        void apiCreateGpo(libhttppp::HttpRequest &curreq, const char *cdid) {
            AuthBackend *backend = getDomain(cdid);
            if (!backend) throw AuthBackendError("createGpo: could not resolve domain!");

            std::string reqbody = getRequestBody(curreq);
            json_object *jreq = json_tokener_parse(reqbody.c_str());
            if (!jreq) throw AuthBackendError("createGpo: invalid JSON!");

            json_object *jid = json_object_object_get(jreq, "gpoid");
            json_object *jname = json_object_object_get(jreq, "gponame");
            json_object *jdesc = json_object_object_get(jreq, "gpodesc");
            json_object *jdefault = json_object_object_get(jreq, "gpodefaultvalue");
            json_object *jgroups = json_object_object_get(jreq, "gpoingroup");

            uuid::uuid gpid;
            if (jid && strlen(json_object_get_string(jid)) > 0) {
                gpid = uuid::uuid(json_object_get_string(jid));
            } else {
                gpid.generate();
            }

            class GpoData gpodat(gpid);
            if (jname) gpodat.setName(json_object_get_string(jname));
            if (jdesc) gpodat.setDesc(json_object_get_string(jdesc));
            if (jdefault) gpodat.setGpoValue(json_object_get_boolean(jdefault));

            std::vector<uuid::uuid> allow;
            if (jgroups && json_object_get_type(jgroups) == json_type_array) {
                int glen = json_object_array_length(jgroups);
                for (int i = 0; i < glen; i++) {
                    allow.emplace_back(json_object_get_string(json_object_array_get_idx(jgroups, i)));
                }
            }
            gpodat.setAllowed(allow);

            Gpo gpo;
            gpo.create(*backend, &gpodat);

            json_object_put(jreq);

            json_object *jobj = json_object_new_object();
            json_object_object_add(jobj, "status", json_object_new_string("ok"));
            json_object_object_add(jobj, "id", json_object_new_string(gpid.c_str()));
            sendJson(curreq, jobj);
            json_object_put(jobj);
        }

        /* ---- JSON API: Remove GPO ---- */
        void apiRemoveGpo(libhttppp::HttpRequest &curreq, const char *cdid, const char *cgpid) {
            AuthBackend *backend = getDomain(cdid);
            if (!backend) throw AuthBackendError("removeGpo: could not resolve domain!");

            uuid::uuid gpid;
            gpid.parse(cgpid);

            Gpo gpo;
            gpo.remove(*backend, gpid);

            json_object *jobj = json_object_new_object();
            json_object_object_add(jobj, "status", json_object_new_string("ok"));
            sendJson(curreq, jobj);
            json_object_put(jobj);
        }

        /* ---- JSON API: Edit GPO ---- */
        void apiEditGpo(libhttppp::HttpRequest &curreq, const char *cdid, const char *cgpid) {
            AuthBackend *backend = getDomain(cdid);
            if (!backend) throw AuthBackendError("editGpo: could not resolve domain!");

            uuid::uuid gpid;
            gpid.parse(cgpid);

            Gpo gpo;
            int ret;
            if (!gpo.exists(*backend, gpid, nullptr, ret))
                throw AuthBackendError("GPO doesn't exist!");

            class GpoData gpodat(gpid);
            size_t gppos = sizeof(authdb::AuthHeader);
            gpo.info(*backend, gpodat, gppos);

            std::string reqbody = getRequestBody(curreq);
            json_object *jreq = json_tokener_parse(reqbody.c_str());
            if (!jreq) throw AuthBackendError("editGpo: invalid JSON!");

            json_object *jdesc = json_object_object_get(jreq, "gpodesc");
            json_object *jdefault = json_object_object_get(jreq, "gpodefaultvalue");
            json_object *jgroups = json_object_object_get(jreq, "gpoingroup");

            if (jdesc) gpodat.setDesc(json_object_get_string(jdesc));
            if (jdefault) gpodat.setGpoValue(json_object_get_boolean(jdefault));

            std::vector<uuid::uuid> allow;
            if (jgroups && json_object_get_type(jgroups) == json_type_array) {
                int glen = json_object_array_length(jgroups);
                for (int i = 0; i < glen; i++) {
                    allow.emplace_back(json_object_get_string(json_object_array_get_idx(jgroups, i)));
                }
            }
            gpodat.setAllowed(allow);
            gpo.edit(*backend, gpodat);

            json_object_put(jreq);

            json_object *jobj = json_object_new_object();
            json_object_object_add(jobj, "status", json_object_new_string("ok"));
            sendJson(curreq, jobj);
            json_object_put(jobj);
        }

        /* ---- JSON API: List Services ---- */
        void apiListServices(libhttppp::HttpRequest &curreq, const char *cdid) {
            AuthBackend *backend = getDomain(cdid);
            if (!backend) throw AuthBackendError("listServices: could not resolve domain!");

            json_object *jobj = json_object_new_object();
            json_object *jarr = json_object_new_array();

            ServiceManager svcmgr;
            std::vector<uuid::uuid> sids;
            svcmgr.list(*backend, sids);

            for (const auto &sid : sids) {
                try {
                    class ServiceData sdat(sid);
                    size_t spos = sizeof(authdb::AuthHeader);
                    svcmgr.info(*backend, sdat, spos);

                    json_object *jsvc = json_object_new_object();
                    json_object_object_add(jsvc, "id", json_object_new_string(sid.c_str()));
                    json_object_object_add(jsvc, "servicename", json_object_new_string(sdat.getServiceName()));
                    try {
                        json_object_object_add(jsvc, "hostname", json_object_new_string(sdat.getHostName()));
                    } catch (...) {
                        json_object_object_add(jsvc, "hostname", json_object_new_string(""));
                    }
                    try {
                        json_object_object_add(jsvc, "clientid", json_object_new_string(sdat.getClientId()));
                    } catch (...) {
                        json_object_object_add(jsvc, "clientid", json_object_new_string(""));
                    }
                    json_object_array_add(jarr, jsvc);
                } catch (const AuthBackendError &e) {
                    std::cerr << "AuthBackendError: " << e.what() << std::endl;
                }
            }
            json_object_object_add(jobj, "services", jarr);
            sendJson(curreq, jobj);
            json_object_put(jobj);
        }

        /* ---- JSON API: Get Service (for edit) ---- */
        void apiGetService(libhttppp::HttpRequest &curreq, const char *cdid, const char *csid) {
            AuthBackend *backend = getDomain(cdid);
            if (!backend) throw AuthBackendError("getService: could not resolve domain!");

            uuid::uuid sid;
            if (sid.parse(csid) != 0)
                throw AuthBackendError("getService: could not parse service uuid!");

            class ServiceData sdat(sid);
            ServiceManager svcmgr;
            size_t pos = sizeof(AuthHeader);
            svcmgr.info(*backend, sdat, pos);

            json_object *jobj = json_object_new_object();
            json_object_object_add(jobj, "servicename", json_object_new_string(sdat.getServiceName()));
            try {
                json_object_object_add(jobj, "hostname", json_object_new_string(sdat.getHostName()));
            } catch (...) {
                json_object_object_add(jobj, "hostname", json_object_new_string(""));
            }
            try {
                json_object_object_add(jobj, "clientid", json_object_new_string(sdat.getClientId()));
            } catch (...) {
                json_object_object_add(jobj, "clientid", json_object_new_string(""));
            }
            sendJson(curreq, jobj);
            json_object_put(jobj);
        }

        /* ---- JSON API: Create Service ---- */
        void apiCreateService(libhttppp::HttpRequest &curreq, const char *cdid) {
            AuthBackend *backend = getDomain(cdid);
            if (!backend) throw AuthBackendError("createService: could not resolve domain!");

            std::string reqbody = getRequestBody(curreq);
            json_object *jreq = json_tokener_parse(reqbody.c_str());
            if (!jreq) throw AuthBackendError("createService: invalid JSON!");

            json_object *jname = json_object_object_get(jreq, "servicename");
            json_object *jhost = json_object_object_get(jreq, "hostname");
            json_object *jclid = json_object_object_get(jreq, "clientid");
            json_object *jsecret = json_object_object_get(jreq, "servicesecret");

            uuid::uuid sid;
            sid.generate();
            class ServiceData sdat(sid);

            std::string svcname;
            if (jname) {
                svcname = json_object_get_string(jname);
                sdat.setServiceName(svcname.c_str());
            }

            std::string hostname;
            if (jhost) {
                hostname = json_object_get_string(jhost);
                sdat.setHostName(hostname.c_str());
            }

            if (jclid) sdat.setClientId(json_object_get_string(jclid));

            if (jsecret) {
                std::string secret = json_object_get_string(jsecret);
                std::string secrethash;
                sha512 hash;
                hash.hash(secret, secrethash);
                sdat.setSecret(secrethash.c_str());

                /* Compute Kerberos long-term key for service principal.
                 * Use raw password (not hash) – kinit derives the key
                 * from the password the user types, so we must match. */
                try {
                    uuid::uuid did_val;
                    did_val.parse(cdid);
                    class DomainData ddat(did_val);
                    Domain dom;
                    size_t dpos = sizeof(AuthHeader);
                    dom.info(_AdminBackend, ddat, dpos);
                    std::string domainName = ddat.getDomainName();
                    std::string realm = domainName;
                    std::transform(realm.begin(), realm.end(), realm.begin(),
                                   [](unsigned char c){ return std::toupper(c); });
                    std::string principalName = svcname;
                    if (!hostname.empty()) principalName += "/" + hostname;
                    std::string salt = realm + principalName;
                    auto krbKeyBytes = g_KdcPlugin->string2key(
                        secret, salt, KDC_ETYPE_AES256_CTS_HMAC_SHA1);
                    sdat.setKrbKey(hexEncode(krbKeyBytes));
                } catch (...) {
                    /* Non-fatal: krbkey generation failed */
                }
            }

            ServiceManager svcmgr;
            svcmgr.create(*backend, &sdat);

            json_object_put(jreq);

            json_object *jobj = json_object_new_object();
            json_object_object_add(jobj, "status", json_object_new_string("ok"));
            json_object_object_add(jobj, "id", json_object_new_string(sid.c_str()));
            sendJson(curreq, jobj);
            json_object_put(jobj);
        }

        /* ---- JSON API: Remove Service ---- */
        void apiRemoveService(libhttppp::HttpRequest &curreq, const char *cdid, const char *csid) {
            AuthBackend *backend = getDomain(cdid);
            if (!backend) throw AuthBackendError("removeService: could not resolve domain!");

            uuid::uuid svid;
            if (svid.parse(csid) != 0)
                throw AuthBackendError("removeService: could not parse service uuid!");

            backend->delRecord(svid, DataType::ServiceData);

            json_object *jobj = json_object_new_object();
            json_object_object_add(jobj, "status", json_object_new_string("ok"));
            sendJson(curreq, jobj);
            json_object_put(jobj);
        }

        /* ---- JSON API: Edit Service ---- */
        void apiEditService(libhttppp::HttpRequest &curreq, const char *cdid, const char *csid) {
            AuthBackend *backend = getDomain(cdid);
            if (!backend) throw AuthBackendError("editService: could not resolve domain!");

            uuid::uuid sid;
            if (sid.parse(csid) != 0)
                throw AuthBackendError("editService: could not parse service uuid!");

            class ServiceData sdat(sid);
            ServiceManager svcmgr;
            size_t pos = sizeof(AuthHeader);
            svcmgr.info(*backend, sdat, pos);

            std::string reqbody = getRequestBody(curreq);
            json_object *jreq = json_tokener_parse(reqbody.c_str());
            if (!jreq) throw AuthBackendError("editService: invalid JSON!");

            json_object *jname = json_object_object_get(jreq, "servicename");
            json_object *jhost = json_object_object_get(jreq, "hostname");
            json_object *jclid = json_object_object_get(jreq, "clientid");
            json_object *jsecret = json_object_object_get(jreq, "servicesecret");

            std::string svcname;
            if (jname) {
                svcname = json_object_get_string(jname);
                sdat.setServiceName(svcname.c_str());
            } else {
                try { svcname = sdat.getServiceName(); } catch (...) {}
            }

            std::string hostname;
            if (jhost) {
                hostname = json_object_get_string(jhost);
                sdat.setHostName(hostname.c_str());
            } else {
                try { const char *h = sdat.getHostName(); if (h) hostname = h; } catch (...) {}
            }

            if (jclid) sdat.setClientId(json_object_get_string(jclid));

            if (jsecret) {
                std::string secret = json_object_get_string(jsecret);
                if (!secret.empty()) {
                    std::string secrethash;
                    sha512 hash;
                    hash.hash(secret, secrethash);
                    sdat.setSecret(secrethash.c_str());

                    /* Recompute Kerberos key (raw password, not hash) */
                    try {
                        uuid::uuid did_val;
                        did_val.parse(cdid);
                        class DomainData ddat(did_val);
                        Domain dom;
                        size_t dpos = sizeof(AuthHeader);
                        dom.info(_AdminBackend, ddat, dpos);
                        std::string domainName = ddat.getDomainName();
                        std::string realm = domainName;
                        std::transform(realm.begin(), realm.end(), realm.begin(),
                                       [](unsigned char c){ return std::toupper(c); });
                        std::string principalName = svcname;
                        if (!hostname.empty()) principalName += "/" + hostname;
                        std::string salt = realm + principalName;
                        auto krbKeyBytes = g_KdcPlugin->string2key(
                            secret, salt, KDC_ETYPE_AES256_CTS_HMAC_SHA1);
                        sdat.setKrbKey(hexEncode(krbKeyBytes));
                    } catch (...) {
                        /* Non-fatal */
                    }
                }
            }

            svcmgr.edit(*backend, sdat);

            json_object_put(jreq);

            json_object *jobj = json_object_new_object();
            json_object_object_add(jobj, "status", json_object_new_string("ok"));
            sendJson(curreq, jobj);
            json_object_put(jobj);
        }

        /* ---- JSON API: List Clients ---- */
        void apiListClients(libhttppp::HttpRequest &curreq, const char *cdid) {
            AuthBackend *backend = getDomain(cdid);
            if (!backend) throw AuthBackendError("listClients: could not resolve domain!");

            json_object *jobj = json_object_new_object();
            json_object *jarr = json_object_new_array();

            Client clt;
            std::vector<uuid::uuid> clids;
            clt.list(*backend, clids);

            for (const auto &clid : clids) {
                try {
                    class ClientData cdat(clid);
                    size_t ccpos = sizeof(authdb::AuthHeader);
                    clt.info(*backend, cdat, ccpos);

                    json_object *jclient = json_object_new_object();
                    json_object_object_add(jclient, "id", json_object_new_string(clid.c_str()));
                    json_object_object_add(jclient, "name", json_object_new_string(cdat.getName()));
                    json_object_array_add(jarr, jclient);
                } catch (const AuthBackendError &e) {
                    std::cerr << "AuthBackendError: " << e.what() << std::endl;
                }
            }
            json_object_object_add(jobj, "clients", jarr);
            sendJson(curreq, jobj);
            json_object_put(jobj);
        }

        /* ---- JSON API: Get Client (for edit) ---- */
        void apiGetClient(libhttppp::HttpRequest &curreq, const char *cdid, const char *cclid) {
            AuthBackend *backend = getDomain(cdid);
            if (!backend) throw AuthBackendError("getClient: could not resolve domain!");

            uuid::uuid clid;
            if (clid.parse(cclid) != 0)
                throw AuthBackendError("getClient: could not parse client uuid!");

            class ClientData cldat(clid);
            Client clt;
            size_t pos = sizeof(AuthHeader);
            clt.info(*backend, cldat, pos);

            json_object *jobj = json_object_new_object();
            json_object_object_add(jobj, "clientname", json_object_new_string(cldat.getName()));
            json_object_object_add(jobj, "clientkey", json_object_new_string(cldat.getKey()));
            sendJson(curreq, jobj);
            json_object_put(jobj);
        }

        /* ---- JSON API: Create Client ---- */
        void apiCreateClient(libhttppp::HttpRequest &curreq, const char *cdid) {
            AuthBackend *backend = getDomain(cdid);
            if (!backend) throw AuthBackendError("createClient: could not resolve domain!");

            std::string reqbody = getRequestBody(curreq);
            json_object *jreq = json_tokener_parse(reqbody.c_str());
            if (!jreq) throw AuthBackendError("createClient: invalid JSON!");

            json_object *jname = json_object_object_get(jreq, "clientname");
            json_object *jkey = json_object_object_get(jreq, "clientkey");

            uuid::uuid clid;
            clid.generate();
            class ClientData cdat(clid);

            if (jname) cdat.setName(json_object_get_string(jname));

            if (jkey) {
                std::string key = json_object_get_string(jkey);
                std::string keyhash;
                sha512 hash;
                hash.hash(key, keyhash);
                cdat.setKey(keyhash.c_str());

                /* Compute Kerberos long-term key for machine account */
                if (jname && !key.empty() && g_KdcPlugin) {
                    std::string clientname = json_object_get_string(jname);
                    std::string realm = backend->getDomain();
                    std::transform(realm.begin(), realm.end(), realm.begin(),
                                   [](unsigned char c){ return std::toupper(c); });
                    std::string salt = realm + "host/" + clientname;
                    auto krbKeyBytes = g_KdcPlugin->string2key(
                        key, salt, KDC_ETYPE_AES256_CTS_HMAC_SHA1);
                    cdat.setKrbKey(hexEncode(krbKeyBytes));
                }
            }

            Client clt;
            clt.create(*backend, &cdat);

            json_object_put(jreq);

            json_object *jobj = json_object_new_object();
            json_object_object_add(jobj, "status", json_object_new_string("ok"));
            json_object_object_add(jobj, "id", json_object_new_string(clid.c_str()));
            sendJson(curreq, jobj);
            json_object_put(jobj);
        }

        /* ---- JSON API: Remove Client ---- */
        void apiRemoveClient(libhttppp::HttpRequest &curreq, const char *cdid, const char *cclid) {
            AuthBackend *backend = getDomain(cdid);
            if (!backend) throw AuthBackendError("removeClient: could not resolve domain!");

            uuid::uuid cldid;
            if (cldid.parse(cclid) != 0)
                throw AuthBackendError("removeClient: could not parse client uuid!");

            backend->delRecord(cldid, DataType::ClientData);

            json_object *jobj = json_object_new_object();
            json_object_object_add(jobj, "status", json_object_new_string("ok"));
            sendJson(curreq, jobj);
            json_object_put(jobj);
        }

        /* ---- JSON API: Edit Client ---- */
        void apiEditClient(libhttppp::HttpRequest &curreq, const char *cdid, const char *cclid) {
            AuthBackend *backend = getDomain(cdid);
            if (!backend) throw AuthBackendError("editClient: could not resolve domain!");

            uuid::uuid clid;
            if (clid.parse(cclid) != 0)
                throw AuthBackendError("editClient: could not parse client uuid!");

            class ClientData cldat(clid);
            Client clt;
            size_t pos = sizeof(AuthHeader);
            clt.info(*backend, cldat, pos);

            std::string reqbody = getRequestBody(curreq);
            json_object *jreq = json_tokener_parse(reqbody.c_str());
            if (!jreq) throw AuthBackendError("editClient: invalid JSON!");

            json_object *jname = json_object_object_get(jreq, "clientname");
            json_object *jkey = json_object_object_get(jreq, "clientkey");

            if (jname) cldat.setName(json_object_get_string(jname));

            if (jkey) {
                std::string key = json_object_get_string(jkey);
                if (!key.empty()) {
                    std::string keyhash;
                    sha512 hash;
                    hash.hash(key, keyhash);
                    cldat.setKey(keyhash.c_str());

                    /* Recompute Kerberos long-term key for machine account */
                    const char *cname = cldat.getName();
                    if (cname && cname[0]) {
                        std::string realm = backend->getDomain();
                        std::transform(realm.begin(), realm.end(), realm.begin(),
                                       [](unsigned char c){ return std::toupper(c); });
                        std::string salt = realm + "host/" + std::string(cname);
                        auto krbKeyBytes = g_KdcPlugin->string2key(
                            key, salt, KDC_ETYPE_AES256_CTS_HMAC_SHA1);
                        cldat.setKrbKey(hexEncode(krbKeyBytes));
                    }
                }
            }

            clt.edit(*backend, cldat);

            json_object_put(jreq);

            json_object *jobj = json_object_new_object();
            json_object_object_add(jobj, "status", json_object_new_string("ok"));
            sendJson(curreq, jobj);
            json_object_put(jobj);
        }

        /* ---- JSON API: Export Database ---- */
        void apiExportDb(libhttppp::HttpRequest &curreq, const char *cdid) {
            AuthBackend *backend = getDomain(cdid);
            if (!backend) throw AuthBackendError("export: could not resolve domain!");
            AuthBackend::Guard guard(*backend);

            json_object *jobj = Export::toJson(*backend);
            sendJson(curreq, jobj);
            json_object_put(jobj);
        }

        /* ---- JSON API: Import Database ---- */
        void apiImportDb(libhttppp::HttpRequest &curreq, const char *cdid) {
            AuthBackend *backend = getDomain(cdid);
            if (!backend) throw AuthBackendError("import: could not resolve domain!");
            AuthBackend::Guard guard(*backend);

            std::string reqbody = getRequestBody(curreq);
            json_object *jreq = json_tokener_parse(reqbody.c_str());
            if (!jreq) throw AuthBackendError("import: invalid JSON!");

            int count = Import::fromJson(*backend, jreq);
            json_object_put(jreq);

            json_object *jobj = json_object_new_object();
            json_object_object_add(jobj, "status", json_object_new_string("ok"));
            json_object_object_add(jobj, "imported", json_object_new_int(count));
            sendJson(curreq, jobj);
            json_object_put(jobj);
        }

        /* ---- JSON API: Vacuum Database ---- */
        void apiVacuum(libhttppp::HttpRequest &curreq, const char *cdid) {
            AuthBackend *backend = getDomain(cdid);
            if (!backend) throw AuthBackendError("vacuum: could not resolve domain!");

            size_t sizeBefore = 0, sizeAfter = 0;
            backend->vacuum(sizeBefore, sizeAfter);

            json_object *jobj = json_object_new_object();
            json_object_object_add(jobj, "status", json_object_new_string("ok"));
            json_object_object_add(jobj, "size_before", json_object_new_int64(sizeBefore));
            json_object_object_add(jobj, "size_after", json_object_new_int64(sizeAfter));
            json_object_object_add(jobj, "freed", json_object_new_int64(sizeBefore - sizeAfter));

            // Vacuum cluster block stores when cluster mode is active
            if (g_Cluster && g_Cluster->isRunning()) {
                auto &pclient = g_Cluster->getClient();
                if (pclient) {
                    size_t nodes_ok = 0;
                    try {
                        nodes_ok = pclient->vacuum_all_nodes();
                    } catch (...) {}
                    json_object_object_add(jobj, "cluster_vacuum",
                                           json_object_new_boolean(true));
                    json_object_object_add(jobj, "cluster_nodes_vacuumed",
                                           json_object_new_int64(static_cast<int64_t>(nodes_ok)));
                } else {
                    json_object_object_add(jobj, "cluster_vacuum",
                                           json_object_new_boolean(false));
                }
            }

            sendJson(curreq, jobj);
            json_object_put(jobj);
        }

        /* ---- Login (keeps HTML form-based login for compatibility) ---- */
        void login(libhttppp::HttpRequest &curreq, const int tid, ULONG_PTR args) {
            libhttppp::HttpForm form;
            form.parse(curreq);

            std::string username, password, pwhash, domain;

            for (const auto &curformdat : form.urlData()) {
                if (curformdat.key == "username")
                    username = curformdat.value;
                else if (curformdat.key == "password")
                    password = curformdat.value;
                else if (curformdat.key == "domain")
                    domain = curformdat.value;
            }

            if (password.empty() || username.empty())
                throw AuthBackendError("login: password or username are empty!");

            sha512 hash;
            hash.hash(password, pwhash);

            AuthBackend *backend = nullptr;
            uuid::uuid did;

            if (domain.empty() || domain == "admin") {
                backend = &_AdminBackend;
            } else {
                did.parse(domain.c_str());
                backend = getDomain(domain.c_str());
            }
            if (!backend) {
                throw AuthBackendError("login: could not parse uuid!");
            }

            User user;
            std::vector<uuid::uuid> uids;
            user.list(*backend, uids);

            for (const auto &uid : uids) {
                class UserData udat(uid);
                size_t upos = sizeof(authdb::AuthHeader);
                user.info(*backend, udat, upos);

                if (!username.empty() && username == udat.getUsername() &&
                    !pwhash.empty() && pwhash == udat.getPwHash()) {

                    libhttppp::HttpCookie cookie;
                    uuid::uuid sid;
                    const SessionData *cses = AuthSession->addSession(*backend, did, udat.Data.ruid);
                    cses->getSid(sid);

                    if (!cses->checkGPO("888cd19b-352d-11f0-a8cc-3cecefce9ac2")) {
                        AuthSession->remSession(sid);
                        throw AuthBackendError("Insufficient Access !");
                    }

                    libhttppp::HttpResponse rep;
                    cookie.setcookie(rep, "authdbid", sid.c_str(), "", "", -1, "/");
                    rep.setState(HTTP303);
                    rep.setHeaderData("Location")->push_back(curreq.getRequestURL());
                    rep.setContentType("text/html");
                    rep.send(curreq, "", 0);
                    return;
                }
            }
            throw AuthBackendError("login: Wrong Password or Username!");
        }

        /* ---- JSON API: Logout ---- */
        void apiLogout(libhttppp::HttpRequest &curreq, const uuid::uuid &ssid) {
            AuthSession->remSession(ssid);
            json_object *jobj = json_object_new_object();
            json_object_object_add(jobj, "status", json_object_new_string("ok"));
            sendJson(curreq, jobj);
            json_object_put(jobj);
        }

        /* ---- Main Settings Controller (combines HTML serving + JSON API routing) ---- */
        void SettingsController(libhttppp::HttpRequest &curreq, const int tid, ULONG_PTR args) {
            libhttppp::HttpCookie cookie;
            const SessionData *sdat = nullptr;
            uuid::uuid ssid;

            try {
                cookie.parse(curreq);
                for (libhttppp::HttpCookie::CookieData *cur = cookie.getfirstCookieData(); cur; cur = cur->nextCookieData()) {
                    std::cerr << "[CTRL] cookie: key='" << cur->getKey() << "' val='" << cur->getValue() << "'" << std::endl;
                    if (cur->getKey() == "authdbid") {
                        int rc = ssid.parse(cur->getValue());
                        std::cerr << "[CTRL] uuid parse rc=" << rc << " sid=" << ssid.c_str() << std::endl;
                        if (rc == 0) {
                            sdat = AuthSession->getSession(ssid);
                            std::cerr << "[CTRL] getSession result=" << (sdat ? "found" : "null") << std::endl;
                        }
                        break;
                    }
                }

                // Logout (both HTML redirect + API)
                if (curreq.getRequestURL().compare("/settings/logout") == 0) {
                    AuthSession->remSession(ssid);
                    libhttppp::HttpResponse rep;
                    rep.setState(HTTP303);
                    rep.setHeaderData("Location")->push_back("/settings/index");
                    rep.setContentType("text/html");
                    std::vector<char> out;
                    rep.send(curreq, out, 0);
                    return;
                }

                // API logout
                if (curreq.getRequestURL().compare("/settings/api/logout") == 0) {
                    apiLogout(curreq, ssid);
                    return;
                }

                if (!sdat) {
                    throw AuthBackendError("No Session found!");
                }
            } catch (...) {
                // If cluster is critical (< k nodes), show warning page with auto-refresh
                if (g_Cluster && g_Cluster->isCritical()) {
                    const char *warn_html =
                        "<!DOCTYPE html><html><head>"
                        "<meta http-equiv=\"refresh\" content=\"10\">"
                        "<meta charset=\"utf-8\">"
                        "<title>Cluster nicht bereit</title>"
                        "<style>"
                        "body{font-family:sans-serif;display:flex;justify-content:center;"
                        "align-items:center;min-height:100vh;margin:0;background:#1a1a2e;color:#e0e0e0}"
                        ".box{text-align:center;background:#16213e;padding:40px 60px;border-radius:12px;"
                        "box-shadow:0 4px 20px rgba(0,0,0,0.4)}"
                        ".spinner{border:4px solid #333;border-top:4px solid #e94560;border-radius:50%;"
                        "width:40px;height:40px;animation:spin 1s linear infinite;margin:20px auto}"
                        "@keyframes spin{to{transform:rotate(360deg)}}"
                        "h2{color:#e94560;margin-bottom:10px}"
                        "p{color:#a0a0a0}"
                        "</style></head><body>"
                        "<div class=\"box\">"
                        "<div class=\"spinner\"></div>"
                        "<h2>Cluster nicht bereit</h2>"
                        "<p>Zu wenig Nodes erreichbar. Warte auf Cluster-Wiederherstellung&hellip;</p>"
                        "<p style=\"font-size:0.8em\">Seite wird alle 10 Sekunden aktualisiert.</p>"
                        "</div></body></html>";
                    libhttppp::HttpResponse rep;
                    rep.setState(HTTP503);
                    rep.setContentType("text/html");
                    rep.send(curreq, warn_html, strlen(warn_html));
                    return;
                }
                try {
                    login(curreq, tid, args);
                    return;
                } catch (AuthBackendError &el) {
                    // Show login page with error
                    std::shared_ptr<libhtmlpp::HtmlString> out = std::make_shared<libhtmlpp::HtmlString>(),
                        dform = std::make_shared<libhtmlpp::HtmlString>();

                    libhtmlpp::HtmlElement index = *_LoginElement, *domel = index.getElementbyID("domain");
                    libhtmlpp::HtmlElement *loginel = index.getElementbyID("login");

                    AuthBackend::Guard domguard(_AdminBackend);
                    Domain domain;
                    std::vector<uuid::uuid> dids;
                    domain.list(_AdminBackend, dids);

                    for (const auto &did : dids) {
                        class DomainData ddat(did);
                        try {
                            size_t dpos = sizeof(authdb::AuthHeader);
                            domain.info(_AdminBackend, ddat, dpos);
                            *dform << "<option value=\"" << did.c_str() << "\" >"
                                << ddat.getDomainName().c_str()
                                << "</option>";
                            domel->appendChild(dform->parse());
                            dform->clear();
                        } catch (AuthBackendError &e) {
                            std::cerr << e.what() << std::endl;
                        }
                    }

                    if (loginel && el.what()) {
                        libhtmlpp::HtmlString errmsg;
                        errmsg << "<span>" << el.what() << "</span>";
                        loginel->appendChild(errmsg.parse());
                    }

                    libhtmlpp::print(index, *out, true);
                    libhttppp::HttpResponse rep;
                    rep.setContentType("text/html");
                    rep.send(curreq, out->data(), out->size());
                    return;
                }
            }

            // ---- Route API requests ----
            const std::string &url = curreq.getRequestURL();
            char domain[255] = {}, apiurl[255] = {}, param[255] = {};

            // Check for /settings/{domain}/api/{action}[/{param}]
            if (sscanf(url.c_str(), "/settings/%[^/]/api/%[^/]/%s", domain, apiurl, param) >= 2) {
                try {
                    if (strcmp(apiurl, "listusers") == 0) {
                        checkDomainAccess(sdat, domain);
                        checkGPO(sdat, "a1b2c3d4-452e-11f0-b1cc-1a2b3c4d5e02");
                        apiListUsers(curreq, domain);
                    } else if (strcmp(apiurl, "getuser") == 0 && param[0]) {
                        checkDomainAccess(sdat, domain);
                        checkGPO(sdat, "a1b2c3d4-452e-11f0-b1cc-1a2b3c4d5e02");
                        apiGetUser(curreq, domain, param);
                    } else if (strcmp(apiurl, "createuser") == 0) {
                        checkDomainAccess(sdat, domain);
                        checkGPO(sdat, "a1b2c3d4-452e-11f0-b1cc-1a2b3c4d5e02");
                        apiCreateUser(curreq, domain);
                    } else if (strcmp(apiurl, "removeuser") == 0 && param[0]) {
                        checkDomainAccess(sdat, domain);
                        checkGPO(sdat, "a1b2c3d4-452e-11f0-b1cc-1a2b3c4d5e02");
                        apiRemoveUser(curreq, domain, param);
                    } else if (strcmp(apiurl, "edituser") == 0 && param[0]) {
                        checkDomainAccess(sdat, domain);
                        checkGPO(sdat, "a1b2c3d4-452e-11f0-b1cc-1a2b3c4d5e02");
                        apiEditUser(curreq, domain, param);
                    } else if (strcmp(apiurl, "listdomains") == 0) {
                        apiListDomains(curreq);
                    } else if (strcmp(apiurl, "checkpermissions") == 0) {
                        apiCheckPermissions(curreq, sdat);
                    } else if (strcmp(apiurl, "createdomain") == 0) {
                        apiCreateDomain(curreq, sdat);
                    } else if (strcmp(apiurl, "removedomain") == 0 && param[0]) {
                        apiRemoveDomain(curreq, param, sdat);
                    } else if (strcmp(apiurl, "listgroups") == 0) {
                        checkDomainAccess(sdat, domain);
                        checkGPO(sdat, "a1b2c3d4-452e-11f0-b1cc-1a2b3c4d5e03");
                        apiListGroups(curreq, domain);
                    } else if (strcmp(apiurl, "getgroup") == 0 && param[0]) {
                        checkDomainAccess(sdat, domain);
                        checkGPO(sdat, "a1b2c3d4-452e-11f0-b1cc-1a2b3c4d5e03");
                        apiGetGroup(curreq, domain, param);
                    } else if (strcmp(apiurl, "creategroup") == 0) {
                        checkDomainAccess(sdat, domain);
                        checkGPO(sdat, "a1b2c3d4-452e-11f0-b1cc-1a2b3c4d5e03");
                        apiCreateGroup(curreq, domain);
                    } else if (strcmp(apiurl, "removegroup") == 0 && param[0]) {
                        checkDomainAccess(sdat, domain);
                        checkGPO(sdat, "a1b2c3d4-452e-11f0-b1cc-1a2b3c4d5e03");
                        apiRemoveGroup(curreq, domain, param);
                    } else if (strcmp(apiurl, "editgroup") == 0 && param[0]) {
                        checkDomainAccess(sdat, domain);
                        checkGPO(sdat, "a1b2c3d4-452e-11f0-b1cc-1a2b3c4d5e03");
                        apiEditGroup(curreq, domain, param);
                    } else if (strcmp(apiurl, "listsessions") == 0) {
                        checkDomainAccess(sdat, domain);
                        apiListSessions(curreq, domain);
                    } else if (strcmp(apiurl, "removesession") == 0 && param[0]) {
                        checkDomainAccess(sdat, domain);
                        apiRemoveSession(curreq, param);
                    } else if (strcmp(apiurl, "reloadsession") == 0 && param[0]) {
                        checkDomainAccess(sdat, domain);
                        apiReloadSession(curreq, domain, param);
                    } else if (strcmp(apiurl, "listgpos") == 0) {
                        checkDomainAccess(sdat, domain);
                        apiListGpos(curreq, domain);
                    } else if (strcmp(apiurl, "getgpo") == 0 && param[0]) {
                        checkDomainAccess(sdat, domain);
                        apiGetGpo(curreq, domain, param);
                    } else if (strcmp(apiurl, "creategpo") == 0) {
                        checkDomainAccess(sdat, domain);
                        apiCreateGpo(curreq, domain);
                    } else if (strcmp(apiurl, "removegpo") == 0 && param[0]) {
                        checkDomainAccess(sdat, domain);
                        apiRemoveGpo(curreq, domain, param);
                    } else if (strcmp(apiurl, "editgpo") == 0 && param[0]) {
                        checkDomainAccess(sdat, domain);
                        apiEditGpo(curreq, domain, param);
                    } else if (strcmp(apiurl, "listclients") == 0) {
                        checkDomainAccess(sdat, domain);
                        apiListClients(curreq, domain);
                    } else if (strcmp(apiurl, "getclient") == 0 && param[0]) {
                        checkDomainAccess(sdat, domain);
                        apiGetClient(curreq, domain, param);
                    } else if (strcmp(apiurl, "createclient") == 0) {
                        checkDomainAccess(sdat, domain);
                        apiCreateClient(curreq, domain);
                    } else if (strcmp(apiurl, "removeclient") == 0 && param[0]) {
                        checkDomainAccess(sdat, domain);
                        apiRemoveClient(curreq, domain, param);
                    } else if (strcmp(apiurl, "editclient") == 0 && param[0]) {
                        checkDomainAccess(sdat, domain);
                        apiEditClient(curreq, domain, param);
                    } else if (strcmp(apiurl, "listservices") == 0) {
                        checkDomainAccess(sdat, domain);
                        checkGPO(sdat, "a1b2c3d4-452e-11f0-b1cc-1a2b3c4d5e04");
                        apiListServices(curreq, domain);
                    } else if (strcmp(apiurl, "getservice") == 0 && param[0]) {
                        checkDomainAccess(sdat, domain);
                        checkGPO(sdat, "a1b2c3d4-452e-11f0-b1cc-1a2b3c4d5e04");
                        apiGetService(curreq, domain, param);
                    } else if (strcmp(apiurl, "createservice") == 0) {
                        checkDomainAccess(sdat, domain);
                        checkGPO(sdat, "a1b2c3d4-452e-11f0-b1cc-1a2b3c4d5e04");
                        apiCreateService(curreq, domain);
                    } else if (strcmp(apiurl, "removeservice") == 0 && param[0]) {
                        checkDomainAccess(sdat, domain);
                        checkGPO(sdat, "a1b2c3d4-452e-11f0-b1cc-1a2b3c4d5e04");
                        apiRemoveService(curreq, domain, param);
                    } else if (strcmp(apiurl, "editservice") == 0 && param[0]) {
                        checkDomainAccess(sdat, domain);
                        checkGPO(sdat, "a1b2c3d4-452e-11f0-b1cc-1a2b3c4d5e04");
                        apiEditService(curreq, domain, param);
                    } else if (strcmp(apiurl, "export") == 0) {
                        checkDomainAccess(sdat, domain);
                        checkGPO(sdat, "a1b2c3d4-452e-11f0-b1cc-1a2b3c4d5e05");
                        apiExportDb(curreq, domain);
                    } else if (strcmp(apiurl, "import") == 0) {
                        checkDomainAccess(sdat, domain);
                        checkGPO(sdat, "a1b2c3d4-452e-11f0-b1cc-1a2b3c4d5e05");
                        apiImportDb(curreq, domain);
                    } else if (strcmp(apiurl, "vacuum") == 0) {
                        checkDomainAccess(sdat, domain);
                        apiVacuum(curreq, domain);
                    } else if (strcmp(apiurl, "clusterstatus") == 0) {
                        apiClusterStatus(curreq);
                    } else if (strcmp(apiurl, "clustersync") == 0) {
                        apiClusterSync(curreq);
                    } else if (strcmp(apiurl, "clusterscrub") == 0) {
                        apiClusterScrub(curreq);
                    } else if (strcmp(apiurl, "clusterrebalance") == 0) {
                        apiClusterRebalance(curreq);
                    } else {
                        sendJsonError(curreq, "Unknown API endpoint", 404);
                    }
                } catch (AuthBackendError &e) {
                    sendJsonError(curreq, e.what());
                }
                return;
            }

            // ---- Serve admin.html for all non-API /settings/ requests ----
            if (!_AdminHtml.empty()) {
                libhttppp::HttpResponse rep;
                rep.setState(HTTP200);
                rep.setContentType("text/html");
                rep.send(curreq, _AdminHtml, _AdminHtml.size());
            } else {
                sendJsonError(curreq, "admin.html not found", 404);
            }
        }

    private:
        AuthBackend* getDomain(const char *cdid) {
            if (strcmp(cdid, "admin") == 0)
                return &_AdminBackend;
            if (_DomainResolver)
                return _DomainResolver(cdid);
            DomainBackend dbackend(_AdminBackend);
            return resolveDomain(cdid, _AdminBackend, dbackend);
        }

        authdb::AuthBackend &_AdminBackend;
        std::shared_ptr<libhtmlpp::HtmlElement> _LoginElement;
        std::shared_ptr<libhtmlpp::HtmlPage> _Loginpage;
        std::vector<char> _AdminHtml;
        std::function<AuthBackend*(const char*)> _DomainResolver;
        std::function<void(const char*)> _DomainCacheEvictor;
    };

    AdminInterface::AdminInterface(AuthBackend &bck, std::vector<netplus::socket*> ssock) : _bck(bck) {
        _ctl = new AdminController(bck, ssock);
    }

    AdminInterface::~AdminInterface() {
        delete _ctl;
    }

    void AdminInterface::setDomainResolver(std::function<AuthBackend*(const char*)> resolver) {
        _ctl->setDomainResolver(std::move(resolver));
    }

    void AdminInterface::setDomainCacheEvictor(std::function<void(const char*)> evictor) {
        _ctl->setDomainCacheEvictor(std::move(evictor));
    }

    void AdminInterface::Request(libhttppp::HttpRequest &curreq, const int tid, ULONG_PTR args) {
        if (curreq.getRequestURL().compare(0, 9, "/settings", 9) == 0) {
            _ctl->SettingsController(curreq, tid, args);
        } else {
            libhttppp::HttpResponse rep;
            rep.setState(HTTP404);
            rep.setContentType("text/html");
            std::vector<char> out;
            rep.send(curreq, out, 0);
        }
    };

};
