/*******************************************************************************
 * 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 <thread>

#include "session.h"
#include "authdb.h"
#include "backend.h"
#include "group.h"
#include "user.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::now();
}

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();
    _lastClusterSync=std::chrono::steady_clock::now();
}

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

void authdb::SessionData::clear(){
    _sid.clear();
    _uid.clear();
    _did.clear();
    _members.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{
    auto it = _gpoIndex.find(gpoid);
    if (it != _gpoIndex.end())
        return it->second;
    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;
    _gpoIndex.clear();

    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;
        }
    }
    for(auto &r : results)
        _gpoIndex[r.first] = r.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();
    if(std::chrono::duration_cast<std::chrono::seconds>(now - _lastCleanup).count() < 60)
        return;
    _lastCleanup = 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;

    // Get username via User::info
    User user;
    class UserData udat(userid);
    size_t upos=sizeof(AuthHeader);
    user.info(backend, udat, upos);
    username=udat.getUsername();

    // Collect group memberships
    std::shared_ptr<std::vector<uuid::uuid>> mbs = std::make_shared<std::vector<uuid::uuid>>();
    Group group;
    std::vector<uuid::uuid> gids;
    group.list(backend, gids);
    for(auto &gid : gids){
        std::shared_ptr<class GroupData> grpi=std::make_shared<class GroupData>(gid);
        size_t gpos=sizeof(AuthHeader);
        group.info(backend,*grpi,gpos);
        if(grpi->isMember(userid)){
            mbs->emplace_back(gid);
        }
    }

    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;

    // Evaluate GPOs
    Gpo gpo;
    std::vector<uuid::uuid> allow;
    std::vector<uuid::uuid> gpoids;
    gpo.list(backend, gpoids);

    for(auto &gpoid : gpoids){
        std::shared_ptr<class GpoData> gpodat=std::make_shared<class GpoData>(gpoid);
        size_t gpos2=sizeof(AuthHeader);
        gpo.info(backend,*gpodat,gpos2);
        if(!gpores->GPOId.empty()){
            gpores->next=new SessionData::GPOResult();
            gpores=gpores->next;
        }
        gpores->GPOId=gpoid;
        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;
                }
            }
        }
        newSession->_gpoIndex[gpoid] = gpores->GPORes;
    }

    // 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 = newSession->_gpoIndex.count(defid) > 0;
        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;
            newSession->_gpoIndex[defid] = 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();

    // Get username via User::info
    User user;
    class UserData udat(cursess->_uid.value);
    size_t upos=sizeof(AuthHeader);
    user.info(backend, udat, upos);
    username=udat.getUsername();

    // Collect group memberships
    std::vector<uuid::uuid> gids;
    group.list(backend, gids);
    for(auto &gid : gids){
        std::shared_ptr<class GroupData> grpi=std::make_shared<class GroupData>(gid);
        size_t gpos=sizeof(AuthHeader);
        group.info(backend,*grpi,gpos);
        if(grpi->isMember(cursess->_uid.value))
            cursess->_members.emplace_back(gid);
    }
    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){
    if (!g_Cluster || !g_Cluster->isRunning())
        throw AuthBackendError("cluster not ready — please try again later");
    if (g_Cluster->isCritical())
        throw AuthBackendError("cluster critical — too few nodes online, please try again later");

    // Create new session
    SessionData *newSession = nullptr;
    try {
        newSession = createSessionData(backend, domainid, userid);
    } catch (const std::exception &e) {
        throw;
    }

    uuid::uuid newSid;
    newSession->getSid(newSid);

    // Keep pointer alive for callers (local cache serves immediate lookups)
    {
        std::lock_guard<std::mutex> guard(_lock);
        appendLocal(newSession);
    }

    // Queue async push to cluster — session is already in local cache
    g_Cluster->pushSession(*newSession);

    return newSession;
}

const authdb::SessionData *authdb::ClusterSession::getSession(uuid::uuid sessionid){
    if (!g_Cluster || !g_Cluster->isRunning())
        return nullptr;

    // Check local cache first (covers the gap until async queuePush completes)
    {
        std::lock_guard<std::mutex> guard(_lock);
        SessionData *local = findLocal(sessionid);
        if (local)
            return local;
    }

    if (g_Cluster->isCritical())
        return nullptr;

    // Fetch from cluster
    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);
        removeLocal(sessionid);
        appendLocal(imported);
        return imported;
    }

    return nullptr;
}

void authdb::ClusterSession::remSession(uuid::uuid sessionid){
    if (g_Cluster && g_Cluster->isRunning()) {
        try {
            g_Cluster->removeSessionBySid(sessionid);
        } catch (...) {}
    }
    std::lock_guard<std::mutex> guard(_lock);
    removeLocal(sessionid);
}

void authdb::ClusterSession::relSession(AuthBackend &backend,uuid::uuid sessionid){
    if (!g_Cluster || !g_Cluster->isRunning())
        throw AuthBackendError("reload session: cluster not available");

    // Fetch from cluster
    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))
        throw AuthBackendError("reload session: sessionid not found !");

    SessionData *cursess = new SessionData(sid, uid, did, members);
    cursess->_username = c_username;
    cursess->setGPOResults(gpo_results);

    reloadSessionData(backend, cursess);

    // Push updated data back to cluster
    try {
        g_Cluster->pushSession(*cursess);
    } catch (...) {}

    std::lock_guard<std::mutex> guard(_lock);
    removeLocal(sessionid);
    appendLocal(cursess);
}

void authdb::ClusterSession::listSessions(std::vector<SessionInfo> &out){
    out.clear();
    if (!g_Cluster || !g_Cluster->isRunning()) return;

    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));
    }
}
