/*******************************************************************************
 * 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 DAMAGE.
 *******************************************************************************/

#include <iostream>
#include <vector>
#include <sstream>
#include <algorithm>
#include <cstring>
#include <memory>
#include <cctype>

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

#include <httppp/exception.h>
#include <netplus/exception.h>

#include "client.h"

namespace authdb {
namespace client {

// Static GPO cache members
int Client::_GpoCacheTTL = Client::DEFAULT_GPO_CACHE_TTL;
std::mutex Client::_GpoCacheMutex;
std::unordered_map<std::string, std::chrono::steady_clock::time_point> Client::_GpoCache;

void Client::setGpoCacheTTL(int seconds) {
    _GpoCacheTTL = seconds;
}

bool Client::checkGpoCache(const std::string& key) {
    std::lock_guard<std::mutex> lk(_GpoCacheMutex);
    auto it = _GpoCache.find(key);
    if (it == _GpoCache.end()) return false;
    if (std::chrono::steady_clock::now() > it->second) {
        _GpoCache.erase(it);
        return false;
    }
    return true;
}

void Client::cacheGpoResult(const std::string& key) {
    std::lock_guard<std::mutex> lk(_GpoCacheMutex);
    _GpoCache[key] = std::chrono::steady_clock::now()
                    + std::chrono::seconds(_GpoCacheTTL);
    if (_GpoCache.size() > 2000) {
        auto now = std::chrono::steady_clock::now();
        for (auto ci = _GpoCache.begin(); ci != _GpoCache.end(); ) {
            if (now > ci->second) ci = _GpoCache.erase(ci);
            else ++ci;
        }
    }
}

namespace {
    std::vector<char> safe_post(libhttppp::HttpClient& client, libhttppp::HttpRequest& req,
                                const std::vector<char>& body, size_t maxTries) {
        try {
            return client.Post(req, body, maxTries);
        } catch (libhttppp::HTTPException&) {
            client.reconnect();
            return client.Post(req, body, maxTries);
        } catch (netplus::NetException&) {
            client.reconnect();
            return client.Post(req, body, maxTries);
        }
    }

    std::vector<char> safe_get(libhttppp::HttpClient& client, libhttppp::HttpRequest& req,
                               size_t maxTries) {
        try {
            return client.Get(req, maxTries);
        } catch (libhttppp::HTTPException&) {
            client.reconnect();
            return client.Get(req, maxTries);
        } catch (netplus::NetException&) {
            client.reconnect();
            return client.Get(req, maxTries);
        }
    }

    std::string normalize_path(std::string path) {
        if (path.empty()) return "/";
        // Strip scheme/host if someone passes a full URL by mistake.
        if (path.rfind("http://", 0) == 0 || path.rfind("https://", 0) == 0) {
            size_t slash = path.find('/', path.find("//") + 2);
            path = (slash == std::string::npos) ? "/" : path.substr(slash);
        }

        // Remove query/fragment from the path only.
        size_t cut = path.find_first_of("?#");
        if (cut != std::string::npos) {
            path.erase(cut);
        }

        if (path.empty()) return "/";
        if (path.front() != '/') path.insert(path.begin(), '/');
        return path;
    }

    std::string extract_path_from_url(const std::string& url) {
        size_t start = 0;
        size_t scheme = url.find("://");
        if (scheme != std::string::npos) {
            start = scheme + 3;
        }

        size_t slash = url.find('/', start);
        if (slash == std::string::npos) {
            return "/";
        }
        return normalize_path(url.substr(slash));
    }

