/*******************************************************************************
 * Copyright (c) 2023, 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 <cstring>

#include <httppp/exception.h>
#include <uuidp.h>
#include <chrono>


#include "session.h"
#include <time.h>

std::mutex blogi::Session::_cacheMutex;
std::unordered_map<std::string, std::chrono::steady_clock::time_point> blogi::Session::_sessionCache;

blogi::Session::Session(const dbpp::Database &db) : _sessiondb(db){
}

blogi::Session::~Session(){
}

void blogi::Session::_initSession(){
    dbpp::SQL sql;
    dbpp::DBResult res;

    sql << "CREATE TABLE IF NOT EXISTS session("
    <<   "id " << _sessiondb.getUUIDType(sql) << ","
    <<   "useragent text,"
    <<   "created date NOT NULL,"
    <<   "PRIMARY KEY (id)"
    << ");"
    << "CREATE TABLE IF NOT EXISTS session_data("
    <<   "sessionid " << _sessiondb.getUUIDType(sql) << ","
    <<   "key varchar(255),"
    <<   "value text,"
    <<   "FOREIGN KEY (sessionid) REFERENCES session (id)"
    << ");";

    _sessiondb.exec(sql,res);
}


const std::string &blogi::Session::createSession(std::string &buf,const std::string &useragent){
    uuid::uuid sessionid;

    sessionid.generate();

    buf = sessionid.c_str();

    dbpp::SQL sql;
    dbpp::DBResult res;

    auto now = std::chrono::system_clock::now();
    // Use std::format to directly format the time point
    // The format string can be customized
    std::string ttmp = std::format("{:%a %b %d %T %Y}", now);

    sql << "INSERT INTO session (id,useragent,created) VALUES ('"
          << sessionid.c_str() << "','" << useragent.c_str() << "','"
          << ttmp.c_str() << "');";
    try{
        _sessiondb.exec(sql, res);
    }catch(char *err){
        libhttppp::HTTPException excep;
        excep[libhttppp::HTTPException::Error] << "createSession DbError: " << err;
        throw excep;
    }

    // Pre-populate session cache
    {
        std::lock_guard<std::mutex> lk(_cacheMutex);
        _sessionCache[buf] = std::chrono::steady_clock::now()
                           + std::chrono::seconds(CACHE_TTL_SECONDS);
    }

    return buf;
};

void blogi::Session::addSessionData(const std::string &sessionid,const char *key,const char *value){
    if(sessionid.empty()){
        libhttppp::HTTPException excep;
        excep[libhttppp::HTTPException::Error] << "addSessionData Sessionid could not be zero!";
        throw excep;
    }

    dbpp::DBResult res;

    dbpp::SQL sql;

    std::vector<char> sbuf;

    sql << "INSERT INTO session_data (sessionid,key,value) VALUES ('" << sql.escaped(sbuf,sessionid.c_str()) << "','" << sql.escaped(sbuf,key) << "','" << sql.escaped(sbuf,value) << "');";

    try{
            _sessiondb.exec(sql,res);
    }catch(char *err){
           libhttppp::HTTPException excep;
           excep[libhttppp::HTTPException::Error] << "addSessionData DbError: " << err;
           throw excep;
    }
};

void blogi::Session::delSessionData(const std::string &sessionid, const char* key){
    if(sessionid.empty() || !key){
      libhttppp::HTTPException excep;
        excep[libhttppp::HTTPException::Error] << "delSessionData Sessionid or key could not be zero!";
        throw excep;
    }

    dbpp::DBResult res;

    dbpp::SQL sql;

    std::vector<char> sbuf;

    sql << "DELETE FROM session_data WHERE sessionid='" << sql.escaped(sbuf,sessionid.c_str()) << "' AND key='" << sql.escaped(sbuf,key) << "';";

    _sessiondb.exec(sql,res);
}


void blogi::Session::getSessionData(const std::string &sessionid,const char *key,std::string &value){
    if(sessionid.empty() || !key ){
        libhttppp::HTTPException excep;
        excep[libhttppp::HTTPException::Error] << "getSessionData Sessionid  or key could not be zero!";
        throw excep;
    }

    dbpp::DBResult res;

    dbpp::SQL sql;

    std::vector<char> sbuf;

    sql << "Select value from session_data where sessionid='" << sql.escaped(sbuf,sessionid.c_str()) <<"'  AND key='" << sql.escaped(sbuf,key) <<"';";

    if(_sessiondb.exec(sql,res)>0){
        value=res[0][0];
        return;
    }

    libhttppp::HTTPException excep;
    excep[libhttppp::HTTPException::Error] << "getSessionData key: " << key  << " with sessionid: " << sessionid << "not found!";
    throw excep;
};

bool blogi::Session::exists(const std::string& sessionid){
    if(sessionid.empty()){
        libhttppp::HTTPException excep;
        excep[libhttppp::HTTPException::Error] << "exists Session: sessionid empty !";
        throw excep;
    }

    // Check in-memory cache first
    {
        std::lock_guard<std::mutex> lk(_cacheMutex);
        auto it = _sessionCache.find(sessionid);
        if (it != _sessionCache.end()) {
            if (std::chrono::steady_clock::now() <= it->second)
                return true;
            _sessionCache.erase(it);
        }
    }

    dbpp::DBResult res;
    std::vector<char> sbuf;

    dbpp::SQL sql;
    sql << "Select id from session where id='" <<  dbpp::SQL::escaped(sbuf,sessionid.c_str()) <<"'";

    if(_sessiondb.exec(sql,res)>0) {
        // Cache the result
        std::lock_guard<std::mutex> lk(_cacheMutex);
        _sessionCache[sessionid] = std::chrono::steady_clock::now()
                                 + std::chrono::seconds(CACHE_TTL_SECONDS);
        // Evict expired entries if map grows too large
        if (_sessionCache.size() > 10000) {
            auto now = std::chrono::steady_clock::now();
            for (auto ci = _sessionCache.begin(); ci != _sessionCache.end(); ) {
                if (now > ci->second) ci = _sessionCache.erase(ci);
                else ++ci;
            }
        }
        return true;
    }

    return false;
}

