/*******************************************************************************
 * 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 <iostream>
#include <cstring>
#include <mutex>
#include <shared_mutex>
#include <fstream>
#include <filesystem>
#include <thread>
#include <map>
#include <memory>
#include <atomic>
#include <condition_variable>
#include <chrono>

#include <cmdplus/cmdplus.h>
#include <confplus/conf.h>

#include <netplus/socket.h>

#include <httppp/httpd.h>
#include <htmlpp/html.h>
#include <htmlpp/exception.h>

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

#include "favicon.h"

#include <json-c/json.h>
#include <netplus/exception.h>

#ifndef _WIN32
#include <unistd.h>
#include <pwd.h>
#include <grp.h>
#include <sys/stat.h>
#include <fcntl.h>
#endif

namespace authdb {

    AdPlugin       *g_AdPlugin       = nullptr;
    PluginManager  *g_PluginManager   = nullptr;

    class Client   AuthClient;
    Session        *AuthSession = nullptr;

    std::mutex g_wizzard;

    bool initalized=true;
    bool DEBUG=false;



    class AuthDB : public libhttppp::HttpEvent{
    public:
        AuthDB(authdb::AuthBackend &backend,std::vector<netplus::socket*> ssock) :
            HttpEvent(ssock),
            _AdminBackend(backend),
            _AdminInterface(backend,ssock){
            std::ifstream authfs(GETDATA(authdb.js), std::ios::ate);
            size_t asize = authfs.tellg();
            _AuthJs.resize(asize);
            authfs.seekg(0);
            authfs.read(_AuthJs.data(),asize);

            std::ifstream cssfs(GETDATA(style.css), std::ios::ate);
            size_t csssize = cssfs.tellg();
            _AuthCss.resize(csssize);
            cssfs.seekg(0);
            cssfs.read(_AuthCss.data(),csssize);

            _AdminInterface.setDomainResolver([this](const char *cdid) -> AuthBackend* {
                uuid::uuid did;
                if (did.parse(cdid) != 0)
                    return nullptr;
                return getDomainBackend(did);
            });

            _AdminInterface.setDomainCacheEvictor([this](const char *cdid) {
                uuid::uuid did;
                if (did.parse(cdid) != 0)
                    return;
                evictDomainCache(did);
            });
        };

        void WizzardEvent(libhttppp::HttpRequest &curreq, const int tid, ULONG_PTR args){
            std::lock_guard<std::mutex> wlock(g_wizzard);

            libhttppp::HttpForm form;

            form.parse(curreq);

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

                if(!password.empty() && !username.empty()){
                    sha512 hash;
                    hash.hash(password,pwhash);

                    authdb::User user;

                    ::uuid::uuid adminid,admingid;

                    adminid.generate();

                    admingid.generate();

                    class authdb::UserData udat(adminid);

                    udat.setUserName(username.c_str());

                    authdb::sha512 sha;
                    sha.hash(password,pwhash);

                    udat.setPwHash(pwhash.c_str());

                    user.create(_AdminBackend,&udat);

                    authdb::Group group;

                    class authdb::GroupData gdat(admingid);

                    gdat.setName("SuperAdmin");

                    gdat.addMember(adminid);

                    group.create(_AdminBackend,&gdat);

                    class Gpo gpo;
                    std::vector<::uuid::uuid> gallow;
                    gallow.emplace_back(admingid);
                    size_t i=0;
                    try {
                        while(gpo_default[i][0]){
                            GpoData gpodat(gpo_default[i][0]);
                            gpodat.setName(gpo_default[i][1]);
                            gpodat.setDesc(gpo_default[i][2]);
                            if(strcmp(gpo_default[i][3],"true")==0)
                                gpodat.setGpoValue(true);
                            else
                                gpodat.setGpoValue(false);
                            gpodat.setAllowed(gallow);
                            gpo.create(_AdminBackend,&gpodat);
                            ++i;
                        }

                    }catch(std::exception &e){
                        std::cerr << e.what() << std::endl;
                    }

                    libhttppp::HTTPException exp;
                    exp[999] << "wizzard finished";
                    throw exp;
                }

                libhtmlpp::HtmlElement index;

                libhtmlpp::HtmlPage page;
                try {
                    page.loadFile(index,GETDATA(wizzard.html));
                } catch (libhtmlpp::HTMLException &e) {
                    libhttppp::HttpResponse erep;
                    erep.setState(HTTP500);
                    erep.setContentType("text/html");
                    erep.send(curreq, e.what(), strlen(e.what()));
                    return;
                }

                libhttppp::HttpResponse rep;

                libhtmlpp::HtmlString sendat;

                libhtmlpp::print(*static_cast<libhtmlpp::Element*>(&index),sendat,false);

                rep.setState(HTTP200);
                rep.setContentType("text/html");
                rep.send(curreq,sendat.data(),sendat.length());

        }

        void RequestEvent(libhttppp::HttpRequest &curreq, const int tid, ULONG_PTR args){
            const char *methods[]={"PARSE","GET","POST","DELETE","PUT"};
            int rtype=curreq.getRequestType();
            const char *method=(rtype>=0 && rtype<=4) ? methods[rtype] : "UNKNOWN";

            std::string clientAddr;
            if(!curreq.slots.empty() && curreq.slots[0].csock)
                curreq.slots[0].csock->getAddress(clientAddr);

            try {
                if(curreq.getRequestURL().compare(0,10,"/authdb.js")==0){
                    libhttppp::HttpResponse rep;
                    rep.setState(HTTP200);
                    rep.setContentType("text/javascript");
                    rep.send(curreq,_AuthJs,_AuthJs.size());
                    return;
                }else if(curreq.getRequestURL().compare(0,10,"/style.css")==0){
                    libhttppp::HttpResponse rep;
                    rep.setState(HTTP200);
                    rep.setContentType("text/css");
                    rep.send(curreq,_AuthCss,_AuthCss.size());
                    return;
                }else if(curreq.getRequestURL().compare(0,12,"/favicon.ico")==0){
                    libhttppp::HttpResponse rep;
                    rep.setState(HTTP200);
                    rep.setContentType("image/ico");
                    std::vector<char> out;
                    std::copy(FAVICON,FAVICON+FAVICON_LEN,std::back_inserter(out));
                    rep.send(curreq,out,FAVICON_LEN);
                    return;
                }

                if(_AdminBackend.end()<=sizeof(authdb::AuthHeader)){
                    // Force a prefetch to attempt retrieving domain data from the cluster.
                    // If we do not call lock_shared, prefetch is skipped, meaning transient
                    // background fetch failures will leave the node fully deadlocked 
                    // looping on 503 cluster data unavailable without retrying.
                    {
                        AuthBackend::Guard prefetch_guard(_AdminBackend, AuthBackend::Shared);
                    }

                }

                if(_AdminBackend.end()<=sizeof(authdb::AuthHeader)){
                    // API requests should get JSON error, not the wizard HTML
                    if(curreq.getRequestURL().compare(0,4,"/api",4)==0){
                        libhttppp::HttpResponse rep;
                        rep.setState(HTTP503);
                        rep.setContentType("application/json");
                        rep.send(curreq,"[{\"error\":\"not initialized\"}]",29);
                        return;
                    }
                    // Cluster has data but not enough peers to reconstruct — don't
                    // show the wizard (would create conflicting admin data).
                    if(_AdminBackend.clusterDataExists()){
                        libhttppp::HttpResponse rep;
                        rep.setState(HTTP503);
                        rep.setContentType("text/html");
                        rep.send(curreq,"cluster data unavailable — waiting for peers",45);
                        return;
                    }
                    std::cerr << "[AUTHDB] wizard check: end=" << _AdminBackend.end()
                              << " header=" << sizeof(authdb::AuthHeader) << std::endl;
                    AuthBackend::Guard admlock(_AdminBackend);
                    try{
                        WizzardEvent(curreq,tid,args);
                        return;
                    }catch(libhttppp::HTTPException &e){
                        if(e.getErrorType()!=999){
                            libhttppp::HttpResponse rep;
                            rep.setState(HTTP500);
                            rep.setContentType("text/html");
                            rep.send(curreq,e.what(),strlen(e.what()));
                            return;
                        }
                        std::cerr << "[AUTHDB] wizard done, end=" << _AdminBackend.end() << std::endl;
                    }
                }

                if(curreq.getRequestURL().compare(0,9,"/settings",9)==0){
                    try{
                        _AdminInterface.Request(curreq,tid,args);
                        g_Log.access(clientAddr,method,curreq.getRequestURL(),200);
                    }catch(AuthBackendError &e){
                        // Retry once on lock timeout before returning 500
                        try{
                            std::this_thread::yield();
                            _AdminInterface.Request(curreq,tid,args);
                            g_Log.access(clientAddr,method,curreq.getRequestURL(),200);
                        }catch(AuthBackendError &e2){
                            g_Log.error(clientAddr,e2.what());
                            g_Log.access(clientAddr,method,curreq.getRequestURL(),500);
                            libhttppp::HttpResponse rep;
                            rep.setState(HTTP500);
                            rep.setContentType("text/html");
                            rep.send(curreq,e2.what(),strlen(e2.what()));
                        }
                    }
                }else if(curreq.getRequestURL().compare(0,4,"/api",4)==0){
                    char domain[255]={0};

                    if(sscanf(curreq.getRequestURL().c_str(),"/api/%254[^/]s",domain)<0){
                        libhttppp::HttpResponse rep;
                        rep.setState(HTTP500);
                        rep.setContentType("text/html");
                        rep.send(curreq,"can't parse url",15);
                        return;
                    }

                    // Handle "admin" as special domain → use _AdminBackend directly
                    if(strcmp(domain,"admin")==0){
                        if(_AdminBackend.end() > sizeof(authdb::AuthHeader)){
                            ::uuid::uuid adminDid;
                            for(int _retry=0; _retry<5; ++_retry){
                                try{
                                    AuthBackend::Guard guard(_AdminBackend, AuthBackend::Shared);
                                    ApiController(_AdminBackend,adminDid,curreq,tid,args);
                                    g_Log.access(clientAddr,method,curreq.getRequestURL(),200);
                                    break;
                                }catch(AuthBackendError &e){
                                    if(_retry<4){
                                        std::this_thread::sleep_for(std::chrono::milliseconds(100));
                                        continue;
                                    }
                                    g_Log.error(clientAddr,e.what());
                                    g_Log.access(clientAddr,method,curreq.getRequestURL(),500);
                                    json_object *jerr = json_object_new_array();
                                    json_object *eobj = json_object_new_object();
                                    json_object_object_add(eobj,"error",json_object_new_string(e.what()));
                                    json_object_array_add(jerr,eobj);
                                    const char *errstr = json_object_to_json_string(jerr);
                                    libhttppp::HttpResponse rep;
                                    rep.setState(HTTP500);
                                    rep.setContentType("application/json");
                                    rep.send(curreq,errstr,strlen(errstr));
                                    json_object_put(jerr);
                                }catch(std::exception &e){
                                    g_Log.error(clientAddr,e.what());
                                    libhttppp::HttpResponse rep;
                                    rep.setState(HTTP500);
                                    rep.setContentType("application/json");
                                    rep.send(curreq,"[{\"error\":\"internal error\"}]",28);
                                    break;
                                }
                            }
                        } else {
                            libhttppp::HttpResponse rep;
                            rep.setState(HTTP503);
                            rep.setContentType("application/json");
                            rep.send(curreq,"[{\"error\":\"not initialized\"}]",29);
                        }
                        return;
                    }

                    ::uuid::uuid did;

                    if(did.parse(domain)!=0){
                        libhttppp::HttpResponse rep;
                        rep.setState(HTTP500);
                        rep.setContentType("application/json");
                        rep.send(curreq,"[{\"error\":\"can't parse domain id\"}]",36);
                        return;
                    }

                    AuthBackend *apidb = nullptr;
                    try {
                        apidb = getDomainBackend(did);
                    }catch(std::exception &e){
                        g_Log.error(clientAddr,e.what());
                        json_object *jerr = json_object_new_array();
                        json_object *eobj = json_object_new_object();
                        json_object_object_add(eobj,"error",json_object_new_string(e.what()));
                        json_object_array_add(jerr,eobj);
                        const char *errstr = json_object_to_json_string(jerr);
                        libhttppp::HttpResponse rep;
                        rep.setState(HTTP500);
                        rep.setContentType("application/json");
                        rep.send(curreq,errstr,strlen(errstr));
                        json_object_put(jerr);
                        return;
                    }catch(...){
                        libhttppp::HttpResponse rep;
                        rep.setState(HTTP500);
                        rep.setContentType("application/json");
                        rep.send(curreq,"[{\"error\":\"internal error loading domain\"}]",43);
                        return;
                    }

                    if(apidb){
                        for(int _retry=0; _retry<5; ++_retry){
                            try{
                                AuthBackend::Guard guard(*apidb, AuthBackend::Shared);
                                ApiController(*apidb,did,curreq,tid,args);
                                g_Log.access(clientAddr,method,curreq.getRequestURL(),200);
                                break;
                            }catch(AuthBackendError &e){
                                if(_retry<4){
                                    std::this_thread::sleep_for(std::chrono::milliseconds(100));
                                    continue;
                                }
                                g_Log.error(clientAddr,e.what());
                                g_Log.access(clientAddr,method,curreq.getRequestURL(),500);
                                json_object *jerr = json_object_new_array();
                                json_object *eobj = json_object_new_object();
                                json_object_object_add(eobj,"error",json_object_new_string(e.what()));
                                json_object_array_add(jerr,eobj);
                                const char *errstr = json_object_to_json_string(jerr);
                                libhttppp::HttpResponse rep;
                                rep.setState(HTTP500);
                                rep.setContentType("application/json");
                                rep.send(curreq,errstr,strlen(errstr));
                                json_object_put(jerr);
                            }catch(std::exception &e){
                                g_Log.error(clientAddr,e.what());
                                g_Log.access(clientAddr,method,curreq.getRequestURL(),500);
                                json_object *jerr = json_object_new_array();
                                json_object *eobj = json_object_new_object();
                                json_object_object_add(eobj,"error",json_object_new_string(e.what()));
                                json_object_array_add(jerr,eobj);
                                const char *errstr = json_object_to_json_string(jerr);
                                libhttppp::HttpResponse rep;
                                rep.setState(HTTP500);
                                rep.setContentType("application/json");
                                rep.send(curreq,errstr,strlen(errstr));
                                json_object_put(jerr);
                                break;
                            }catch(...){
                                g_Log.access(clientAddr,method,curreq.getRequestURL(),500);
                                libhttppp::HttpResponse rep;
                                rep.setState(HTTP500);
                                rep.setContentType("application/json");
                                rep.send(curreq,"[{\"error\":\"internal error\"}]",28);
                                break;
                            }
                        }
                    }else{
                        g_Log.access(clientAddr,method,curreq.getRequestURL(),404);
                        libhttppp::HttpResponse rep;
                        rep.setState(HTTP404);
                        rep.setContentType("application/json");
                        rep.send(curreq,"[{\"error\":\"uuid not found\"}]",27);
                    }
                }else if(curreq.getRequestURL().compare(0,10,"/getavatar",10)==0){
                    try{
                        getAvatar(curreq,tid,args);
                        g_Log.access(clientAddr,method,curreq.getRequestURL(),200);
                    }catch(AuthBackendError &e){
                        try{
                            std::this_thread::yield();
                            getAvatar(curreq,tid,args);
                            g_Log.access(clientAddr,method,curreq.getRequestURL(),200);
                        }catch(AuthBackendError &e2){
                            g_Log.error(clientAddr,e2.what());
                            g_Log.access(clientAddr,method,curreq.getRequestURL(),500);
                            libhttppp::HttpResponse rep;
                            rep.setState(HTTP500);
                            rep.setContentType("text/html");
                            rep.send(curreq,e2.what(),strlen(e2.what()));
                        }
                    }
                }else if(curreq.getRequestURL().compare(0,10,"/admin/api",10)==0){
                    // Admin JSON API endpoint (used by authdb client library)
                    if(_AdminBackend.end() > sizeof(authdb::AuthHeader)){
                        ::uuid::uuid adminDid;
                        for(int _retry=0; _retry<5; ++_retry){
                            try{
                                AuthBackend::Guard guard(_AdminBackend, AuthBackend::Shared);
                                ApiController(_AdminBackend,adminDid,curreq,tid,args);
                                g_Log.access(clientAddr,method,curreq.getRequestURL(),200);
                                break;
                            }catch(AuthBackendError &e){
                                if(_retry<4){
                                    std::this_thread::sleep_for(std::chrono::milliseconds(100));
                                    continue;
                                }
                                g_Log.error(clientAddr,e.what());
                                g_Log.access(clientAddr,method,curreq.getRequestURL(),500);
                                json_object *jerr = json_object_new_array();
                                json_object *eobj = json_object_new_object();
                                json_object_object_add(eobj,"error",json_object_new_string(e.what()));
                                json_object_array_add(jerr,eobj);
                                const char *errstr = json_object_to_json_string(jerr);
                                libhttppp::HttpResponse rep;
                                rep.setState(HTTP500);
                                rep.setContentType("application/json");
                                rep.send(curreq,errstr,strlen(errstr));
                                json_object_put(jerr);
                            }catch(std::exception &e){
                                g_Log.error(clientAddr,e.what());
                                libhttppp::HttpResponse rep;
                                rep.setState(HTTP500);
                                rep.setContentType("application/json");
                                rep.send(curreq,"[{\"error\":\"internal error\"}]",28);
                                break;
                            }
                        }
                    } else {
                        libhttppp::HttpResponse rep;
                        rep.setState(HTTP503);
                        rep.setContentType("application/json");
                        rep.send(curreq,"[{\"error\":\"not initialized\"}]",29);
                    }
                }else if(curreq.getRequestURL() == "/" || curreq.getRequestURL().empty()){
                    // Root-level JSON API requests (used by authdb client library)
                    libhttppp::HttpHeader::HeaderData *ct = nullptr;
                    try { ct = curreq.getHeaderData("content-type"); } catch(...) {}
                    bool isJson = false;
                    if (ct && ct->getfirstValue()) {
                        std::string ctval = ct->getfirstValue()->getvalue();
                        isJson = (ctval.find("application/json") != std::string::npos);
                    }
                    if (isJson && _AdminBackend.end() > sizeof(authdb::AuthHeader)) {
                        ::uuid::uuid adminDid;
                        for(int _retry=0; _retry<5; ++_retry){
                            try{
                                AuthBackend::Guard guard(_AdminBackend, AuthBackend::Shared);
                                ApiController(_AdminBackend,adminDid,curreq,tid,args);
                                g_Log.access(clientAddr,method,curreq.getRequestURL(),200);
                                break;
                            }catch(AuthBackendError &e){
                                if(_retry<4){
                                    std::this_thread::sleep_for(std::chrono::milliseconds(100));
                                    continue;
                                }
                                g_Log.error(clientAddr,e.what());
                                g_Log.access(clientAddr,method,curreq.getRequestURL(),500);
                                json_object *jerr = json_object_new_array();
                                json_object *eobj = json_object_new_object();
                                json_object_object_add(eobj,"error",json_object_new_string(e.what()));
                                json_object_array_add(jerr,eobj);
                                const char *errstr = json_object_to_json_string(jerr);
                                libhttppp::HttpResponse rep;
                                rep.setState(HTTP500);
                                rep.setContentType("application/json");
                                rep.send(curreq,errstr,strlen(errstr));
                                json_object_put(jerr);
                            }catch(std::exception &e){
                                g_Log.error(clientAddr,e.what());
                                libhttppp::HttpResponse rep;
                                rep.setState(HTTP500);
                                rep.setContentType("application/json");
                                rep.send(curreq,"[{\"error\":\"internal error\"}]",28);
                                break;
                            }
                        }
                    } else {
                        libhttppp::HttpResponse rep;
                        if(_AdminBackend.end()>sizeof(authdb::AuthHeader)){
                            rep.setState(HTTP307);
                            rep.setHeaderData("Location")->push_back("/settings/index");
                            rep.setContentType("text/html");
                            rep.send(curreq,"",0);
                        }else{
                            rep.setState(HTTP503);
                            rep.setContentType("text/html");
                            rep.send(curreq,"service not ready",17);
                        }
                    }
                }
            } catch (std::exception &e) {
                libhttppp::HttpResponse rep;
                rep.setState(HTTP500);
                rep.setContentType("text/html");
                rep.send(curreq,e.what(),strlen(e.what()));
            } catch (...) {
                libhttppp::HttpResponse rep;
                rep.setState(HTTP500);
                rep.setContentType("text/html");
                rep.send(curreq,"internal error",14);
            }
        }

        void getAvatar(libhttppp::HttpRequest &curreq, const int tid, ULONG_PTR args){
            libhttppp::HttpResponse rep;

            AuthBackend *backend=nullptr;

            char domain[255],cuid[255],ext[16];
            if(sscanf(curreq.getRequestURL().c_str(),"/getavatar/%[^/]/%[^.].%s",domain,cuid,ext)<0){
                libhttppp::HttpResponse rep;
                rep.setState(HTTP400);
                rep.setContentType("text/html");
                rep.send(curreq,"bad avatar url",14);
                return;
            }

            ::uuid::uuid did;

            if(strcmp(domain,"admin") == 0 ){
                backend=&_AdminBackend;
            }else if(did.parse(domain) == 0 ){
                backend=getDomainBackend(did);
            }else{
                throw AuthBackendError("createUser: could not parse uuid!");
            }

            ::uuid::uuid uid(cuid);

            User user;
            class UserData udat(uid);
            size_t upos = sizeof(AuthHeader);
            try {
                user.info(*backend, udat, upos);
                std::vector<char> img;
                udat.getAvatar(img);
                if (!img.empty()) {
                    char ctype[255];
                    snprintf(ctype, 255, "image/%s", ext);
                    rep.setContentType(ctype);
                    rep.send(curreq, img, img.size());
                    return;
                }
            } catch (AuthBackendError &) {
            }

            rep.setState(HTTP404);
            rep.send(curreq,"",0);

        };

        AuthBackend *getDomainBackend(const ::uuid::uuid &did){
            {
                std::shared_lock<std::shared_mutex> rl(_DomainCacheMtx);
                auto it = _DomainCache.find(std::string(did.value, did.value+16));
                if(it != _DomainCache.end())
                    return it->second.get();
            }

            DomainBackend db(_AdminBackend);
            DomainInfo dinfo;
            {
                AuthBackend::Guard admlock(_AdminBackend, AuthBackend::Shared);
                dinfo = db.resolve(did.value);
            }
            if(!dinfo.found)
                return nullptr;

            auto backend = std::make_shared<AuthBackend>(
                dinfo.storageType, dinfo.storageOptions, dinfo.domainName);

            std::unique_lock<std::shared_mutex> wl(_DomainCacheMtx);
            auto key = std::string(did.value, did.value+16);
            auto [it, inserted] = _DomainCache.emplace(key, std::move(backend));
            return it->second.get();
        }

        // Pre-populate domain cache so API requests never need to lock _AdminBackend
        void warmDomainCache() {
            Domain domain;
            std::vector<uuid::uuid> dids;
            {
                AuthBackend::Guard admlock(_AdminBackend, AuthBackend::Shared);
                domain.list(_AdminBackend, dids);
            }
            DomainBackend db(_AdminBackend);
            for (const auto &did : dids) {
                // Skip if already cached
                {
                    std::shared_lock<std::shared_mutex> rl(_DomainCacheMtx);
                    if (_DomainCache.count(std::string(did.value, did.value+16)))
                        continue;
                }
                DomainInfo dinfo;
                {
                    AuthBackend::Guard admlock(_AdminBackend, AuthBackend::Shared);
                    dinfo = db.resolve(did);
                }
                if (!dinfo.found) continue;
                if (dinfo.storageOptions.empty()) {
                    std::cerr << "[AUTHDB] skipping domain '" << dinfo.domainName
                              << "': empty storage path" << std::endl;
                    continue;
                }
                auto backend = std::make_shared<AuthBackend>(
                    dinfo.storageType, dinfo.storageOptions, dinfo.domainName);
                std::unique_lock<std::shared_mutex> wl(_DomainCacheMtx);
                auto key = std::string(did.value, did.value+16);
                _DomainCache.emplace(key, std::move(backend));
            }
            std::cerr << "[AUTHDB] domain cache warmed: " << dids.size() << " domain(s)" << std::endl;
        }

        void evictDomainCache(const ::uuid::uuid &did) {
            std::unique_lock<std::shared_mutex> wl(_DomainCacheMtx);
            auto key = std::string(did.value, did.value+16);
            _DomainCache.erase(key);
        }

        void vacuumAll() {
            /* Vacuum admin backend */
            try {
                size_t before = 0, after = 0;
                _AdminBackend.vacuum(before, after);
                if (before != after) {
                    std::cerr << "[AUTHDB] auto-vacuum admin: "
                              << before << " -> " << after << " bytes"
                              << std::endl;
                }
            } catch (const std::exception &e) {
                std::cerr << "[AUTHDB] auto-vacuum admin error: "
                          << e.what() << std::endl;
            }
            /* Vacuum all cached domain backends */
            std::vector<std::shared_ptr<AuthBackend>> domainsToVacuum;
            {
                std::shared_lock<std::shared_mutex> rl(_DomainCacheMtx);
                for (auto &[key, domBackend] : _DomainCache) {
                    domainsToVacuum.push_back(domBackend);
                }
            }

            for (auto &domBackend : domainsToVacuum) {
                try {
                    size_t before = 0, after = 0;
                    domBackend->vacuum(before, after);
                    if (before != after) {
                        std::cerr << "[AUTHDB] auto-vacuum domain ("
                                  << domBackend->getDomain() << "): "
                                  << before << " -> " << after
                                  << " bytes" << std::endl;
                    }
                } catch (const std::exception &e) {
                    std::cerr << "[AUTHDB] auto-vacuum domain error: "
                              << e.what() << std::endl;
                }
            }
        }

    private:
        AuthBackend        &_AdminBackend;
        std::unique_ptr<Session>  _Session;
        AdminInterface       _AdminInterface;
        std::vector<char>  _AuthJs;
        std::vector<char>  _AuthCss;
        std::shared_mutex                                     _DomainCacheMtx;
        std::map<std::string, std::shared_ptr<AuthBackend>>   _DomainCache;
    };

};

