/*******************************************************************************
 * 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 <iostream>
#include <mutex>

#include "session.h"
#include "authdb.h"
#include "backend.h"
#include "group.h"
#include "gpo.h"
#include "gpo_default.h"
#include "types.h"
#include "exception.h"
#include "cluster.h"

namespace authdb {
        SessionData _Sessionempty;
        std::mutex   _SessionLock;
}

authdb::SessionData::SessionData(){
    _next=nullptr;
    _created=std::chrono::steady_clock::now();
    _lastClusterSync=std::chrono::steady_clock::time_point{};
}

authdb::SessionData::SessionData(const SessionData& src) : SessionData() {
        std::copy(src._members.begin(),src._members.end(),
                        std::back_inserter(_members));
         _sid=src._sid;
         _uid=src._uid;
         _did=src._did;
}

authdb::SessionData::SessionData(const SessionData *src) : SessionData() {
         std::copy(src->_members.begin(),src->_members.end(),
                        std::back_inserter(_members));
         _sid=src->_sid;
         _uid=src->_uid;
         _did=src->_did;
}

authdb::SessionData::SessionData(uuid::uuid sessionid, uuid::uuid userid, uuid::uuid domainid,std::vector<uuid::uuid> &members) : _members(members){
    _sid=sessionid;
    _uid=userid;
    _did=domainid;
    _next=nullptr;
    _created=std::chrono::steady_clock::now();
}

authdb::SessionData::~SessionData(){
    clear();
}

void authdb::SessionData::clear(){
    _sid.clear();
    _uid.clear();
    _did.clear();
    _next=nullptr;
}


void authdb::SessionData::getUid(uuid::uuid& dest) const{
    dest=_uid;
}

void authdb::SessionData::getSid(uuid::uuid& dest) const{
    dest=_sid;
}

void authdb::SessionData::getDid(uuid::uuid& dest) const{
    dest=_did;
}

const char * authdb::SessionData::getUsername() const{
    return _username.c_str();
}

bool authdb::SessionData::empty() const{
    if( _uid.empty() &&  _sid.empty() && _did.empty() && _members.empty() )
        return true;
    return false;
}


bool authdb::SessionData::isMemberOf(const uuid::uuid& gid) const{
    for(const auto &member : _members){
        if(member==gid)
            return true;
    }
    return false;
}

const authdb::SessionData *authdb::SessionData::next() const{
    return _next;
}

authdb::SessionData::GPOResult::GPOResult(){
    next=nullptr;
    GPORes=false;
}

authdb::SessionData::GPOResult::~GPOResult(){
    while(next){
        GPOResult *ne=next->next;
        next->next=nullptr;
        delete next;
        next=ne;
    }
    delete next;
}

bool authdb::SessionData::checkGPO(uuid::uuid gpoid) const{
    for(const GPOResult *cures=&_GPOResult; cures; cures=cures->next){
        if(cures->GPOId==gpoid){
            return cures->GPORes;
        }
    }
    return false;
}

void authdb::SessionData::getMembers(std::vector<uuid::uuid> &out) const{
    out = _members;
}

void authdb::SessionData::getGPOResults(std::vector<std::pair<uuid::uuid, bool>> &out) const{
    out.clear();
    for(const GPOResult *cures=&_GPOResult; cures; cures=cures->next){
        if(!cures->GPOId.empty()){
            out.emplace_back(cures->GPOId, cures->GPORes);
        }
    }
}

void authdb::SessionData::setGPOResults(const std::vector<std::pair<uuid::uuid, bool>> &results){
    // Clear existing GPO results (except the embedded first one)
    while(_GPOResult.next){
        GPOResult *ne=_GPOResult.next->next;
        _GPOResult.next->next=nullptr;
        delete _GPOResult.next;
        _GPOResult.next=ne;
    }
    _GPOResult.GPOId.clear();
    _GPOResult.GPORes=false;

    GPOResult *cur=&_GPOResult;
    for(size_t i=0; i<results.size(); ++i){
        if(i==0){
            cur->GPOId=results[i].first;
            cur->GPORes=results[i].second;
        }else{
            cur->next=new GPOResult();
            cur=cur->next;
            cur->GPOId=results[i].first;
            cur->GPORes=results[i].second;
        }
    }
}

// ===== LocalSession =====

authdb::LocalSession::LocalSession() {
}

void authdb::LocalSession::clear(){
    std::lock_guard<std::mutex> guard(_lock);
    for (auto &[k, v] : _sessions)
        delete v;
    _sessions.clear();
}

authdb::LocalSession::~LocalSession(){
    clear();
}

authdb::SessionData *authdb::LocalSession::findLocal(uuid::uuid sessionid){
    auto it = _sessions.find(sessionid);
    if (it != _sessions.end())
        return it->second;
    return nullptr;
}

void authdb::LocalSession::appendLocal(SessionData *sess){
    _sessions[sess->_sid] = sess;
}

void authdb::LocalSession::removeLocal(uuid::uuid sessionid){
    auto it = _sessions.find(sessionid);
    if (it != _sessions.end()) {
        delete it->second;
        _sessions.erase(it);
    }
}

void authdb::LocalSession::cleanupExpired(){
    auto now=std::chrono::steady_clock::now();
    for (auto it = _sessions.begin(); it != _sessions.end(); ) {
        auto age=std::chrono::duration_cast<std::chrono::hours>(now - it->second->_created).count();
        if(age >= 24){
            delete it->second;
            it = _sessions.erase(it);
        }else{
            ++it;
        }
    }
}

namespace authdb {

SessionData *createSessionData(AuthBackend &backend, uuid::uuid domainid, uuid::uuid userid){
    std::string username;
    std::shared_ptr<AuthData::Record> cur=std::make_shared<AuthData::Record>();

    size_t rd=sizeof(AuthHeader),end=backend.end();
    while(rd<end){
        cur->type=EmptyData;
        backend.setPos(rd);
        backend.read(reinterpret_cast<unsigned char*>(cur.get()),sizeof(AuthData::Record));
        rd=backend.getPos()+cur->datasize;
        if(cur->type == UserData && strcmp(cur->fieldname,"username")==0
            && userid==cur->ruid){
            unsigned char *tmp = new unsigned char[cur->datasize];
            backend.read(tmp,cur->datasize);
            username=reinterpret_cast<char*>(tmp);
            delete[] tmp;
        }
    }

    std::shared_ptr<std::vector<uuid::uuid>> mbs = std::make_shared<std::vector<uuid::uuid>>();
    rd=sizeof(AuthHeader);
    while(rd<end){
        backend.setPos(rd);
        backend.read(reinterpret_cast<unsigned char*>(cur.get()),sizeof(AuthData::Record));
        rd=backend.getPos()+cur->datasize;
        if( !uuid::uuid(cur->ruid).empty() && cur->type == GroupData && strcmp(cur->fieldname,"groupname")==0){
            Group group;
            std::shared_ptr<class GroupData> grpi=std::make_shared<class GroupData>(cur->ruid);
            size_t upos=sizeof(AuthHeader);
            group.info(backend,*grpi,upos);
            if(grpi->isMember(userid)){
                mbs->emplace_back(cur->ruid);
            }
        }
    }

    uuid::uuid sid;
    sid.generate();

    SessionData *newSession=new SessionData(sid,userid,domainid,*mbs);
    newSession->_username=username;
    newSession->_GPOResult.next=nullptr;

    SessionData::GPOResult *gpores=&newSession->_GPOResult;

    Gpo gpo;
    std::vector<uuid::uuid> allow;
    rd=sizeof(AuthHeader);

    while(rd<end){
        backend.setPos(rd);
        backend.read(reinterpret_cast<unsigned char*>(cur.get()),sizeof(AuthData::Record));
        rd=backend.getPos()+cur->datasize;
        if(cur->type == PolicyData && strcmp(cur->fieldname,"gponame")==0){
            std::shared_ptr<class GpoData> gpodat=std::make_shared<class GpoData>(cur->ruid);
            size_t upos=sizeof(AuthHeader);
            gpo.info(backend,*gpodat,upos);
            if(!gpores->GPOId.empty()){
                gpores->next=new SessionData::GPOResult();
                gpores=gpores->next;
            }
            gpores->GPOId=cur->ruid;
            gpores->GPORes=gpodat->getGpoValue();

            allow.clear();
            gpodat->getAlowed(allow);

            for(const uuid::uuid &al : allow){
                for(uuid::uuid member : newSession->_members){
                    if(member==al){
                        gpores->GPORes=gpodat->getGpoValue() ? false : true;
                    }
                }
            }
        }
    }

    // Populate default GPOs that are missing from the backend and create them
    for(size_t i=0; gpo_default[i][0]!=nullptr; ++i){
        uuid::uuid defid(gpo_default[i][0]);
        bool found=false;
        for(const SessionData::GPOResult *cures=&newSession->_GPOResult; cures; cures=cures->next){
            if(cures->GPOId==defid){
                found=true;
                break;
            }
        }
        if(!found){
            bool defval=(strcmp(gpo_default[i][3],"true")==0);
            if(!gpores->GPOId.empty()){
                gpores->next=new SessionData::GPOResult();
                gpores=gpores->next;
            }
            gpores->GPOId=defid;
            gpores->GPORes=defval;
            try {
                GpoData gdat(defid);
                gdat.setName(gpo_default[i][1]);
                gdat.setDesc(gpo_default[i][2]);
                gdat.setGpoValue(defval);
                gpo.create(backend, &gdat);
            } catch (...) {}
        }
    }

    return newSession;
}

void reloadSessionData(AuthBackend &backend, SessionData *cursess){
    Group group;
    std::string username;

    cursess->_members.clear();

    std::shared_ptr<AuthData::Record> cur=std::make_shared<AuthData::Record>();
    size_t rd=sizeof(AuthHeader),end=backend.end();
    while(rd<end){
        cur->type=EmptyData;
        backend.setPos(rd);
        backend.read(reinterpret_cast<unsigned char*>(cur.get()),sizeof(AuthData::Record));
        rd=backend.getPos()+cur->datasize;

        if(cur->type == UserData && strcmp(cur->fieldname,"username")==0
            && uuid::uuid(cur->ruid)==cursess->_uid.value){
            const std::shared_ptr<unsigned char[]>tmp(new unsigned char[cur->datasize]);
            backend.read(tmp.get(),cur->datasize);
            username=reinterpret_cast<char*>(tmp.get());
        }
        if(cur->type == GroupData && strcmp(cur->fieldname,"groupname")==0){
            std::shared_ptr<class GroupData> grpi=std::make_shared<class GroupData>(cur->ruid);
            size_t upos=sizeof(AuthHeader);
            group.info(backend,*grpi,upos);
            if(grpi->isMember(cursess->_uid.value))
                cursess->_members.emplace_back(cur->ruid);
        }
    }
    cursess->_username=username;
}

} // namespace authdb

const authdb::SessionData *authdb::LocalSession::addSession(AuthBackend &backend,uuid::uuid domainid,uuid::uuid userid){
    {
        std::lock_guard<std::mutex> guard(_lock);
        cleanupExpired();
        for (auto &[k, cur] : _sessions) {
            uuid::uuid cur_uid, cur_did;
            cur->getUid(cur_uid);
            cur->getDid(cur_did);
            if (cur_uid == userid && cur_did == domainid)
                return cur;
        }
    }

    SessionData *newSession = createSessionData(backend, domainid, userid);

    std::lock_guard<std::mutex> guard(_lock);
    appendLocal(newSession);
    return newSession;
}

const authdb::SessionData *authdb::LocalSession::getSession(uuid::uuid sessionid){
    std::lock_guard<std::mutex> guard(_lock);
    cleanupExpired();
    return findLocal(sessionid);
}

void authdb::LocalSession::remSession(uuid::uuid sessionid){
    std::lock_guard<std::mutex> guard(_lock);
    removeLocal(sessionid);
}

void authdb::LocalSession::relSession(AuthBackend &backend,uuid::uuid sessionid){
    std::lock_guard<std::mutex> guard(_lock);
    SessionData *cursess = findLocal(sessionid);
    if(!cursess)
        throw AuthBackendError("reload session: sessionid not found !");
    reloadSessionData(backend, cursess);
}

void authdb::LocalSession::listSessions(std::vector<SessionInfo> &out){
    std::lock_guard<std::mutex> guard(_lock);
    cleanupExpired();
    out.clear();
    for (auto &[k, cur] : _sessions) {
        SessionInfo info;
        cur->getSid(info.sid);
        cur->getUid(info.uid);
        cur->getDid(info.did);
        info.username = cur->getUsername();
        out.push_back(std::move(info));
    }
}

// ===== ClusterSession =====

authdb::ClusterSession::ClusterSession() : LocalSession() {}

authdb::ClusterSession::~ClusterSession() {}

const authdb::SessionData *authdb::ClusterSession::addSession(AuthBackend &backend,uuid::uuid domainid,uuid::uuid userid){
    // Check local cache first
    {
        std::lock_guard<std::mutex> guard(_lock);
        cleanupExpired();
        for (auto &[k, cur] : _sessions) {
            uuid::uuid cur_uid, cur_did;
            cur->getUid(cur_uid);
            cur->getDid(cur_did);
            if (cur_uid == userid && cur_did == domainid)
                return cur;
        }
    }

    // Check cluster for existing session (skip when degraded to avoid blocking)
    if (g_Cluster && g_Cluster->isRunning() && !g_Cluster->isDegraded()) {
        uuid::uuid cluster_sid;
        std::vector<uuid::uuid> cluster_members;
        std::string cluster_username;
        std::vector<std::pair<uuid::uuid, bool>> cluster_gpo;
        if (g_Cluster->fetchSession(userid, domainid, cluster_sid,
                                     cluster_members, cluster_username, cluster_gpo)) {
            SessionData *imported = new SessionData(cluster_sid, userid, domainid, cluster_members);
            imported->_username = cluster_username;
            imported->setGPOResults(cluster_gpo);

            std::lock_guard<std::mutex> guard(_lock);
            appendLocal(imported);
            return imported;
        }
    }

    // Create new session
    SessionData *newSession = createSessionData(backend, domainid, userid);

    {
        std::lock_guard<std::mutex> guard(_lock);
        appendLocal(newSession);
    }

    // Push to cluster for replication
    if (g_Cluster && g_Cluster->isRunning()) {
        g_Cluster->pushSession(*newSession);
    }

    return newSession;
}

const authdb::SessionData *authdb::ClusterSession::getSession(uuid::uuid sessionid){
    {
        std::lock_guard<std::mutex> guard(_lock);
        cleanupExpired();
        SessionData *cur = findLocal(sessionid);
        if (cur) {
            if (g_Cluster && g_Cluster->isRunning()) {
                auto age = std::chrono::duration_cast<std::chrono::seconds>(
                    std::chrono::steady_clock::now() - cur->_lastClusterSync).count();
                if (age >= 5) {
                    uuid::uuid c_sid, c_uid, c_did;
                    std::vector<uuid::uuid> c_members;
                    std::string c_username;
                    std::vector<std::pair<uuid::uuid, bool>> c_gpo;
                    if (g_Cluster->fetchSessionLocalBySid(sessionid, c_sid, c_uid, c_did,
                                                          c_members, c_username, c_gpo)) {
                        cur->_members = c_members;
                        cur->_username = c_username;
                        cur->setGPOResults(c_gpo);
                        cur->_lastClusterSync = std::chrono::steady_clock::now();
                    } else {
                        // Session was removed from cluster — invalidate locally
                        removeLocal(sessionid);
                        return nullptr;
                    }
                }
            }
            return cur;
        }
    }

    // Not found locally — fetch from cluster peers
    if (g_Cluster && g_Cluster->isRunning()) {
        uuid::uuid sid, uid, did;
        std::vector<uuid::uuid> members;
        std::string username;
        std::vector<std::pair<uuid::uuid, bool>> gpo_results;
        if (g_Cluster->fetchSessionBySid(sessionid, sid, uid, did, members, username, gpo_results)) {
            SessionData *imported = new SessionData(sid, uid, did, members);
            imported->_username = username;
            imported->setGPOResults(gpo_results);

            std::lock_guard<std::mutex> guard(_lock);
            appendLocal(imported);
            return imported;
        }
    }

    return nullptr;
}

void authdb::ClusterSession::remSession(uuid::uuid sessionid){
    // Remove from cluster on ALL nodes
    if (g_Cluster && g_Cluster->isRunning()) {
        g_Cluster->removeSessionBySid(sessionid);
    }
    // Remove from local cache
    std::lock_guard<std::mutex> guard(_lock);
    removeLocal(sessionid);
}

void authdb::ClusterSession::relSession(AuthBackend &backend,uuid::uuid sessionid){
    {
        std::lock_guard<std::mutex> guard(_lock);
        SessionData *cursess = findLocal(sessionid);

        // Not found locally — try to import from cluster
        if (!cursess && g_Cluster && g_Cluster->isRunning()) {
            uuid::uuid sid, uid, did;
            std::vector<uuid::uuid> members;
            std::string c_username;
            std::vector<std::pair<uuid::uuid, bool>> gpo_results;
            if (g_Cluster->fetchSessionBySid(sessionid, sid, uid, did, members, c_username, gpo_results)) {
                cursess = new SessionData(sid, uid, did, members);
                cursess->_username = c_username;
                cursess->setGPOResults(gpo_results);
                appendLocal(cursess);
            }
        }

        if (!cursess)
            throw AuthBackendError("reload session: sessionid not found !");

        reloadSessionData(backend, cursess);

        // Push updated data to cluster
        if (g_Cluster && g_Cluster->isRunning()) {
            g_Cluster->pushSession(*cursess);
        }
    }
}

void authdb::ClusterSession::listSessions(std::vector<SessionInfo> &out){
    out.clear();

    // Use cluster store as single source of truth
    if (g_Cluster && g_Cluster->isRunning()) {
        std::vector<Cluster::SessionInfo> csessions;
        g_Cluster->listAllSessions(csessions);
        for (const auto &cs : csessions) {
            SessionInfo info;
            info.sid = cs.sid;
            info.uid = cs.uid;
            info.did = cs.did;
            info.username = cs.username;
            out.push_back(std::move(info));
        }
    } else {
        // Fallback: local sessions only
        std::lock_guard<std::mutex> guard(_lock);
        cleanupExpired();
        for (auto &[k, cur] : _sessions) {
            SessionInfo info;
            cur->getSid(info.sid);
            cur->getUid(info.uid);
            cur->getDid(info.did);
            info.username = cur->getUsername();
            out.push_back(std::move(info));
        }
    }
}
