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

#ifdef Windows
#define WIN32_LEAN_AND_MEAN
#include <time.h>
#else
#include <sys/time.h>
#endif

#include <httppp/exception.h>

#include "auth.h"

#include "conf.h"
#include "theme.h"
#include "session.h"
#include <authdb/user.h>

namespace blogi {
  const uuid::uuid emptyuuid;
};

blogi::Auth::Auth(std::vector<std::unique_ptr<dbpp::Database>> &pcon,const blogi::Config &cfg) : _dbconn(pcon), _config(cfg){
    dbpp::SQL sql;
    dbpp::DBResult res;
    sql << "CREATE TABLE IF NOT EXISTS users ("
        <<      "id integer PRIMARY KEY " << pcon[0]->autoincrement(sql) << ","
        <<      "sid character varying(255),"
        <<      "username character varying(255),"
        <<      "email character varying(255),"
        <<      "displayname character varying(255) NOT NULL,"
        <<      "authprovider integer DEFAULT 0 NOT NULL,"
        <<      "password character varying(255)"
        << ");";
    _dbconn[0]->exec(sql,res);
}

blogi::Auth::~Auth(){

}

void blogi::Auth::_doLogin(const AuthSource &src, const char *username, const char *password,
                           uuid::uuid &authid, const int tid, const std::string *ssid){
    libhttppp::HTTPException httpException;

    authdb::client::ClientConnection authcon;
    authcon.setUrl(src.url);
    authcon.setClientName(src.clientName);
    authcon.setClientSecret(src.clientSecret);

    authdb::client::Client clt(authcon);

    try {
        if(!clt.ClientAuth()){
            httpException[libhttppp::HTTPException::Error] << "ClientAuth failed!";
            throw httpException;
        }
    } catch (libhttppp::HTTPException&) {
        throw;
    } catch (std::exception &e) {
        httpException[libhttppp::HTTPException::Error] << "ClientAuth error: " << e.what();
        throw httpException;
    }

    authcon.setUsername(username);
    authcon.setUserPassword(password);

    try {
        bool ret = clt.UserAuth();
        if(!ret) {
            httpException[libhttppp::HTTPException::Error] << "UserAuth failed (Invalid credentials)!";
            throw httpException;
        }
    } catch (libhttppp::HTTPException&) {
        throw;
    } catch (std::exception &e) {
        httpException[libhttppp::HTTPException::Error] << "UserAuth error: " << e.what();
        throw httpException;
    }

    try {
        bool ret = clt.GPOcheck("e7d3b8b3-4825-11f0-ae2b-3cecefce9cb6");

        if(ret){
            if(ssid){
                Session sess(*_dbconn[tid]);
                
sess.delSessionData(*ssid, "authid");
sess.addSessionData(*ssid,"authid",authcon.getSessionID().c_str());

                class authdb::SessionData sdat;
                clt.SessionInfo(sdat);

                uuid::uuid uid;
                sdat.getUid(uid);
                
sess.delSessionData(*ssid, "uid");
sess.addSessionData(*ssid,"uid",uid.c_str());
            }
            authid = authcon.getSessionID();
        } else {
            clt.remSession();
            httpException[libhttppp::HTTPException::Error] << "User GPO check failed!";
            throw httpException;
        }
    } catch (authdb::AuthBackendError &e) {
        try {
            clt.remSession();
        } catch (authdb::AuthBackendError &cleanupE) {
        }
        httpException[libhttppp::HTTPException::Error] << "Authentication backend error: " << e.what();
        throw httpException;
    }
}

void blogi::Auth::login(const int tid,const char* username, const char* password,uuid::uuid &authid, const std::string &ssid, const std::string &domain){
    libhttppp::HTTPException httpException;

    // Parse user@domain if no explicit domain provided
    std::string user(username);
    std::string dom = domain;
    if(dom.empty()){
        auto atpos = user.rfind('@');
        if(atpos != std::string::npos){
            dom = user.substr(atpos + 1);
            user = user.substr(0, atpos);
        }
    }

    const AuthSource *src = _config.findAuthSource(dom);
    if(!src){
        httpException[libhttppp::HTTPException::Error] << "No auth source found for domain: " << dom;
        throw httpException;
    }

    _doLogin(*src, user.c_str(), password, authid, tid, &ssid);
}