int main(int argc,char *argv[]){
    cmdplus::CmdController cmd(cmdplus::CmdController::getInstance());

    cmd.registerCmd("config",'c',true,"","Config Path");
    cmd.registerCmd("debug",'d',false,"false","Debug Mode");
#ifndef _WIN32
    cmd.registerCmd("foreground",'f',false,"false","Run in foreground (disable daemon)");
#endif

    cmd.parseCmd(argc,argv);

    if(!cmd.checkRequired()){
        std::cerr << "Config Path required !" << std::endl;
        cmd.printHelp();
        return -1;
    }

#ifndef _WIN32
    bool foreground = (cmd.getCmdbyKey("foreground")->getValue() == "true");

    if (!foreground) {
        pid_t pid = fork();
        if (pid < 0) {
            std::cerr << "fork failed" << std::endl;
            return 1;
        }
        if (pid > 0)
            return 0;

        if (setsid() < 0) {
            std::cerr << "setsid failed" << std::endl;
            return 1;
        }

        umask(0);

        int devnull = open("/dev/null", O_RDWR);
        if (devnull >= 0) {
            dup2(devnull, STDIN_FILENO);
            dup2(devnull, STDOUT_FILENO);
            dup2(devnull, STDERR_FILENO);
            if (devnull > STDERR_FILENO)
                close(devnull);
        }
    }

#endif

    /* Create log directory and open log files */
#ifndef _WIN32
    {
        struct stat st;
        if (stat("/var/log/authdb", &st) != 0) {
            if (mkdir("/var/log/authdb", 0750) != 0)
                std::cerr << "mkdir /var/log/authdb failed: " << strerror(errno) << std::endl;
        }
        struct passwd *lpw = getpwnam("authdb");
        if (lpw) {
            if (chown("/var/log/authdb", lpw->pw_uid, lpw->pw_gid) != 0)
                std::cerr << "chown /var/log/authdb failed: " << strerror(errno) << std::endl;
        }
    }
    authdb::g_Log.open("/var/log/authdb/access.log", "/var/log/authdb/error.log");
#else
    authdb::g_Log.open("access.log", "error.log");
#endif

    try {

    confplus::Config config(cmd.getCmdbyKey("config")->getValue());

    /* Determine backend type: cluster or file */
    bool clusterEnabled = false;
    {
        auto *clusterEnable = config.getKey("/AUTHDB/CLUSTER/ENABLE");
        if (clusterEnable && config.getValue(clusterEnable, 0) == "true")
            clusterEnabled = true;
    }
        std::string cert;
        std::string key;
        if (auto *certKey = config.getKey("/AUTHDB/SSL/CERT"))
            cert = config.getValue(certKey, 0);
        if (auto *keyKey = config.getKey("/AUTHDB/SSL/KEY"))
            key = config.getValue(keyKey, 0);

        if (clusterEnabled && (cert.empty() || key.empty())) {
            std::cerr << "Cluster mode requires /AUTHDB/SSL/CERT and /AUTHDB/SSL/KEY" << std::endl;
            return 1;
        }

        /* Cluster config — parsed early, started after priv drop */
        authdb::Cluster cluster;
        authdb::ClusterConfig ccfg;
        if (clusterEnabled) {
            ccfg.bind_address = config.getValue(config.getKey("/AUTHDB/CLUSTER/BIND"), 0);
            ccfg.port = config.getIntValue(config.getKey("/AUTHDB/CLUSTER/PORT"), 0);
            ccfg.cert_file = cert;
            ccfg.key_file = key;

            auto *authName = config.getKey("/AUTHDB/CLUSTER/AUTH_NAME");
            auto *authKey  = config.getKey("/AUTHDB/CLUSTER/AUTH_KEY");
            if (authName) ccfg.client_name = config.getValue(authName, 0);
            if (authKey)  ccfg.client_key  = config.getValue(authKey, 0);

            auto *dataBlocks   = config.getKey("/AUTHDB/CLUSTER/DATA_BLOCKS");
            auto *parityBlocks = config.getKey("/AUTHDB/CLUSTER/PARITY_BLOCKS");
            if (dataBlocks)   ccfg.data_blocks   = config.getIntValue(dataBlocks, 0);
            if (parityBlocks) ccfg.parity_blocks = config.getIntValue(parityBlocks, 0);

            auto *storePath = config.getKey("/AUTHDB/CLUSTER/PATH");
            if (storePath) ccfg.store_path = config.getValue(storePath, 0);

            auto *peersKey = config.getKey("/AUTHDB/CLUSTER/PEERS");
            if (peersKey) {
                for (int pi = 0; ; pi++) {
                    try {
                        std::string peerStr = config.getValue(peersKey, pi);
                        if (peerStr.empty()) break;
                        auto colon = peerStr.rfind(':');
                        if (colon != std::string::npos) {
                            authdb::ClusterNode node;
                            node.address = peerStr.substr(0, colon);
                            node.port = std::stoi(peerStr.substr(colon + 1));
                            ccfg.peers.push_back(node);
                        }
                    } catch (...) { break; }
                }
            }
        }

        std::string backendPath;
        if (!clusterEnabled) {
            backendPath = config.getValue(config.getKey("/AUTHDB/ADMINDB/PATH"), 0);
        }

        std::unique_ptr<libhttppp::HttpD> httpd;
        if (!cert.empty() && !key.empty()) {
            httpd = std::make_unique<libhttppp::HttpD>(
                config.getValue(config.getKey("/AUTHDB/BIND"),0),
                config.getIntValue(config.getKey("/AUTHDB/PORT"),0),
                config.getIntValue(config.getKey("/AUTHDB/MAXCONN"),0),
                cert,
                key,
                ""
            );
        } else {
            httpd = std::make_unique<libhttppp::HttpD>(
                config.getValue(config.getKey("/AUTHDB/BIND"),0),
                config.getIntValue(config.getKey("/AUTHDB/PORT"),0),
                config.getIntValue(config.getKey("/AUTHDB/MAXCONN"),0),
                "",
                "",
                ""
            );
        }

        /* Start cluster while still root so QUIC can bind privileged ports */
        if (clusterEnabled) {
            cluster.init(ccfg);
            cluster.start();
            authdb::g_Cluster = &cluster;
            std::cerr << "Cluster started on " << ccfg.bind_address << ":" << ccfg.port
                      << " with " << ccfg.peers.size() << " peer(s)" << std::endl;
            // Wait for peers to become reachable before constructing backends.
            // This ensures fetchFromCluster() in ClusterBackend can retrieve data.
            size_t online = cluster.waitForPeers(120);
            if (online == 0) {
                std::cerr << "Cluster: no peers reachable, cannot start. Exiting." << std::endl;
                return 1;
            }
        }

        // Instantiate session store: cluster-aware or local-only
        std::unique_ptr<authdb::Session> sessionStore;
        if (clusterEnabled) {
            sessionStore = std::make_unique<authdb::ClusterSession>();
        } else {
            sessionStore = std::make_unique<authdb::LocalSession>();
        }
        authdb::AuthSession = sessionStore.get();

        std::cerr << "[DEBUG] backendPath='" << backendPath << "' clusterEnabled=" << clusterEnabled << std::endl;

        authdb::AuthBackend backend(
            clusterEnabled ? authdb::AuthBackendType::ClusterStore : authdb::AuthBackendType::File,
            backendPath,
            "admin.local"
        );

        // Cluster backends defer the network fetch to avoid constructor timeouts.
        // Trigger a single initial fetch so backend.end() reflects cluster state.
        if (clusterEnabled) {
            authdb::AuthBackend::Guard initGuard(backend, authdb::AuthBackend::Shared);
            std::cerr << "[AUTHDB] initial cluster fetch: admin.local size="
                      << backend.end() << std::endl;
        }

        authdb::AuthDB authdb(backend,httpd->getServerSockets());

        // Pre-load all domain backends into cache so API requests
        // don't need to lock the admin backend for domain resolution.
        if (backend.end() > sizeof(authdb::AuthHeader))
            authdb.warmDomainCache();

        // Signal cluster that domain backends are loaded — scrub may now
        // safely identify orphaned groups without risking data loss.
        if (clusterEnabled)
            cluster.setDomainsReady();

        if(cmd.getCmdbyKey("debug")->getValue()=="true"){
            authdb::DEBUG=true;
            authdb.threads=1;
        }

        /* Load plugins from configured directory */
        std::string pluginDir;
        if(config.getKey("/AUTHDB/PLUGINS/PATH")!=nullptr)
            pluginDir = config.getValue(config.getKey("/AUTHDB/PLUGINS/PATH"), 0);

        /* Build JSON config string for plugins to parse */
        json_object *jcfg = json_object_new_object();
        {
            auto *kdcKey = config.getKey("/AUTHDB/KDC/BIND");
            auto *kdcPort = config.getKey("/AUTHDB/KDC/PORT");
            auto *maxConn = config.getKey("/AUTHDB/MAXCONN");
            if (kdcKey)
                json_object_object_add(jcfg, "bind",
                    json_object_new_string(config.getValue(kdcKey, 0).c_str()));
            if (kdcPort)
                json_object_object_add(jcfg, "port",
                    json_object_new_int(config.getIntValue(kdcPort, 0)));
            if (maxConn)
                json_object_object_add(jcfg, "maxconn",
                    json_object_new_int(config.getIntValue(maxConn, 0)));

            /* AD/LDAP plugin config */
            auto *adBind = config.getKey("/AUTHDB/AD/BIND");
            auto *adPort = config.getKey("/AUTHDB/AD/PORT");
            auto *adMax  = config.getKey("/AUTHDB/AD/MAXCONN");
            if (adBind)
                json_object_object_add(jcfg, "ad_bind",
                    json_object_new_string(config.getValue(adBind, 0).c_str()));
            if (adPort)
                json_object_object_add(jcfg, "ad_port",
                    json_object_new_int(config.getIntValue(adPort, 0)));
            if (adMax)
                json_object_object_add(jcfg, "ad_maxconn",
                    json_object_new_int(config.getIntValue(adMax, 0)));
        }
        const char *cfgStr = json_object_to_json_string(jcfg);

        authdb::PluginManager pluginMgr;
        authdb::g_PluginManager = &pluginMgr;

        if (!pluginDir.empty()) {
            int n = pluginMgr.loadAll(pluginDir, backend, cfgStr);
            std::cerr << "Loaded " << n << " plugin(s) from " << pluginDir << std::endl;
        }
        json_object_put(jcfg);

        /* Wire up well-known plugin interfaces */
        authdb::g_KdcPlugin = pluginMgr.getPlugin<authdb::KdcPlugin>("krb5");
        authdb::g_AdPlugin  = pluginMgr.getPlugin<authdb::AdPlugin>("ad");

        /* Init plugins while still root — bind privileged ports (88, 389, etc.) */
        pluginMgr.initAll();

        /* Drop privileges after all sockets are bound */
#ifndef _WIN32
        if (getuid() == 0) {
            struct passwd *pw = getpwnam("authdb");
            if (pw) {
                /* Chown data files created while running as root (e.g. blocks.bin) */
                if (!backendPath.empty()) {
                    namespace fs = std::filesystem;
                    fs::path parentDir = fs::path(backendPath).parent_path();
                    if (fs::is_directory(parentDir)) {
                        for (auto &entry : fs::recursive_directory_iterator(parentDir)) {
                            if (chown(entry.path().c_str(), pw->pw_uid, pw->pw_gid) != 0)
                                std::cerr << "chown " << entry.path() << " failed: " << strerror(errno) << std::endl;
                        }
                        if (chown(parentDir.c_str(), pw->pw_uid, pw->pw_gid) != 0)
                            std::cerr << "chown " << parentDir << " failed: " << strerror(errno) << std::endl;
                    }
                }
                if (clusterEnabled && !ccfg.store_path.empty()) {
                    namespace fs = std::filesystem;
                    if (fs::is_directory(ccfg.store_path)) {
                        for (auto &entry : fs::recursive_directory_iterator(ccfg.store_path)) {
                            if (chown(entry.path().c_str(), pw->pw_uid, pw->pw_gid) != 0)
                                std::cerr << "chown " << entry.path() << " failed: " << strerror(errno) << std::endl;
                        }
                        if (chown(ccfg.store_path.c_str(), pw->pw_uid, pw->pw_gid) != 0)
                            std::cerr << "chown " << ccfg.store_path << " failed: " << strerror(errno) << std::endl;
                    }
                }
                if (setgid(pw->pw_gid) != 0) {
                    std::cerr << "setgid failed" << std::endl;
                    return 1;
                }
                if (initgroups("authdb", pw->pw_gid) != 0) {
                    std::cerr << "initgroups failed" << std::endl;
                    return 1;
                }
                if (setuid(pw->pw_uid) != 0) {
                    std::cerr << "setuid failed" << std::endl;
                    return 1;
                }
            } else {
                std::cerr << "user authdb not found, running as root" << std::endl;
            }
        }
#endif

        /* Start each plugin in its own background thread (runs as unprivileged user) */
        std::vector<std::thread> pluginThreads;
        for (auto &[pname, lp] : pluginMgr.plugins()) {
            pluginThreads.emplace_back([&lp, &pname](){
                try {
                    lp.instance->run();
                } catch(const netplus::NetException &e) {
                    std::cerr << "Plugin '" << pname << "' net error: "
                              << e.what() << std::endl;
                } catch(const std::exception &e) {
                    std::cerr << "Plugin '" << pname << "' error: "
                              << e.what() << std::endl;
                } catch(...) {
                    std::cerr << "Plugin '" << pname << "' unknown error" << std::endl;
                }
            });
            pluginThreads.back().detach();
        }

        /* Periodic auto-vacuum thread — compacts file backends to prevent
           bloat from deleted/overwritten records which causes slow I/O and
           timeouts over time.  Interval is configurable via config (hours),
           default 6 hours. */
        int vacuumIntervalHours = 6;
        {
            auto *vacKey = config.getKey("/AUTHDB/VACUUM_INTERVAL");
            if (vacKey)
                vacuumIntervalHours = config.getIntValue(vacKey, 0);
            if (vacuumIntervalHours < 1)
                vacuumIntervalHours = 1;
        }

        std::atomic<bool> vacuumRunning{true};
        std::mutex vacuumMtx;
        std::condition_variable vacuumCv;

        std::thread vacuumThread([&](){
            std::cerr << "[AUTHDB] auto-vacuum thread started (interval: "
                      << vacuumIntervalHours << "h)" << std::endl;
            while (vacuumRunning) {
                {
                    std::unique_lock<std::mutex> lk(vacuumMtx);
                    vacuumCv.wait_for(lk,
                        std::chrono::hours(vacuumIntervalHours),
                        [&]{ return !vacuumRunning.load(); });
                }
                if (!vacuumRunning) break;

                authdb.vacuumAll();
            }
            std::cerr << "[AUTHDB] auto-vacuum thread stopped" << std::endl;
        });
        vacuumThread.detach();

        authdb.runEventloop();

        /* Stop auto-vacuum */
        vacuumRunning = false;
        vacuumCv.notify_all();

        /* Shutdown cluster */
        authdb::g_Cluster = nullptr;
        cluster.stop();

        authdb::g_KdcPlugin = nullptr;
        authdb::g_AdPlugin  = nullptr;
        authdb::g_PluginManager = nullptr;
    }catch(const netplus::NetException &e){
        std::cerr << "NetException: " << e.what() << std::endl;
    }catch(authdb::AuthBackendError &e){
        std::cerr << e.what() << std::endl;
    }catch(std::exception &e){
        std::cerr << e.what() << std::endl;
    }catch(...){
        std::cerr << "Unknown fatal exception" << std::endl;
    }
    return 0;
}
