/*******************************************************************************
 * 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 <memory>
#include <iostream>
#include <vector>
#include <algorithm>
#include <cstring>

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

#include "hash.h"
#include "hash/sha512.h"
#include "authdb.h"
#include "backend.h"
#include "api.h"
#include "user.h"
#include "client.h"
#include "gpo.h"
#include "gpo_default.h"
#include "group.h"
#include "session.h"
#include "domain.h"
#include "plugin.h"

namespace authdb {

namespace {
    // Constant-time compare to avoid timing side-channels on secrets
    bool constant_time_eq(const std::string& a, const std::string& b) {
        if (a.size() != b.size()) return false;
        unsigned char diff = 0;
        for (size_t i = 0; i < a.size(); ++i) {
            diff |= static_cast<unsigned char>(a[i]) ^ static_cast<unsigned char>(b[i]);
        }
        return diff == 0;
    }

    // Hex-encode raw bytes to a lowercase hex string
    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;
    }

    // Helper: get required top-level "commands" array and type-check it
    json_object* get_commands_array_or_throw(json_object* request) {
        json_object* cmd = json_object_object_get(request, "commands");
        if (!cmd || json_object_get_type(cmd) != json_type_array) {
            throw(AuthBackendError("Invalid Api Commands"));
        }
        return cmd;
    }

    // Helper: after reading a record header, compute next rd with bounds checks.
    // Assumes backend position is at end of the header bytes for this record.
    bool compute_next_rd(size_t& rd, size_t end, size_t datasize, size_t header_end_pos) {
        if (header_end_pos > end) return false;
        size_t next = header_end_pos + datasize;
        if (next < header_end_pos || next > end) return false; // overflow / OOB
        rd = next;
        return true;
    }
}

class Api {
public:
    void checkGPO(json_object *request, json_object *response) {
        json_object* cmd = get_commands_array_or_throw(request);

        int fcount = json_object_array_length(cmd);
        for (int i = 0; i < fcount; ++i) {
            json_object_object_foreach(json_object_array_get_idx(cmd, i), key, val) {
                if (strcmp(key, "checkgpo") == 0) {
                    if (!_SessionData) continue; // defensive
                    const char* id = json_object_get_string(val);

                    json_object* respid = json_object_new_object();
                    json_object* value  = json_object_new_object();

                    json_object_object_add(value, id,
                        json_object_new_boolean(_SessionData->checkGPO(id)));
                    json_object_object_add(respid, "gpo", value);
                    json_object_array_add(response, respid);
                }
            }
        }
    }

    void existsGPO(json_object *request, json_object *response) {
        json_object* cmd = get_commands_array_or_throw(request);

        int fcount = json_object_array_length(cmd);
        for (int i = 0; i < fcount; ++i) {
            json_object_object_foreach(json_object_array_get_idx(cmd, i), key, val) {
                if (strcmp(key, "gpoexists") == 0) {
                    const char* id = json_object_get_string(val);

                    json_object* respid = json_object_new_object();
                    json_object* value  = json_object_new_object();

                    class Gpo gpo;
                    int ret = -1;

                    // If your API method is actually "exits", revert this to exits(...)
                    if (gpo.exists(_Backend, ::uuid::uuid(id), nullptr, ret)) {
                        json_object_object_add(value, id, json_object_new_boolean(true));
                    } else {
                        json_object_object_add(value, id, json_object_new_boolean(false));
                    }

                    json_object_object_add(respid, "gpoexists", value);
                    json_object_array_add(response, respid);
                }
            }
        }
    }

    void addGPO(json_object *request, json_object *response) {
        json_object* cmd = get_commands_array_or_throw(request);

        int fcount = json_object_array_length(cmd);
        for (int i = 0; i < fcount; ++i) {
            json_object_object_foreach(json_object_array_get_idx(cmd, i), key, val) {
                if (strcmp(key, "gpoadd") == 0) {
                    std::string name, desc, gpoid;
                    bool dflt = false;
                    std::vector<::uuid::uuid> members;

                    json_object_object_foreach(val, okey, oval) {
                        if (strcmp(okey, "gpoid") == 0) {
                            gpoid = json_object_get_string(oval);
                        } else if (strcmp(okey, "gponame") == 0) {
                            name = json_object_get_string(oval);
                        } else if (strcmp(okey, "gpodesc") == 0) {
                            desc = json_object_get_string(oval);
                        } else if (strcmp(okey, "gpodefault") == 0) {
                            dflt = json_object_get_boolean(oval);
                        } else if (strcmp(okey, "gpomembers") == 0) {
                            size_t alen = json_object_array_length(oval);
                            for (size_t iii = 0; iii < alen; ++iii) {
                                members.emplace_back(
                                    json_object_get_string(json_object_array_get_idx(oval, iii)));
                            }
                        }
                    }

                    json_object* respid = json_object_new_object();
                    json_object* value  = json_object_new_object();

                    if (name.empty() || desc.empty() || gpoid.empty()) {
                        json_object_object_add(value, gpoid.empty() ? "" : gpoid.c_str(),
                                               json_object_new_boolean(false));
                        json_object_object_add(respid, "gpoadd", value);
                        json_object_array_add(response, respid);
                        continue;
                    }

                    ::uuid::uuid id(gpoid.c_str());

                    try {
                        class GpoData gdat(id);
                        gdat.setName(name.c_str());
                        gdat.setDesc(desc.c_str());
                        gdat.setGpoValue(dflt);
                        gdat.setAllowed(members);

                        class Gpo gpo;
                        gpo.create(_Backend, &gdat);

                        int ret = -1;
                        if (gpo.exists(_Backend, id, nullptr, ret)) {
                            json_object_object_add(value, id.c_str(), json_object_new_boolean(true));
                        } else {
                            json_object_object_add(value, id.c_str(), json_object_new_boolean(false));
                        }
                    } catch (...) {
                        json_object_object_add(value, id.c_str(), json_object_new_boolean(false));
                    }

                    json_object_object_add(respid, "gpoadd", value);
                    json_object_array_add(response, respid);
                }
            }
        }
    }

    const SessionData* checkUser(json_object *request, json_object * /*response*/) {
        json_object* cmd = get_commands_array_or_throw(request);

        int fcount = json_object_array_length(cmd);
        for (int i = 0; i < fcount; ++i) {
            json_object_object_foreach(json_object_array_get_idx(cmd, i), key, val) {
                if (strcmp(key, "usersession") == 0) {
                    const char* id = json_object_get_string(val);
                    return AuthSession->getSession(id);
                }
            }
        }
        return nullptr;
    }

    const ClientConnections* checkConnection(json_object *request, json_object * /*response*/) {
        json_object* cmd = get_commands_array_or_throw(request);

        int fcount = json_object_array_length(cmd);
        for (int i = 0; i < fcount; ++i) {
            json_object_object_foreach(json_object_array_get_idx(cmd, i), key, val) {
                if (strcmp(key, "clientsession") == 0) {
                    const char* id = json_object_get_string(val);
                    ::uuid::uuid cid;
                    if (cid.parse(id) != 0) return nullptr;
                    return AuthClient.findConnection(cid);
                }
            }
        }
        return nullptr;
    }

    const SessionData* userLogin(json_object *request, json_object *response) {
        json_object* cmd = get_commands_array_or_throw(request);

        int fcount = json_object_array_length(cmd);
        for (int i = 0; i < fcount; ++i) {
            json_object_object_foreach(json_object_array_get_idx(cmd, i), key, val) {
                if (strcmp(key, "login") != 0) continue;

                std::string type, name, pw, pwsalt;
                json_object_object_foreach(val, okey, oval) {
                    if (strcmp(okey, "type") == 0) type = json_object_get_string(oval);
                    else if (strcmp(okey, "name") == 0) name = json_object_get_string(oval);
                    else if (strcmp(okey, "key")  == 0) pw   = json_object_get_string(oval);
                }

                if (type != "user" || name.empty() || pw.empty()) continue;

                // Hash incoming password (NOTE: consider replacing with Argon2id)
                sha512 sha;
                sha.hash(pw, pwsalt);

                size_t rd  = sizeof(authdb::AuthHeader);
                size_t end = _Backend.end();

                _SessionData = nullptr;

                std::shared_ptr<authdb::AuthData::Record> cur =
                    std::make_shared<authdb::AuthData::Record>();

                while (rd + sizeof(AuthData::Record) <= end) {
                    _Backend.setPos(rd);
                    _Backend.read(reinterpret_cast<unsigned char*>(cur.get()),
                                  sizeof(AuthData::Record));

                    size_t header_end_pos = _Backend.getPos();

                    if (!compute_next_rd(rd, end, cur->datasize, header_end_pos)) {
                        break;
                    }

                    if (cur->type == UserData && strcmp(cur->fieldname, "username") == 0) {
                        class User user;
                        std::shared_ptr<class UserData> udat =
                            std::make_shared<class UserData>(cur->ruid);
                        size_t upos = sizeof(AuthHeader);
                        user.info(_Backend, *udat, upos);

                        if (name == udat->getUsername()) {
                            if (constant_time_eq(pwsalt, udat->getPwHash())) {
                                const SessionData* sdat =
                                    AuthSession->addSession(_Backend, _Did.value, cur->ruid);

                                json_object* respid = json_object_new_object();
                                ::uuid::uuid sessid;
                                sdat->getSid(sessid);
                                json_object_object_add(respid, "sessionid",
                                                       json_object_new_string(sessid.c_str()));
                                json_object_array_add(response, respid);
                                return sdat;
                            }
                            return nullptr; // wrong password
                        }
                    }
                }
            }
        }
        return nullptr;
    }

    const ClientConnections* ClientLogin(json_object *request, json_object *response) {
        json_object* cmd = get_commands_array_or_throw(request);

        int fcount = json_object_array_length(cmd);
        for (int i = 0; i < fcount; ++i) {
            json_object_object_foreach(json_object_array_get_idx(cmd, i), key, val) {
                if (strcmp(key, "login") != 0) continue;

                std::string atype, name, pw, pwsalt;
                json_object_object_foreach(val, okey, oval) {
                    if (strcmp(okey, "type") == 0) atype = json_object_get_string(oval);
                    else if (strcmp(okey, "name") == 0) name = json_object_get_string(oval);
                    else if (strcmp(okey, "key")  == 0) pw   = json_object_get_string(oval);
                }

                if (atype != "client" || name.empty() || pw.empty()) continue;

                sha512 sha;
                sha.hash(pw, pwsalt);

                size_t rd  = sizeof(authdb::AuthHeader);
                size_t end = _Backend.end();

                while (rd + sizeof(AuthData::Record) <= end) {
                    std::shared_ptr<authdb::AuthData::Record> cur =
                        std::make_shared<authdb::AuthData::Record>();
                    _Backend.setPos(rd);

                    _Backend.read(reinterpret_cast<unsigned char*>(cur.get()),
                                  sizeof(AuthData::Record));
                    size_t header_end_pos = _Backend.getPos();

                    if (!compute_next_rd(rd, end, cur->datasize, header_end_pos)) {
                        break;
                    }

                    if (cur->type == ClientData && strcmp(cur->fieldname, "clientname") == 0) {
                        class ClientData cdat(cur->ruid);
                        size_t upos = sizeof(AuthHeader);
                        AuthClient.info(_Backend, cdat, upos);

                        if (name == cdat.getName()) {
                            if (constant_time_eq(pwsalt, cdat.getKey())) {
                                ClientConnections* cid = AuthClient.addConnection(_Backend, cdat);
                                json_object* respid = json_object_new_object();
                                uuid::uuid coid = cid->getId();
                                json_object_object_add(respid, "connectionid",
                                                       json_object_new_string(coid.c_str()));
                                json_object_array_add(response, respid);
                                return cid;
                            }
                            return nullptr; // wrong secret
                        }
                    }
                }
            }
        }
        return nullptr;
    }

    void Connection(json_object *request, json_object *response) {
        json_object* cmd = get_commands_array_or_throw(request);

        int fcount = json_object_array_length(cmd);
        for (int i = 0; i < fcount; ++i) {
            json_object_object_foreach(json_object_array_get_idx(cmd, i), key, val) {
                if (strcmp(key, "addconnection") == 0) {
                    if (!_SessionData) continue; // require user session

                    std::string clientname, clientsecret;
                    json_object_object_foreach(val, okey, oval) {
                        if (strcmp(okey, "clientname") == 0) {
                            clientname = json_object_get_string(oval);
                        } else if (strcmp(okey, "clientsecret") == 0) {
                            clientsecret = json_object_get_string(oval);
                        }
                    }
                    if (!clientname.empty() && !clientsecret.empty()) {
                        ::uuid::uuid gpid;
                        gpid.generate();

                        class ClientData cdat(gpid);
                        cdat.setName(clientname.c_str());

                        std::string hash;
                        sha512 sha;
                        sha.hash(clientsecret, hash);
                        cdat.setKey(hash.c_str());

                        /*
                         * Compute Kerberos long-term key for machine account.
                         * Salt = REALM + "host/" + clientname.
                         */
                        {
                            if (g_KdcPlugin) {
                                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(
                                    clientsecret, salt, KDC_ETYPE_AES256_CTS_HMAC_SHA1);
                                cdat.setKrbKey(hexEncode(krbKeyBytes));
                            }
                        }

                        class Client clt;
                        clt.create(_Backend, &cdat);

                        json_object* respid = json_object_new_object();
                        json_object_object_add(respid, "addclient",
                                               json_object_new_string(gpid.c_str()));
                        json_object_array_add(response, respid);
                    }
                } else if (strcmp(key, "remconnection") == 0) {
                    if (!_SessionData) continue;

                    ::uuid::uuid clientid;
                    json_object_object_foreach(val, okey, oval) {
                        if (strcmp(okey, "clientid") == 0) {
                            clientid.parse(json_object_get_string(oval));
                        }
                    }
                    if (!clientid.empty()) {
                        class Client clt;
                        clt.remove(_Backend, clientid);

                        json_object* respid = json_object_new_object();
                        json_object_object_add(respid, "remclient",
                                               json_object_new_string(clientid.c_str()));
                        json_object_array_add(response, respid);
                    }
                } else if (strcmp(key, "editconnection") == 0) {
                    if (!_SessionData) continue;

                    ::uuid::uuid clientid;
                    std::string name, secret;
                    json_object_object_foreach(val, okey, oval) {
                        if (strcmp(okey, "clientid") == 0) {
                            clientid.parse(json_object_get_string(oval));
                        } else if (strcmp(okey, "clientname") == 0) {
                            name = json_object_get_string(oval);
                        } else if (strcmp(okey, "clientsecret") == 0) {
                            secret = json_object_get_string(oval);
                        }
                    }

                    if (!clientid.empty() && (!name.empty() || !secret.empty())) {
                        class Client clt;
                        class ClientData cdat(clientid);

                        size_t pos = sizeof(AuthHeader);
                        clt.info(_Backend, cdat, pos);

                        if (!name.empty())   cdat.setName(name.c_str());
                        if (!secret.empty()) {
                            std::string hash;
                            sha512 sha;
                            sha.hash(secret, hash);     // ensure hashing on edit as well
                            cdat.setKey(hash.c_str());

                            /*
                             * Recompute Kerberos long-term key for machine account.
                             */
                            {
                                std::string cname = cdat.getName() ? cdat.getName() : "";
                                if (!cname.empty() && g_KdcPlugin) {
                                    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/" + cname;
                                    auto krbKeyBytes = g_KdcPlugin->string2key(
                                        secret, salt, KDC_ETYPE_AES256_CTS_HMAC_SHA1);
                                    cdat.setKrbKey(hexEncode(krbKeyBytes));
                                }
                            }
                        }

                        // Prefer an in-place edit; if not available, recreate while preserving UUID
                        clt.remove(_Backend, clientid);
                        clt.create(_Backend, &cdat);

                        json_object* respid = json_object_new_object();
                        json_object_object_add(respid, "editclient",
                                               json_object_new_string(clientid.c_str()));
                        json_object_array_add(response, respid);
                    }
                }
            }
        }
    }

    void SessionInfo(json_object *request, json_object *response) {
        json_object* cmd = get_commands_array_or_throw(request);

        int fcount = json_object_array_length(cmd);
        for (int i = 0; i < fcount; ++i) {
            json_object_object_foreach(json_object_array_get_idx(cmd, i), key, val) {
                if (strcmp("sessioninfo", key) == 0) {
                    if (!_SessionData) continue;

                    ::uuid::uuid tmp;
                    json_object* respid = json_object_new_object();

                    json_object_object_add(respid, "username",
                                           json_object_new_string(_SessionData->getUsername()));
                    _SessionData->getUid(tmp);
                    json_object_object_add(respid, "userid",
                                           json_object_new_string(tmp.c_str()));
                    _SessionData->getSid(tmp);
                    json_object_object_add(respid, "sessionid",
                                           json_object_new_string(tmp.c_str()));
                    _SessionData->getDid(tmp);
                    json_object_object_add(respid, "domainid",
                                           json_object_new_string(tmp.c_str()));

                    json_object* sinfo = json_object_new_object();
                    json_object_object_add(sinfo, "sessioninfo", respid);
                    json_object_array_add(response, sinfo);
                }
            }
        }
    }

    void remSession(json_object *request, json_object *response) {
        json_object* cmd = get_commands_array_or_throw(request);

        int fcount = json_object_array_length(cmd);
        for (int i = 0; i < fcount; ++i) {
            json_object_object_foreach(json_object_array_get_idx(cmd, i), key, val) {
                if (strcmp(key, "remsession") == 0) {
                    if (!_SessionData) continue;

                    ::uuid::uuid tmp, csid;
                    tmp.parse(json_object_get_string(val));
                    _SessionData->getSid(csid);

                    AuthSession->remSession(tmp);
                    if (tmp == csid) _SessionData = nullptr;

                    json_object* slogoff = json_object_new_object();
                    json_object_object_add(slogoff, "remsession",
                                           json_object_new_string(tmp.c_str()));
                    json_object_array_add(response, slogoff);
                    return;
                }
            }
        }
    }

    void changeUserPw(json_object *request, json_object *response) {
        json_object* cmd = get_commands_array_or_throw(request);

        int fcount = json_object_array_length(cmd);
        for (int i = 0; i < fcount; ++i) {
            json_object_object_foreach(json_object_array_get_idx(cmd, i), key, val) {
                if (strcmp(key, "changeuserpw") == 0) {
                    if (!_SessionData) continue;

                    if (!_SessionData->checkGPO("a1b2c3d4-452e-11f0-b1cc-1a2b3c4d5e01")) {
                        json_object* out = json_object_new_object();
                        json_object_object_add(out, "changeuserpw", json_object_new_boolean(false));
                        json_object_array_add(response, out);
                        return;
                    }

                    std::string oldpw, newpw;
                    json_object_object_foreach(val, okey, oval) {
                        if (strcmp(okey, "oldpw") == 0) oldpw = json_object_get_string(oval);
                        else if (strcmp(okey, "newpw") == 0) newpw = json_object_get_string(oval);
                    }

                    std::string oldhash, newhash;
                    sha512 sha;  // NOTE: consider Argon2id with per-user salt
                    sha.hash(oldpw, oldhash);
                    sha.hash(newpw, newhash);

                    ::uuid::uuid uid;
                    _SessionData->getUid(uid);

                    class UserData udat(uid.value);
                    User user;
                    size_t ustart = sizeof(AuthHeader);
                    user.info(_Backend, udat, ustart);

                    bool ok = true;
                    if (constant_time_eq(newhash, oldhash)) ok = false;
                    if (!constant_time_eq(oldhash, udat.getPwHash())) ok = false;

                    json_object* out = json_object_new_object();
                    if (!ok) {
                        json_object_object_add(out, "changeuserpw", json_object_new_boolean(false));
                        json_object_array_add(response, out);
                        return;
                    }

                    udat.setPwHash(newhash.c_str());

                    /*
                     * Recompute Kerberos long-term key from raw new password.
                     * Salt = REALM + username, REALM = upper-case domain name.
                     */
                    {
                        std::string realm = _Backend.getDomain();
                        std::transform(realm.begin(), realm.end(), realm.begin(),
                                       [](unsigned char c){ return std::toupper(c); });
                        std::string uname = udat.getUsername();
                        if (!uname.empty() && g_KdcPlugin) {
                            std::string salt = realm + uname;
                            auto krbKeyBytes = g_KdcPlugin->string2key(
                                newpw, salt, KDC_ETYPE_AES256_CTS_HMAC_SHA1);
                            udat.setKrbKey(hexEncode(krbKeyBytes));
                        }
                    }

                    ustart = sizeof(AuthHeader);
                    user.edit(_Backend, udat, ustart);

                    json_object_object_add(out, "changeuserpw", json_object_new_boolean(true));
                    json_object_array_add(response, out);
                    return;
                }
            }
        }
    }

    void UserInfo(json_object *request, json_object *response) {
        json_object* cmd = get_commands_array_or_throw(request);

        int fcount = json_object_array_length(cmd);
        for (int i = 0; i < fcount; ++i) {
            json_object_object_foreach(json_object_array_get_idx(cmd, i), key, val) {
                (void)val;
                if (strcmp(key, "userinfo") == 0) {
                    if (!_SessionData) continue;

                    ::uuid::uuid uid;
                    _SessionData->getUid(uid);

                    class UserData udat(uid.value);
                    User user;
                    size_t ustart = sizeof(AuthHeader);
                    user.info(_Backend, udat, ustart);

                    json_object* respid = json_object_new_object();
                    json_object_object_add(respid, "username",
                                           json_object_new_string(udat.getUsername().c_str()));
                    json_object_object_add(respid, "firstname",
                                           json_object_new_string(udat.getFirstname().c_str()));
                    json_object_object_add(respid, "lastname",
                                           json_object_new_string(udat.getLastname().c_str()));
                    json_object_object_add(respid, "mail",
                                           json_object_new_string(udat.getMail().c_str()));
                    json_object_object_add(respid, "userid",
                                           json_object_new_string(uid.c_str()));

                    json_object* out = json_object_new_object();
                    json_object_object_add(out, "userinfo", respid);
                    json_object_array_add(response, out);
                    return;
                }
            }
        }
    }

    void listGroups(json_object *request, json_object *response) {
        json_object* cmd = get_commands_array_or_throw(request);

        int fcount = json_object_array_length(cmd);
        for (int i = 0; i < fcount; ++i) {
            json_object_object_foreach(json_object_array_get_idx(cmd, i), key, val) {
                (void)val;
                if (strcmp(key, "listgroups") == 0) {
                    json_object* out = json_object_new_object();
                    json_object* jarr = json_object_new_array();

                    Group group;
                    size_t rd = sizeof(authdb::AuthHeader), end = _Backend.end();

                    while (rd + sizeof(AuthData::Record) <= end) {
                        std::shared_ptr<authdb::AuthData::Record> cur = std::make_shared<authdb::AuthData::Record>();
                        _Backend.setPos(rd);
                        _Backend.read((unsigned char*)cur.get(), sizeof(AuthData::Record));
                        size_t next = _Backend.getPos() + cur->datasize;
                        if (next < _Backend.getPos() || next > end) break;
                        rd = next;

                        if (cur->type == GroupData && strcmp(cur->fieldname, "groupname") == 0) {
                            class GroupData gdat(cur->ruid);
                            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(uuid::uuid(cur->ruid).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(out, "groups", jarr);
                    json_object_array_add(response, out);
                }
            }
        }
    }

    Api(AuthBackend &admin, class ::uuid::uuid did, libhttppp::HttpRequest& curreq,
        const int /*tid*/, ULONG_PTR /*args*/)
        : _Backend(admin), _Did(did), _SessionData(nullptr) {

        libhttppp::HttpResponse rep;

        std::vector<char> recv;

        size_t bodyLen = curreq.getContentLength();
        if (bodyLen == 0)
            bodyLen = curreq.RecvData.size();

        if (bodyLen > 0 && bodyLen <= curreq.RecvData.size()) {
            std::copy(curreq.RecvData.begin(),
                      curreq.RecvData.begin() + bodyLen,
                      std::back_inserter(recv));
        } else {
            throw AuthBackendError("get no data input!");
        }

        recv.push_back('\0');

        json_tokener_error jerr;
        json_object *jrequest = nullptr, *jresponse = nullptr;

        if (!(jrequest = json_tokener_parse_verbose(recv.data(), &jerr))) {
            json_object *jerrArr = json_object_new_array();
            json_object *eobj = json_object_new_object();
            json_object_object_add(eobj,"error",json_object_new_string(json_tokener_error_desc(jerr)));
            json_object_array_add(jerrArr,eobj);
            const char *errstr = json_object_to_json_string(jerrArr);
            rep.setState(HTTP500);
            rep.setContentType("application/json");
            rep.send(curreq, errstr, strlen(errstr));
            json_object_put(jerrArr);
            return;
        }

        jresponse = json_object_new_array();

        const ClientConnections* curcon = nullptr;

        // Try to reuse existing client session; otherwise perform client login
        if (!(curcon = checkConnection(jrequest, jresponse))) {
            curcon = ClientLogin(jrequest, jresponse);
        }

        // Try to reuse existing user session; otherwise perform user login
        if (!(_SessionData = checkUser(jrequest, jresponse))) {
            _SessionData = userLogin(jrequest, jresponse);
        }

        // With client connection only
        if (curcon) {
            existsGPO(jrequest, jresponse);
            addGPO(jrequest, jresponse);
            listGroups(jrequest, jresponse);
        }

        // With user session only
        if (_SessionData) {
            Connection(jrequest, jresponse);
        }

        // With both client connection AND user session
        if (curcon && _SessionData) {
            changeUserPw(jrequest, jresponse);
            checkGPO(jrequest, jresponse);           
            SessionInfo(jrequest, jresponse);
            UserInfo(jrequest, jresponse);
            remSession(jrequest, jresponse);
        }

        size_t outsize = 0;
        const char* out = json_object_to_json_string_length(jresponse, 0, &outsize);

        rep.setState(HTTP200);
        rep.setContentType("application/json");
        rep.send(curreq, out, outsize);

        json_object_put(jresponse);
        json_object_put(jrequest);
    }

    ~Api() = default;

private:
    AuthBackend              &_Backend;
    ::uuid::uuid              _Did;
    const SessionData        *_SessionData;
};

} // namespace authdb

// Controller wrapper stays minimal and keeps the same behavior
authdb::ApiController::ApiController(AuthBackend &admin, class ::uuid::uuid did,
                                     libhttppp::HttpRequest &curreq,
                                     const int tid, ULONG_PTR args) {
    Api(admin, did, curreq, tid, args);
}

authdb::ApiController::~ApiController() = default;