void blogi::Auth::Apilogin(const int tid,const char *username,const char *password,uuid::uuid &authid, const std::string &domain){
    libhttppp::HTTPException httpException;

    // Parse user@domain if no explicit domain provided
    std::string user(username);
    std::string dom = domain;
    if(dom.empty()){
        auto atpos = user.rfind('@');
        if(atpos != std::string::npos){
            dom = user.substr(atpos + 1);
            user = user.substr(0, atpos);
        }
    }

    const AuthSource *src = _config.findAuthSource(dom);
    if(!src){
        httpException[libhttppp::HTTPException::Error] << "No auth source found for domain: " << dom;
        throw httpException;
    }

    _doLogin(*src, user.c_str(), password, authid, tid, nullptr);
}

bool blogi::Auth::isLoggedIn(const int tid,const uuid::uuid &authid){
      for(size_t i = 0; i < _config.getAuthSourceCount(); ++i){
          const AuthSource &src = _config.getAuthSource(i);
          try {
              authdb::client::ClientConnection authcon;
              authcon.setUrl(src.url);
              authcon.setClientName(src.clientName);
              authcon.setClientSecret(src.clientSecret);
              authcon.setSessionID(authid);

              std::unique_ptr<authdb::client::Client> clt;
              try{
                    clt=std::make_unique<authdb::client::Client>(authcon);
              }catch(...){
                    authcon.reConnect();
                    clt=std::make_unique<authdb::client::Client>(authcon);
              }

              if(clt->ClientAuth() && clt->GPOcheck("e7d3b8b3-4825-11f0-ae2b-3cecefce9cb6")){
                    return true;
              }
          } catch(...) {
              continue;
          }
      }
      return false;
}

bool blogi::Auth::isLoggedIn(const int tid,libhttppp::HttpRequest &curreq,const std::string &ssid){
        try{
            libhttppp::HttpCookie cook;

            cook.parse(curreq);

            uuid::uuid sessid;

            for(libhttppp::HttpCookie::CookieData *curcook=cook.getfirstCookieData(); curcook; curcook=curcook->nextCookieData()){
                if(curcook->getKey()=="authid"){
                    sessid=curcook->getValue();
                }
            }

            if(sessid.empty())
                return false;
            return isLoggedIn(tid,sessid);
        }catch(...){
            return false;
        }
}

int blogi::Auth::getAuthorId(const int tid, const uuid::uuid &authid) {
    for (size_t si = 0; si < _config.getAuthSourceCount(); ++si) {
        const AuthSource &src = _config.getAuthSource(si);
        try {
            authdb::client::ClientConnection authcon;
            authcon.setUrl(src.url);
            authcon.setClientName(src.clientName);
            authcon.setClientSecret(src.clientSecret);
            authcon.setSessionID(authid);

            authdb::client::Client clt(authcon);
            if (!clt.ClientAuth()) continue;

            authdb::SessionData sdat;
            clt.SessionInfo(sdat);
            uuid::uuid uid;
            sdat.getUid(uid);
            class authdb::UserData ud(uid);
            clt.UserInfo(ud);
            
            std::string username = ud.getUsername();
            std::string displayname = ud.getFirstname() + " " + ud.getLastname();
            std::string email = ud.getMail();

            std::vector<char> sbuf;
            dbpp::SQL asql;
            dbpp::DBResult ares;
            asql << "SELECT id FROM users WHERE sid='" << dbpp::SQL::escaped(sbuf, uid.c_str()) << "' LIMIT 1;";
            int ucount = _dbconn[tid]->exec(asql, ares);
            if (ucount > 0) {
                int authorId = atoi(ares[0][0]);
                // Update displayname in case it changed
                dbpp::SQL usql;
                dbpp::DBResult ures;
                usql << "UPDATE users SET displayname='" << dbpp::SQL::escaped(sbuf, displayname.c_str())
                     << "', username='" << dbpp::SQL::escaped(sbuf, username.c_str())
                     << "', email='" << dbpp::SQL::escaped(sbuf, email.c_str())
                     << "' WHERE id=" << authorId << ";";
                try { _dbconn[tid]->exec(usql, ures); } catch (...) {}
                return authorId;
            } else {
                dbpp::SQL isql;
                dbpp::DBResult ires;
                isql << "INSERT INTO users (sid,username,email,displayname) VALUES ('"
                     << dbpp::SQL::escaped(sbuf, uid.c_str()) << "','"
                     << dbpp::SQL::escaped(sbuf, username.c_str()) << "','"
                     << dbpp::SQL::escaped(sbuf, email.c_str()) << "','"
                     << dbpp::SQL::escaped(sbuf, displayname.c_str()) << "') RETURNING id;";
                _dbconn[tid]->exec(isql, ires);
                return atoi(ires[0][0]);
            }
        } catch (...) {
            continue;
        }
    }
    return 0;
}