    std::string extract_location_from_message(const std::string& msg) {
        std::string lower;
        lower.reserve(msg.size());
        for (char ch : msg) {
            lower.push_back(static_cast<char>(std::tolower(static_cast<unsigned char>(ch))));
        }

        const std::string key = "location:";
        size_t pos = lower.find(key);
        if (pos == std::string::npos) return {};

        size_t val_start = pos + key.size();
        while (val_start < msg.size() && (msg[val_start] == ' ' || msg[val_start] == '\t')) {
            ++val_start;
        }

        size_t val_end = msg.find_first_of("\r\n", val_start);
        if (val_end == std::string::npos) val_end = msg.size();

        std::string location = msg.substr(val_start, val_end - val_start);
        // Trim trailing spaces/tabs
        while (!location.empty() && (location.back() == ' ' || location.back() == '\t')) {
            location.pop_back();
        }
        return location;
    }
}

// -------- ClientConnection --------

ClientConnection::ClientConnection() : _Client(nullptr) {}

ClientConnection::~ClientConnection() = default;

void ClientConnection::reConnect() {
    if (_Client) _Client->reconnect();
}

void ClientConnection::setUrl(const libhttppp::HttpUrl& url) {
    _Url = url;
    std::string path = normalize_path(_Url.getPath());
    if (path == "/") path = "/settings/admin/api";
    _RequestPath = path;
    if (_Client) {
        _Client = std::make_unique<libhttppp::HttpClient>(_Url);
        _Client->setTimeout(_Timeout);
    }
}

void ClientConnection::setUrl(const std::string& url) {
    _Url = libhttppp::HttpUrl(url.c_str());
    std::string path = _Url.getPath();
    if (path.empty() || path == "/") {
        path = extract_path_from_url(url);
    }
    path = normalize_path(path);
    if (path == "/") path = "/settings/admin/api";
    _RequestPath = path;
    if (_Client) {
        _Client = std::make_unique<libhttppp::HttpClient>(_Url);
        _Client->setTimeout(_Timeout);
    }
}

void ClientConnection::setClientName(const std::string& name) {
    _CltName = name;
}

void ClientConnection::setClientSecret(const std::string& secret) {
    _CltSecret = secret;
}

void ClientConnection::setUsername(const std::string& username) {
    _UserName = username;
}

void ClientConnection::setUserPassword(const std::string& password) {
    _UserPassword = password;
}

const uuid::uuid& ClientConnection::getClientId() { return _ClientId; }
const uuid::uuid& ClientConnection::getSessionID() { return _SessionID; }

void ClientConnection::setSessionID(const ::uuid::uuid& sid) { _SessionID = sid; }

void ClientConnection::setMaxTries(int maxtries) { _MaxTries = maxtries; }

void ClientConnection::setTimeout(int timeout_sec) { _Timeout = timeout_sec; }

// -------- Client --------

Client::Client(ClientConnection& clt) : _Con(clt) {
    if (!_Con._Client) {
        _Con._Client = std::make_unique<libhttppp::HttpClient>(_Con._Url);
    }
}

Client::~Client() = default;

inline void ensure_http_client(ClientConnection& con) {
    if (!con._Client) {
        con._Client = std::make_unique<libhttppp::HttpClient>(con._Url);
        con._Client->setTimeout(con._Timeout);
    }
}

bool Client::ClientAuth() {
    ensure_http_client(_Con);

    json_object* jresponse = json_object_new_object();
    json_object* cmd = json_object_new_array();
    json_object* login = json_object_new_object();
    json_object* userauth = json_object_new_object();

    json_object_object_add(userauth, "type", json_object_new_string("client"));
    json_object_object_add(userauth, "name", json_object_new_string(_Con._CltName.c_str()));
    json_object_object_add(userauth, "key", json_object_new_string(_Con._CltSecret.c_str()));

    json_object_object_add(login, "login", userauth);
    json_object_array_add(cmd, login);
    json_object_object_add(jresponse, "commands", cmd);

    size_t outsize = 0;
    const char* tmp = json_object_to_json_string_length(jresponse, 0, &outsize);
    std::vector<char> out;
    out.reserve(outsize);
    std::copy(tmp, tmp + outsize, std::back_inserter(out));

    libhttppp::HttpRequest nreq;
    nreq.setHeaderData("accept")->push_back("application/json");
    nreq.setHeaderData("content-type")->push_back("application/json");
    nreq.setRequestURL(_Con._RequestPath.c_str());
    std::vector<char> resp(safe_post(*_Con._Client, nreq, out, _Con._MaxTries));
    resp.push_back('\0');

    json_object_put(jresponse);

    json_tokener* jtok = json_tokener_new();
    json_object* jrequest = json_tokener_parse_ex(jtok, resp.data(), resp.size() - 1);

    if (!jrequest) {
        const char* err = json_tokener_error_desc(json_tokener_get_error(jtok));
        json_tokener_free(jtok);
        throw authdb::AuthBackendError(err);
    }

    if (json_object_get_type(jrequest) != json_type_array) {
        json_object_put(jrequest);
        json_tokener_free(jtok);
        throw(authdb::AuthBackendError("Invalid Api Commands"));
    }

    int fcount = json_object_array_length(jrequest);
    for (int i = 0; i < fcount; ++i) {
        json_object_object_foreach(json_object_array_get_idx(jrequest, i), key, val) {
            if (strcmp(key, "error") == 0) {
                std::string msg = json_object_get_string(val);
                json_object_put(jrequest);
                json_tokener_free(jtok);
                throw authdb::AuthBackendError(msg);
            }
            if (strcmp(key, "connectionid") == 0) {
                _Con._ClientId = json_object_get_string(val);
                json_object_put(jrequest);
                json_tokener_free(jtok);
                return !_Con._ClientId.empty();
            }
        }
    }

    json_object_put(jrequest);
    json_tokener_free(jtok);
    return false;
}

bool Client::UserAuth() {
    ensure_http_client(_Con);

    json_object* jresponse = json_object_new_object();
    json_object* cmd = json_object_new_array();
    json_object* login = json_object_new_object();
    json_object* userauth = json_object_new_object();

    json_object_object_add(userauth, "type", json_object_new_string("user"));
    json_object_object_add(userauth, "name", json_object_new_string(_Con._UserName.c_str()));
    json_object_object_add(userauth, "key", json_object_new_string(_Con._UserPassword.c_str()));

    json_object_object_add(login, "login", userauth);
    json_object_array_add(cmd, login);
    json_object_object_add(jresponse, "commands", cmd);

    size_t outsize = 0;
    const char* tmp = json_object_to_json_string_length(jresponse, 0, &outsize);
    std::vector<char> out;
    out.reserve(outsize);
    std::copy(tmp, tmp + outsize, std::back_inserter(out));

    libhttppp::HttpRequest nreq;
    nreq.setHeaderData("accept")->push_back("application/json");
    nreq.setHeaderData("content-type")->push_back("application/json");
    nreq.setRequestURL(_Con._RequestPath.c_str());
    std::vector<char> resp(safe_post(*_Con._Client, nreq, out, _Con._MaxTries));
    resp.push_back('\0');

    json_object_put(jresponse);

    json_tokener* jtok = json_tokener_new();
    json_object* jrequest = json_tokener_parse_ex(jtok, resp.data(), resp.size() - 1);

    if (!jrequest) {
        const char* err = json_tokener_error_desc(json_tokener_get_error(jtok));
        json_tokener_free(jtok);
        throw authdb::AuthBackendError(err);
    }

    if (json_object_get_type(jrequest) != json_type_array) {
        json_object_put(jrequest);
        json_tokener_free(jtok);
        throw(authdb::AuthBackendError("Invalid Api Commands"));
    }

    int fcount = json_object_array_length(jrequest);
    for (int i = 0; i < fcount; ++i) {
        json_object_object_foreach(json_object_array_get_idx(jrequest, i), key, val) {
            if (strcmp(key, "error") == 0) {
                std::string msg = json_object_get_string(val);
                json_object_put(jrequest);
                json_tokener_free(jtok);
                throw authdb::AuthBackendError(msg);
            }
            if (strcmp("sessionid", key) == 0) {
                _Con._SessionID = json_object_get_string(val);
                json_object_put(jrequest);
                json_tokener_free(jtok);
                return !_Con._SessionID.empty();
            }
        }
    }

    json_object_put(jrequest);
    json_tokener_free(jtok);
    return false;
}

void Client::SessionInfo(class SessionData& sdat) {
    ensure_http_client(_Con);

    json_object* jresponse = json_object_new_object();
    json_object* cmd = json_object_new_array();
    json_object* obj = json_object_new_object();
    json_object* sinfo = json_object_new_object();

    json_object_object_add(obj, "clientsession",
                           json_object_new_string(_Con.getClientId().c_str()));
    json_object_object_add(obj, "usersession",
                           json_object_new_string(_Con.getSessionID().c_str()));
    json_object_object_add(sinfo, "sessioninfo", json_object_new_string(""));

    json_object_array_add(cmd, obj);
    json_object_array_add(cmd, sinfo);
    json_object_object_add(jresponse, "commands", cmd);

    size_t outsize = 0;
    const char* tmp = json_object_to_json_string_length(jresponse, 0, &outsize);
    std::vector<char> out;
    out.reserve(outsize);
    std::copy(tmp, tmp + outsize, std::back_inserter(out));

    libhttppp::HttpRequest nreq;
    nreq.setHeaderData("accept")->push_back("application/json");
    nreq.setHeaderData("content-type")->push_back("application/json");
    nreq.setRequestURL(_Con._RequestPath.c_str());
    std::vector<char> resp(safe_post(*_Con._Client, nreq, out, _Con._MaxTries));
    resp.push_back('\0');

    json_object_put(jresponse);

    json_tokener* jtok = json_tokener_new();
    json_object* jrequest = json_tokener_parse_ex(jtok, resp.data(), resp.size() - 1);

    if (!jrequest) {
        const char* err = json_tokener_error_desc(json_tokener_get_error(jtok));
        json_tokener_free(jtok);
        throw authdb::AuthBackendError(err);
    }

    if (json_object_get_type(jrequest) != json_type_array) {
        json_object_put(jrequest);
        json_tokener_free(jtok);
        throw(authdb::AuthBackendError("Invalid Api Commands"));
    }

    sdat.clear();

    int fcount = json_object_array_length(jrequest);
    for (int i = 0; i < fcount; ++i) {
        json_object_object_foreach(json_object_array_get_idx(jrequest, i), key, val) {
            if (strcmp(key, "error") == 0) {
                std::string msg = json_object_get_string(val);
                json_object_put(jrequest);
                json_tokener_free(jtok);
                throw authdb::AuthBackendError(msg);
            }
            if (strcmp("sessioninfo", key) == 0) {
                sdat._sid      = json_object_get_string(json_object_object_get(val, "sessionid"));
                sdat._uid      = json_object_get_string(json_object_object_get(val, "userid"));
                sdat._username = json_object_get_string(json_object_object_get(val, "username"));
                sdat._did      = json_object_get_string(json_object_object_get(val, "domainid"));
            }
        }
    }
    json_object_put(jrequest);
    json_tokener_free(jtok);

    if (sdat._sid.empty()) {
        throw(authdb::AuthBackendError("Session not found !"));
    }
}

bool Client::remSession() {
    ensure_http_client(_Con);

    json_object* jresponse = json_object_new_object();
    json_object* cmd = json_object_new_array();
    json_object* obj = json_object_new_object();

    json_object_object_add(obj, "clientsession",
                           json_object_new_string(_Con.getClientId().c_str()));
    json_object_object_add(obj, "usersession",
                           json_object_new_string(_Con.getSessionID().c_str()));
    json_object_object_add(obj, "remsession",
                           json_object_new_string(_Con.getSessionID().c_str()));

    json_object_array_add(cmd, obj);
    json_object_object_add(jresponse, "commands", cmd);

    size_t outsize = 0;
    const char* tmp = json_object_to_json_string_length(jresponse, 0, &outsize);
    std::vector<char> out;
    out.reserve(outsize);
    std::copy(tmp, tmp + outsize, std::back_inserter(out));

    libhttppp::HttpRequest nreq;
    nreq.setHeaderData("accept")->push_back("application/json");
    nreq.setHeaderData("content-type")->push_back("application/json");
    nreq.setRequestURL(_Con._RequestPath.c_str());
    std::vector<char> resp(safe_post(*_Con._Client, nreq, out, _Con._MaxTries));
    resp.push_back('\0');

    json_object_put(jresponse);

    json_tokener* jtok = json_tokener_new();
    json_object* jrequest = json_tokener_parse_ex(jtok, resp.data(), resp.size() - 1);

    if (!jrequest) {
        const char* err = json_tokener_error_desc(json_tokener_get_error(jtok));
        json_tokener_free(jtok);
        throw authdb::AuthBackendError(err);
    }

    if (json_object_get_type(jrequest) != json_type_array) {
        json_object_put(jrequest);
        json_tokener_free(jtok);
        throw(authdb::AuthBackendError("Invalid Api Commands"));
    }

    bool ok = false;
    int fcount = json_object_array_length(jrequest);
    for (int i = 0; i < fcount; ++i) {
        json_object_object_foreach(json_object_array_get_idx(jrequest, i), key, val) {
            if (strcmp(key, "error") == 0) {
                std::string msg = json_object_get_string(val);
                json_object_put(jrequest);
                json_tokener_free(jtok);
                throw authdb::AuthBackendError(msg);
            }
            if (strcmp(key, "remsession") == 0) {
                const char* removed = json_object_get_string(val);
                ok = (removed && _Con.getSessionID() == removed);
            }
        }
    }

    json_object_put(jrequest);
    json_tokener_free(jtok);
    return ok;
}

void Client::changeUserPw(const std::string& oldpw, const std::string& newpw) {
    ensure_http_client(_Con);

    json_object* jresponse = json_object_new_object();
    json_object* cmd = json_object_new_array();
    json_object* obj = json_object_new_object();
    json_object* pchange = json_object_new_object();

    json_object_object_add(obj, "usersession",
                           json_object_new_string(_Con.getSessionID().c_str()));
    json_object_object_add(obj, "clientsession",
                           json_object_new_string(_Con.getClientId().c_str()));
    json_object_object_add(obj, "changeuserpw", pchange);

    json_object_object_add(pchange, "oldpw", json_object_new_string(oldpw.c_str()));
    json_object_object_add(pchange, "newpw", json_object_new_string(newpw.c_str()));

    json_object_array_add(cmd, obj);
    json_object_object_add(jresponse, "commands", cmd);

    size_t outsize = 0;
    const char* tmp = json_object_to_json_string_length(jresponse, 0, &outsize);
    std::vector<char> out;
    out.reserve(outsize);
    std::copy(tmp, tmp + outsize, std::back_inserter(out));

    libhttppp::HttpRequest nreq;
    nreq.setHeaderData("accept")->push_back("application/json");
    nreq.setHeaderData("content-type")->push_back("application/json");
    nreq.setRequestURL(_Con._RequestPath.c_str());
    (void)safe_post(*_Con._Client, nreq, out, _Con._MaxTries); // Optionally: parse response if you care

    json_object_put(jresponse);
}

void Client::UserInfo(class UserData& udat) {
    ensure_http_client(_Con);

    json_object* jresponse = json_object_new_object();
    json_object* cmd = json_object_new_array();
    json_object* obj = json_object_new_object();

    json_object_object_add(obj, "clientsession",
                           json_object_new_string(_Con.getClientId().c_str()));
    json_object_object_add(obj, "usersession",
                           json_object_new_string(_Con.getSessionID().c_str()));
    json_object_object_add(obj, "userinfo", json_object_new_string(""));

    json_object_array_add(cmd, obj);
    json_object_object_add(jresponse, "commands", cmd);

    size_t outsize = 0;
    const char* tmp = json_object_to_json_string_length(jresponse, 0, &outsize);
    std::vector<char> out;
    out.reserve(outsize);
    std::copy(tmp, tmp + outsize, std::back_inserter(out));

    libhttppp::HttpRequest nreq;
    nreq.setHeaderData("accept")->push_back("application/json");
    nreq.setHeaderData("content-type")->push_back("application/json");
    nreq.setRequestURL(_Con._RequestPath.c_str());
    std::vector<char> resp(safe_post(*_Con._Client, nreq, out, _Con._MaxTries));
    resp.push_back('\0');

    json_object_put(jresponse);

    json_tokener* jtok = json_tokener_new();
    json_object* jrequest = json_tokener_parse_ex(jtok, resp.data(), resp.size() - 1);

    if (!jrequest) {
        const char* err = json_tokener_error_desc(json_tokener_get_error(jtok));
        json_tokener_free(jtok);
        throw authdb::AuthBackendError(err);
    }

    if (json_object_get_type(jrequest) != json_type_array) {
        json_object_put(jrequest);
        json_tokener_free(jtok);
        throw(authdb::AuthBackendError("Invalid Api Commands"));
    }

    int fcount = json_object_array_length(jrequest);
    for (int i = 0; i < fcount; ++i) {
        json_object_object_foreach(json_object_array_get_idx(jrequest, i), key, val) {
            if (strcmp(key, "error") == 0) {
                std::string msg = json_object_get_string(val);
                json_object_put(jrequest);
                json_tokener_free(jtok);
                throw authdb::AuthBackendError(msg);
            }
            if (strcmp("userinfo", key) == 0) {
                json_object* jobj = nullptr;
                if (json_object_object_get_ex(val, "username", &jobj)) {
                    const char* v = json_object_get_string(jobj);
                    if (v) udat.setUserName(v);
                }
                if (json_object_object_get_ex(val, "firstname", &jobj)) {
                    const char* v = json_object_get_string(jobj);
                    if (v) udat.setFirstName(v);
                }
                if (json_object_object_get_ex(val, "lastname", &jobj)) {
                    const char* v = json_object_get_string(jobj);
                    if (v) udat.setLastName(v);
                }
                if (json_object_object_get_ex(val, "mail", &jobj)) {
                    const char* v = json_object_get_string(jobj);
                    if (v) udat.setMail(v);
                }
                json_object_put(jrequest);
                json_tokener_free(jtok);
                return;
            }
        }
    }

    json_object_put(jrequest);
    json_tokener_free(jtok);
    throw(authdb::AuthBackendError("UserInfo not found in response"));
}

void Client::getAvatar(const ::uuid::uuid& did, const ::uuid::uuid& uid,
                       libhttppp::HttpRequest& resq, libhttppp::HttpResponse& resp) {
    ensure_http_client(_Con);

    std::stringstream url;
    url << "/getavatar/" << did.c_str() << "/" << uid.c_str() << ".jpg";

    resq.setHeaderData("accept")->push_back("image/jpeg");
    resq.setRequestURL(url.str().c_str());
    std::vector<char> avatar = safe_get(*_Con._Client, resq, _Con._MaxTries);

    resp.setState(HTTP200);
    resp.setContentType("image/jpeg");
    resp.send(resq, avatar, avatar.size());
}

bool Client::GPOcheck(class ::uuid::uuid gpoid) {
    // Check cache first — avoid network round-trip
    std::string cache_key = std::string(_Con.getSessionID().c_str()) + ":" + gpoid.c_str();
    if (checkGpoCache(cache_key)) return true;

    ensure_http_client(_Con);

    json_object* jresponse = json_object_new_object();
    json_object* cmd = json_object_new_array();
    json_object* obj = json_object_new_object();

    json_object_object_add(obj, "usersession",
                           json_object_new_string(_Con.getSessionID().c_str()));
    json_object_object_add(obj, "clientsession",
                           json_object_new_string(_Con.getClientId().c_str()));
    json_object_object_add(obj, "checkgpo",
                           json_object_new_string(gpoid.c_str()));

    json_object_array_add(cmd, obj);
    json_object_object_add(jresponse, "commands", cmd);

    size_t outsize = 0;
    const char* tmp = json_object_to_json_string_length(jresponse, 0, &outsize);
    std::vector<char> out;
    out.reserve(outsize);
    std::copy(tmp, tmp + outsize, std::back_inserter(out));

    libhttppp::HttpRequest nreq;
    nreq.setHeaderData("accept")->push_back("application/json");
    nreq.setHeaderData("content-type")->push_back("application/json");
    nreq.setRequestURL(_Con._RequestPath.c_str());
    std::vector<char> resp(safe_post(*_Con._Client, nreq, out, _Con._MaxTries));
    resp.push_back('\0');

    json_object_put(jresponse);

    json_tokener* jtok = json_tokener_new();
    json_object* jrequest = json_tokener_parse_ex(jtok, resp.data(), resp.size() - 1);

    if (!jrequest) {
        const char* err = json_tokener_error_desc(json_tokener_get_error(jtok));
        json_tokener_free(jtok);
        throw authdb::AuthBackendError(err);
    }

    if (json_object_get_type(jrequest) != json_type_array) {
        json_object_put(jrequest);
        json_tokener_free(jtok);
        throw(authdb::AuthBackendError("Invalid Api Commands"));
    }

    bool ret = false;
    int fcount = json_object_array_length(jrequest);
    for (int i = 0; i < fcount; ++i) {
        json_object_object_foreach(json_object_array_get_idx(jrequest, i), key, val) {
            if (strcmp(key, "error") == 0) {
                std::string msg = json_object_get_string(val);
                json_object_put(jrequest);
                json_tokener_free(jtok);
                throw authdb::AuthBackendError(msg);
            }
            if (strcmp(key, "gpo") == 0) {
                json_object* v = json_object_object_get(val, gpoid.c_str());
                if (v) ret = json_object_get_boolean(v);
                break;
            }
        }
    }
    json_object_put(jrequest);
    json_tokener_free(jtok);
    if (ret) cacheGpoResult(cache_key);
    return ret;
}

std::string Client::addConnection(const std::string& clientname, const std::string& cltsecret) {
    ensure_http_client(_Con);

    json_object* jresponse = json_object_new_object();
    json_object* cmd = json_object_new_array();
    json_object* obj = json_object_new_object();
    json_object* addclt = json_object_new_object();

    json_object_object_add(obj, "usersession",
                           json_object_new_string(_Con.getSessionID().c_str()));
    json_object_object_add(obj, "addconnection", addclt);

    json_object_object_add(addclt, "clientname", json_object_new_string(clientname.c_str()));
    json_object_object_add(addclt, "clientsecret", json_object_new_string(cltsecret.c_str()));

    json_object_array_add(cmd, obj);
    json_object_object_add(jresponse, "commands", cmd);

    size_t outsize = 0;
    const char* tmp = json_object_to_json_string_length(jresponse, 0, &outsize);
    std::vector<char> out;
    out.reserve(outsize);
    std::copy(tmp, tmp + outsize, std::back_inserter(out));

    libhttppp::HttpRequest nreq;
    nreq.setHeaderData("accept")->push_back("application/json");
    nreq.setHeaderData("content-type")->push_back("application/json");
    nreq.setRequestURL(_Con._RequestPath.c_str());
    std::vector<char> resp(safe_post(*_Con._Client, nreq, out, _Con._MaxTries));
    resp.push_back('\0');

    json_object_put(jresponse);

    json_tokener* jtok = json_tokener_new();
    json_object* jrequest = json_tokener_parse_ex(jtok, resp.data(), resp.size() - 1);

    std::string result;

    if (jrequest && json_object_get_type(jrequest) == json_type_array) {
        int fcount = json_object_array_length(jrequest);
        for (int i = 0; i < fcount; ++i) {
            json_object_object_foreach(json_object_array_get_idx(jrequest, i), key, val) {
                if (strcmp(key, "error") == 0) {
                    std::string msg = json_object_get_string(val);
                    json_object_put(jrequest);
                    json_tokener_free(jtok);
                    throw authdb::AuthBackendError(msg);
                }
                if (strcmp(key, "addclient") == 0) {
                    result = json_object_get_string(val);
                }
            }
        }
    }

    if (jrequest) json_object_put(jrequest);
    json_tokener_free(jtok);

    if (result.empty()) {
        throw authdb::AuthBackendError("addConnection failed: no client id returned");
    }

    return result;
}

void Client::remConnection(const uuid::uuid& clientid) {
    ensure_http_client(_Con);

    json_object* jresponse = json_object_new_object();
    json_object* cmd = json_object_new_array();
    json_object* obj = json_object_new_object();
    json_object* remclt = json_object_new_object();

    json_object_object_add(obj, "usersession",
                           json_object_new_string(_Con.getSessionID().c_str()));
    json_object_object_add(obj, "remconnection", remclt);

    json_object_object_add(remclt, "clientid", json_object_new_string(clientid.c_str()));

    json_object_array_add(cmd, obj);
    json_object_object_add(jresponse, "commands", cmd);

    size_t outsize = 0;
    const char* tmp = json_object_to_json_string_length(jresponse, 0, &outsize);
    std::vector<char> out;
    out.reserve(outsize);
    std::copy(tmp, tmp + outsize, std::back_inserter(out));

    libhttppp::HttpRequest nreq;
    nreq.setHeaderData("accept")->push_back("application/json");
    nreq.setHeaderData("content-type")->push_back("application/json");
    nreq.setRequestURL(_Con._RequestPath.c_str());
    (void)safe_post(*_Con._Client, nreq, out, _Con._MaxTries);

    json_object_put(jresponse);
}

void Client::editConnection(const uuid::uuid& clientid,
                            const std::string& clientname,
                            const std::string& cltsecret) {
    ensure_http_client(_Con);

    json_object* jresponse = json_object_new_object();
    json_object* cmd = json_object_new_array();
    json_object* obj = json_object_new_object();
    json_object* editclt = json_object_new_object();

    json_object_object_add(obj, "usersession",
                           json_object_new_string(_Con.getSessionID().c_str()));
    json_object_object_add(obj, "editconnection", editclt);

    json_object_object_add(editclt, "clientid", json_object_new_string(clientid.c_str()));
    json_object_object_add(editclt, "clientname", json_object_new_string(clientname.c_str()));
    json_object_object_add(editclt, "clientsecret", json_object_new_string(cltsecret.c_str()));

    json_object_array_add(cmd, obj);
    json_object_object_add(jresponse, "commands", cmd);

    size_t outsize = 0;
    const char* tmp = json_object_to_json_string_length(jresponse, 0, &outsize);
    std::vector<char> out;
    out.reserve(outsize);
    std::copy(tmp, tmp + outsize, std::back_inserter(out));

    libhttppp::HttpRequest nreq;
    nreq.setHeaderData("accept")->push_back("application/json");
    nreq.setHeaderData("content-type")->push_back("application/json");
    nreq.setRequestURL(_Con._RequestPath.c_str());
    (void)safe_post(*_Con._Client, nreq, out, _Con._MaxTries);

    json_object_put(jresponse);
}

std::string Client::GPOadd(const uuid::uuid& gpoid, const std::string& name, const std::string& desc,
                           bool gpodefault, const std::vector<std::string>& members) {
    ensure_http_client(_Con);

    json_object* jresponse = json_object_new_object();
    json_object* cmd = json_object_new_array();
    json_object* obj = json_object_new_object();
    json_object* gpoadd = json_object_new_object();

    json_object_object_add(obj, "clientsession",
                           json_object_new_string(_Con.getClientId().c_str()));

    json_object_object_add(gpoadd, "gpoid", json_object_new_string(gpoid.c_str()));
    json_object_object_add(gpoadd, "gponame", json_object_new_string(name.c_str()));
    json_object_object_add(gpoadd, "gpodesc", json_object_new_string(desc.c_str()));
    json_object_object_add(gpoadd, "gpodefault", json_object_new_boolean(gpodefault));

    json_object* jmembers = json_object_new_array();
    for (const std::string& m : members) {
        json_object_array_add(jmembers, json_object_new_string(m.c_str()));
    }
    json_object_object_add(gpoadd, "gpomembers", jmembers);

    json_object_object_add(obj, "gpoadd", gpoadd);

    json_object_array_add(cmd, obj);
    json_object_object_add(jresponse, "commands", cmd);

    size_t outsize = 0;
    const char* tmp = json_object_to_json_string_length(jresponse, 0, &outsize);
    std::vector<char> out;
    out.reserve(outsize);
    std::copy(tmp, tmp + outsize, std::back_inserter(out));

    libhttppp::HttpRequest nreq;
    nreq.setHeaderData("accept")->push_back("application/json");
    nreq.setHeaderData("content-type")->push_back("application/json");
    nreq.setRequestURL(_Con._RequestPath.c_str());
    std::vector<char> resp(safe_post(*_Con._Client, nreq, out, _Con._MaxTries));
    resp.push_back('\0');

    json_object_put(jresponse);

    json_tokener* jtok = json_tokener_new();
    json_object* jrequest = json_tokener_parse_ex(jtok, resp.data(), resp.size() - 1);

    if (!jrequest) {
        const char* err = json_tokener_error_desc(json_tokener_get_error(jtok));
        json_tokener_free(jtok);
        throw authdb::AuthBackendError(err);
    }

    if (json_object_get_type(jrequest) != json_type_array) {
        json_object_put(jrequest);
        json_tokener_free(jtok);
        throw(authdb::AuthBackendError("Invalid Api Commands"));
    }

    std::string result;
    int fcount = json_object_array_length(jrequest);
    for (int i = 0; i < fcount; ++i) {
        json_object_object_foreach(json_object_array_get_idx(jrequest, i), key, val) {
            if (strcmp(key, "error") == 0) {
                std::string msg = json_object_get_string(val);
                json_object_put(jrequest);
                json_tokener_free(jtok);
                throw authdb::AuthBackendError(msg);
            }
            if (strcmp(key, "gpoadd") == 0) {
                json_object_object_foreach(val, gpokey, gpoval) {
                    if (json_object_get_boolean(gpoval)) {
                        result = gpokey;
                    }
                }
            }
        }
    }

    json_object_put(jrequest);
    json_tokener_free(jtok);

    if (result.empty()) {
        throw authdb::AuthBackendError("addGPO failed: no gpo id returned");
    }

    return result;
}

bool Client::GPOexits(class uuid::uuid gpoid) { // keeping name for compatibility
    ensure_http_client(_Con);

    json_object* jresponse = json_object_new_object();
    json_object* cmd = json_object_new_array();
    json_object* obj = json_object_new_object();

    json_object_object_add(obj, "clientsession",
                           json_object_new_string(_Con.getClientId().c_str()));
    json_object_object_add(obj, "gpoexists",
                           json_object_new_string(gpoid.c_str()));

    json_object_array_add(cmd, obj);
    json_object_object_add(jresponse, "commands", cmd);

    size_t outsize = 0;
    const char* tmp = json_object_to_json_string_length(jresponse, 0, &outsize);
    std::vector<char> out;
    out.reserve(outsize);
    std::copy(tmp, tmp + outsize, std::back_inserter(out));

    libhttppp::HttpRequest nreq;
    nreq.setHeaderData("accept")->push_back("application/json");
    nreq.setHeaderData("content-type")->push_back("application/json");
    nreq.setRequestURL(_Con._RequestPath.c_str());
    std::vector<char> resp(safe_post(*_Con._Client, nreq, out, _Con._MaxTries));
    resp.push_back('\0');

    json_object_put(jresponse);

    json_tokener* jtok = json_tokener_new();
    json_object* jrequest = json_tokener_parse_ex(jtok, resp.data(), resp.size() - 1);

    if (!jrequest) {
        const char* err = json_tokener_error_desc(json_tokener_get_error(jtok));
        json_tokener_free(jtok);
        throw authdb::AuthBackendError(err);
    }

    if (json_object_get_type(jrequest) != json_type_array) {
        json_object_put(jrequest);
        json_tokener_free(jtok);
        throw(authdb::AuthBackendError("Invalid Api Commands"));
    }

    bool ret = false;
    int fcount = json_object_array_length(jrequest);
    for (int i = 0; i < fcount; ++i) {
        json_object_object_foreach(json_object_array_get_idx(jrequest, i), key, val) {
            if (strcmp(key, "error") == 0) {
                std::string msg = json_object_get_string(val);
                json_object_put(jrequest);
                json_tokener_free(jtok);
                throw authdb::AuthBackendError(msg);
            }
            if (strcmp(key, "gpoexists") == 0) {
                json_object* v = json_object_object_get(val, gpoid.c_str());
                if (v) ret = json_object_get_boolean(v);
                break;
            }
        }
    }
    json_object_put(jrequest);
    json_tokener_free(jtok);
    return ret;
}

std::vector<Client::GroupInfo> Client::listGroups() {
    ensure_http_client(_Con);

    json_object* jresponse = json_object_new_object();
    json_object* cmd = json_object_new_array();
    json_object* obj = json_object_new_object();

    json_object_object_add(obj, "clientsession",
                           json_object_new_string(_Con.getClientId().c_str()));
    json_object_object_add(obj, "listgroups",
                           json_object_new_string(""));

    json_object_array_add(cmd, obj);
    json_object_object_add(jresponse, "commands", cmd);

    size_t outsize = 0;
    const char* tmp = json_object_to_json_string_length(jresponse, 0, &outsize);
    std::vector<char> out;
    out.reserve(outsize);
    std::copy(tmp, tmp + outsize, std::back_inserter(out));

    libhttppp::HttpRequest nreq;
    nreq.setHeaderData("accept")->push_back("application/json");
    nreq.setHeaderData("content-type")->push_back("application/json");
    nreq.setRequestURL(_Con._RequestPath.c_str());
    std::vector<char> resp(safe_post(*_Con._Client, nreq, out, _Con._MaxTries));
    resp.push_back('\0');

    json_object_put(jresponse);

    json_tokener* jtok = json_tokener_new();
    json_object* jrequest = json_tokener_parse_ex(jtok, resp.data(), resp.size() - 1);

    if (!jrequest) {
        const char* err = json_tokener_error_desc(json_tokener_get_error(jtok));
        json_tokener_free(jtok);
        throw authdb::AuthBackendError(err);
    }

    if (json_object_get_type(jrequest) != json_type_array) {
        json_object_put(jrequest);
        json_tokener_free(jtok);
        throw(authdb::AuthBackendError("Invalid Api Commands"));
    }

    std::vector<GroupInfo> groups;
    int fcount = json_object_array_length(jrequest);
    for (int i = 0; i < fcount; ++i) {
        json_object_object_foreach(json_object_array_get_idx(jrequest, i), key, val) {
            if (strcmp(key, "error") == 0) {
                std::string msg = json_object_get_string(val);
                json_object_put(jrequest);
                json_tokener_free(jtok);
                throw authdb::AuthBackendError(msg);
            }
            if (strcmp(key, "groups") == 0 && json_object_get_type(val) == json_type_array) {
                int glen = json_object_array_length(val);
                for (int g = 0; g < glen; ++g) {
                    json_object* jgrp = json_object_array_get_idx(val, g);
                    json_object* jgid  = json_object_object_get(jgrp, "gid");
                    json_object* jname = json_object_object_get(jgrp, "name");
                    if (jgid && jname) {
                        groups.push_back({json_object_get_string(jgid),
                                          json_object_get_string(jname)});
                    }
                }
            }
        }
    }

    json_object_put(jrequest);
    json_tokener_free(jtok);
    return groups;
}

// -------- PooledConnection --------

PooledConnection::PooledConnection(ConnectionPool* pool,
                                   std::unique_ptr<ClientConnection> con,
                                   std::unique_ptr<Client> clt)
    : _Pool(pool), _Con(std::move(con)), _Clt(std::move(clt)) {}

PooledConnection::PooledConnection(PooledConnection&& other) noexcept
    : _Pool(other._Pool), _Con(std::move(other._Con)), _Clt(std::move(other._Clt)) {
    other._Pool = nullptr;
}

PooledConnection& PooledConnection::operator=(PooledConnection&& other) noexcept {
    if (this != &other) {
        if (_Pool && _Con && _Clt)
            _Pool->release(std::move(_Con), std::move(_Clt));
        _Pool = other._Pool;
        _Con = std::move(other._Con);
        _Clt = std::move(other._Clt);
        other._Pool = nullptr;
    }
    return *this;
}

PooledConnection::~PooledConnection() {
    if (_Pool && _Con && _Clt)
        _Pool->release(std::move(_Con), std::move(_Clt));
}

ClientConnection& PooledConnection::connection() { return *_Con; }
Client& PooledConnection::client() { return *_Clt; }

void PooledConnection::setSessionID(const uuid::uuid& sid) {
    _Con->setSessionID(sid);
}

// -------- ConnectionPool --------

ConnectionPool::ConnectionPool(const std::string& url,
                               const std::string& client_name,
                               const std::string& client_secret,
                               std::size_t max_idle)
    : _Url(url), _CltName(client_name), _CltSecret(client_secret), _MaxIdle(max_idle) {}

ConnectionPool::~ConnectionPool() = default;

PooledConnection ConnectionPool::acquire() {
    {
        std::lock_guard<std::mutex> lk(_Mutex);
        if (!_Idle.empty()) {
            auto entry = std::move(_Idle.front());
            _Idle.pop_front();
            return PooledConnection(this, std::move(entry.con), std::move(entry.clt));
        }
    }
    // Create new connection
    auto con = std::make_unique<ClientConnection>();
    con->setUrl(_Url);
    con->setClientName(_CltName);
    con->setClientSecret(_CltSecret);
    auto clt = std::make_unique<Client>(*con);
    return PooledConnection(this, std::move(con), std::move(clt));
}

void ConnectionPool::release(std::unique_ptr<ClientConnection> con,
                             std::unique_ptr<Client> clt) {
    std::lock_guard<std::mutex> lk(_Mutex);
    if (_Idle.size() < _MaxIdle) {
        _Idle.push_back({std::move(con), std::move(clt)});
    }
    // else: drop — exceeds max idle, connection closes
}

} // namespace client
} // namespace authdb
