#include "PgUserAuth.hpp"

#include <sqlpp11/sqlpp11.h>

#include <eedb/auth/Services.hpp>
#include <eedb/db/connection.hpp>
#include <eedb/model/auth_identity.h>
#include <eedb/model/auth_info.h>
#include <eedb/model/auth_token.h>
#include <eedb/model/user.h>
#include <eedb/model/user_action.h>
#include <eedb/model/user_audit.h>

#include <algorithm>
#include <cctype>
#include <functional>
#include <iostream>
#include <random>
#include <string>

#include <Wt/Auth/AuthService>
#include <Wt/Auth/Dbo/AuthInfo>
#include <Wt/WEnvironment>
#include <Wt/WLogger>

#include <tao/json.hh>
#include <tao/json/value.hh>

using namespace sqlpp;
using namespace Wt::Auth;
using namespace std::string_literals;

// enum LoginActions { Login, Logout };
// static std::array< std::string_view, 2 > UserActionNames = {{"login", "logout"}};

std::string RandomString(int len) {
    using namespace std;
    srand(time(0));
    string str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
    string newstr;
    int pos;
    while(newstr.size() != len) {
        pos = ((rand() % (str.size() - 1)));
        newstr += str.substr(pos, 1);
    }
    return newstr;
}

template < typename Connection >
struct TransactionGuard : public Wt::Auth::AbstractUserDatabase::Transaction {
    TransactionGuard(Connection & c) : _c(c) {
        _c.native()->start_transaction();
    }

    void commit() override {
        _c.native()->commit_transaction();
    }
    void rollback() override {
        _c.native()->rollback_transaction();
    }

  private:
    Connection & _c;
};

namespace eedb::auth {

namespace {
    constexpr eedb::user t_user;
    constexpr eedb::user_audit t_user_history;
    constexpr eedb::user_action t_user_action;
    constexpr eedb::auth_identity t_identity;
    constexpr eedb::auth_info t_info;
    constexpr eedb::auth_token t_auth_token;

    static const auto auth_info_identity = t_info.join(t_identity).on(t_info.id == t_identity.auth_info_id);
    static const auto user_auth_info     = t_user.join(t_info).on(t_info.user_uid == t_user.uid);
    static const auto auth_token_info    = user_auth_info.join(t_auth_token).on(t_auth_token.auth_info_id == t_info.id);

    static const auto select_login_action_id = select(t_user_action.id)                //
                                                 .from(t_user_action)                  //
                                                 .where(t_user_action.name == "login") //
                                                 .limit(1u);
    static const auto select_logout_action_id = select(t_user_action.id)                 //
                                                  .from(t_user_action)                   //
                                                  .where(t_user_action.name == "logout") //
                                                  .limit(1u);
}

PgUserAuth::~PgUserAuth() {}

PgUserAuth::PgUserAuth(eedb::db::PgConnection & _db, const Wt::WEnvironment & env) : db(_db), _env(env) {
    this->setAuthService(eedb::auth::Services::authService());
}

User PgUserAuth::findWithId(const std::string & id) const {
    const auto uid_eq = t_user.uid == std::atoi(id.c_str());

    if(db(select(exists(select(t_user.uid) //
                          .from(t_user)    //
                          .where(uid_eq))))
         .front()
         .exists) {
        return User(id, *this);
    }
    return User();
}

User PgUserAuth::findWithIdentity(const std::string & provider, const Wt::WString & identity) const {
    auto _identity = identity.toUTF8();
    if(_authService && _authService->identityPolicy() == EmailAddressIdentity) {
        std::transform(_identity.begin(), _identity.end(), _identity.begin(), ::tolower);
    }

    const auto identity_eq = t_identity.identity == _identity;
    const auto provider_eq = t_identity.provider == provider;

    const auto result = db(select(t_info.user_uid)     //
                             .from(auth_info_identity) //
                             .where(provider_eq and identity_eq));

    if(!result.empty()) {
        return User(std::to_string(result.front().user_uid), *this);
    }
    return User();
}

void PgUserAuth::addIdentity(const User & user, const std::string & provider, const Wt::WString & identity) {
    const auto uid_eq      = t_info.user_uid == std::atoi(user.id().c_str());
    const auto identity_eq = t_identity.identity == identity.toUTF8();
    const auto provider_eq = t_identity.provider == provider;

    auto res = db(select(t_info.user_uid, t_info.id) //
                    .from(auth_info_identity)
                    .where(identity_eq and provider_eq and uid_eq));

    if(!res.empty()) {
        Wt::log("error") << "cannot add identity " << provider << ":'" << identity << "': already exists";
        return;
    }

    auto auth_info_id = db(select(t_info.id)
                             .from(t_info) //
                             .where(uid_eq));

    if(auth_info_id.empty()) {
        throw std::exception();
    }

    db(insert_into(t_identity)
         .set(t_identity.identity  = identity.toUTF8(), //
           t_identity.provider     = provider,          //
           t_identity.auth_info_id = auth_info_id.front().id));

    Wt::log("info") << "created new identity for user";
}

void PgUserAuth::setIdentity(const User & user, const std::string & provider, const Wt::WString & identity) {
    const auto uid         = std::atoi(user.id().c_str());
    const auto provider_eq = t_identity.provider == provider;

    db(update(t_identity)                              //
         .set(t_identity.identity = identity.toUTF8()) //
         .where(t_identity.auth_info_id == select(t_info.id).from(t_info).where(t_info.user_uid == uid) and provider_eq));
}

Wt::WString PgUserAuth::identity(const User & user, const std::string & provider) const {
    const int uid          = std::atoi(user.id().c_str());
    const auto id_eq       = t_info.user_uid == uid;
    const auto provider_eq = t_identity.provider == provider;

    auto res = db(select(t_identity.identity) //
                    .from(auth_info_identity) //
                    .where(id_eq and provider_eq));

    if(res.empty())
        return Wt::WString::Empty;
    return {res.front().identity};
}

void PgUserAuth::removeIdentity(const User & user, const std::string & provider) {
    const auto id_eq       = t_identity.id == std::atoi(user.id().c_str());
    const auto provider_eq = t_identity.provider == provider;

    db(remove_from(t_identity).where(id_eq and provider_eq));
}

User PgUserAuth::registerNew() {
    auto user_id = db(sqlpp::postgresql::insert_into(t_user)    //
                        .set(                                   //
                          t_user.full_name = RandomString(256), //
                          t_user.status    = -1)
                        .returning(t_user.uid))
                     .front()
                     .uid;

    db(insert_into(t_info).set(         //
      t_info.password_hash   = "NONE",  //
      t_info.password_method = "NONE",  //
      t_info.password_salt   = "NONE",  //
      t_info.user_uid        = user_id, //
      t_info.email           = RandomString(20) + "@NONE.org"));

    return User{std::to_string(user_id), *this};
}

void PgUserAuth::deleteUser(const User & user) {
    db(remove_from(t_info) //
         .where(t_info.id == std::atoi(user.id().c_str())));
}

// User::Status PgUserAuth::status(const User & user) const {
//    const auto id_eq = auth_identity.id == std::atoi(user.id().c_str());

//    auto status = db(select(auth_info.status.as(user_status)) //
//                       .from(join)                            //
//                       .where(id_eq))
//                    .front()
//                    .user_status;
//    return status.value() == User::Status::Normal ? User::Status::Normal : User::Status::Disabled;
//}

// void PgUserAuth::setStatus(const User & user, User::Status status) {
//    db(update(auth_info)                                     //
//         .set(auth_info.status = static_cast< int >(status)) //
//         .where(auth_info.id == std::atoi(user.id().c_str())));
//}

void PgUserAuth::setPassword(const User & user, const PasswordHash & password) {
    db(update(t_info)
         .set(                                           //
           t_info.password_hash   = password.value(),    //
           t_info.password_method = password.function(), //
           t_info.password_salt   = password.salt())
         .where(t_info.user_uid == std::atoi(user.id().c_str())));
}

PasswordHash PgUserAuth::password(const User & user) const {
    const auto id_eq = t_info.user_uid == std::atoi(user.id().c_str());

    auto row = db(select(t_info.password_hash, t_info.password_method, t_info.password_salt)
                    .from(t_info) //
                    .where(id_eq));
    if(row.empty())
        return PasswordHash();
    auto & front = row.front();
    return {std::move(front.password_method), std::move(front.password_salt), std::move(front.password_hash)};
}

bool PgUserAuth::setEmail(const User & user, const std::string & address) {
    const auto uid_eq = t_info.user_uid == std::atoi(user.id().c_str());

    db(update(t_info) //
         .set(        //
           t_info.email          = address,
           t_info.email_verified = true) //
         .where(uid_eq));
    return true;
}

std::string PgUserAuth::email(const User & user) const {
    auto ret = db(select(t_info.email) //
                    .from(t_info)      //
                    .where(t_info.user_uid == std::atoi(user.id().c_str()) and t_info.email_verified == true));
    if(ret.empty())
        return {};
    return std::move(ret.front().email);
}

void PgUserAuth::setUnverifiedEmail(const User & user, const std::string & address) {
    // orginal implementation of UserDatabase (provided by WT team) sets veryfied and unverified emails in
    // different fields in database. So in order to verify email, they just copy the unverified_email to email and then
    // set unverified_email to empty string (db don't allow empty strings as email*)
    if(address.empty())
        db(update(t_info)                       //
             .set(t_info.email_verified = true) //
             .where(t_info.user_uid == std::atoi(user.id().c_str())));
    else
        db(update(t_info)                       //
             .set(                              //
               t_info.email          = address, //
               t_info.email_verified = false)   //
             .where(t_info.user_uid == std::atoi(user.id().c_str())));
}

std::string PgUserAuth::unverifiedEmail(const User & user) const {
    auto ret = db(select(t_info.email) //
                    .from(t_info)      //
                    .where(t_info.user_uid == std::atoi(user.id().c_str()) and t_info.email_verified == false));
    if(ret.empty())
        return {};
    return std::move(ret.front().email);
}

User PgUserAuth::findWithEmail(const std::string & address) const {
    auto ret = db(select(t_info.user_uid) //
                    .from(t_info)         //
                    .where(t_info.email == address and t_info.email_verified == true));

    if(ret.empty())
        return {};
    return {std::to_string(ret.front().user_uid), *this};
}

void PgUserAuth::setEmailToken(const User & user, const Token & token, User::EmailTokenRole role) {
    auto exp = ::date::floor<::std::chrono::milliseconds >(std::chrono::system_clock::from_time_t(token.expirationTime().toTime_t()));
    db(update(t_info)                                 //
         .set(                                        //
           t_info.email_token         = token.hash(), //
           t_info.email_token_expires = exp,
           t_info.email_token_role    = static_cast< int >(role)) //
         .where(t_info.user_uid == std::atoi(user.id().c_str())));
}

Token PgUserAuth::emailToken(const User & user) const {
    auto ret = db(select(t_info.email_token, t_info.email_token_expires) //
                    .from(t_info)                                        //
                    .where(t_info.user_uid == std::atoi(user.id().c_str())));
    if(ret.empty())
        return {};
    auto exp     = Wt::WDateTime();
    auto time    = ret.front().email_token_expires.value();
    auto systime = std::chrono::system_clock::to_time_t(time);
    exp.setTime_t(systime);
    return {ret.front().email_token, exp};
}

User::EmailTokenRole PgUserAuth::emailTokenRole(const User & user) const {
    auto ret = db(select(t_info.email_token_role) //
                    .from(t_info)                 //
                    .where(t_info.user_uid == std::atoi(user.id().c_str())));
    if(ret.empty())
        throw std::exception();
    auto val = ret.front().email_token_role;
    return static_cast< User::EmailTokenRole >(val.value());
}

User PgUserAuth::findWithEmailToken(const std::string & hash) const {
    auto ret = db(select(t_user.uid)      //
                    .from(user_auth_info) //
                    .where(t_info.email_token == hash));

    if(ret.empty())
        return {};
    return {std::to_string(ret.front().uid), *this};
}

void PgUserAuth::addAuthToken(const User & user, const Token & token) {
    auto select_identity_id = select(t_info.id) //
                                .from(t_info)
                                .where(t_info.user_uid == std::atoi(user.id().c_str()))
                                .limit(1u);
    db(insert_into(t_auth_token)                                                                     //
         .set(                                                                                       //
           t_auth_token.auth_info_id = select_identity_id,                                           //
           t_auth_token.expires      = std::chrono::system_clock::now() + ::sqlpp::chrono::days{14}, //
           t_auth_token.value        = token.hash()));
}

void PgUserAuth::removeAuthToken(const User &, const std::string & hash) {
    db(remove_from(t_auth_token).where(t_auth_token.value == hash));
}

User PgUserAuth::findWithAuthToken(const std::string & hash) const {
    auto fullUser = t_auth_token //
                      .join(t_info)
                      .on(t_info.id == t_auth_token.auth_info_id);

    auto ret = db(select(t_info.user_uid) //
                    .from(fullUser)       //
                    .where(t_auth_token.value == hash and t_auth_token.expires > std::chrono::system_clock::now()));

    if(ret.empty())
        return {};
    return {std::to_string(ret.front().user_uid), *this};
}

int PgUserAuth::updateAuthToken(const User & user, const std::string & oldhash, const std::string & newhash) {
    // method called only after successful login
    using namespace std::chrono;
    const auto identity_id = db(select(t_info.id) //
                                  .from(t_info)   //
                                  .where(t_info.user_uid == std::atoi(user.id().c_str())));
    if(identity_id.empty())
        return 0;

    const auto expires = db(sqlpp::postgresql::update(t_auth_token) //
                              .set(t_auth_token.value = newhash)
                              .where(t_auth_token.value == oldhash)
                              .returning(t_auth_token.expires));

    if(expires.empty())
        return 0;

    const tao::json::value data{
      {"status"s, "success"},                  //
      {"method"s, "token"},                    //
      {"user_address"s, _env.clientAddress()}, //
      {"user_agent"s, _env.userAgent()},       //
      {"user_locale"s, _env.locale().name()},  //
      {"referer", _env.referer()},             //
      {"url_scheme", _env.urlScheme()}         //
    };

    db(insert_into(t_user_history)
         .set(t_user_history.user_id = std::atoi(user.id().c_str()),
           t_user_history.action_id  = select_login_action_id, //
           t_user_history.data       = tao::json::to_string(data)));

    const auto now  = system_clock::now();
    const auto diff = expires.front().expires.value() - now;
    return duration_cast< duration< int > >(diff).count();
}

void PgUserAuth::setFailedLoginAttempts(const User & user, int count) {
    const auto getStatus = [count]() { return count ? "failed"s : "success"s; };

    const tao::json::value data{
      {"status"s, getStatus()},                //
      {"method"s, "password"},                 //
      {"user_address"s, _env.clientAddress()}, //
      {"user_agent"s, _env.userAgent()},       //
      {"user_locale"s, _env.locale().name()},  //
      {"referer", _env.referer()},             //
      {"url_scheme", _env.urlScheme()}         //
    };

    db(insert_into(t_user_history)
         .set(t_user_history.user_id = std::atoi(user.id().c_str()),
           t_user_history.action_id  = select_login_action_id, //
           t_user_history.data       = tao::json::to_string(data)));
}

template < typename T >
inline std::string name_of(const T &) {
    static_assert(sqlpp::is_column_t< T >::value, "T should by a calumn");
    return std::string{sqlpp::name_of< typename T::_table >::char_ptr()} + '.' + sqlpp::name_of< T >::char_ptr();
}

int PgUserAuth::failedLoginAttempts(const User & user) const {
    const auto res =
      db(select(count(t_user_history.id))                                                                                         //
           .from(t_user_history)                                                                                                  //
           .where(t_user_history.user_id == std::atoi(user.id().c_str()) and                                                      //
             t_user_history.action_id == select_login_action_id and                                                               //
             sqlpp::verbatim< sqlpp::boolean >(name_of(t_user_history.data) + "->>'status' = 'failed'") and                       //
             t_user_history.id > select(t_user_history.id)                                                                        //
                                   .from(t_user_history)                                                                          //
                                   .where(t_user_history.user_id == std::atoi(user.id().c_str()) and                              //
                                     t_user_history.action_id == select_login_action_id and                                       //
                                     sqlpp::verbatim< sqlpp::boolean >(name_of(t_user_history.data) + "->>'status' = 'success'")) //
                                   .limit(1u)));
    return static_cast< int >(res.front().count);
}

void PgUserAuth::setLastLoginAttempt(const User &, const Wt::WDateTime &) {}

Wt::WDateTime PgUserAuth::lastLoginAttempt(const User & user) const {
    using namespace std::chrono;
    const auto res =
      db(select(t_user_history._when)                                                                                            //
           .from(t_user_history)                                                                                                 //
           .where(t_user_history.action_id == select_login_action_id and t_user_history.user_id == std::atoi(user.id().c_str())) //
           .order_by(t_user_history.id.desc())                                                                                   //
           .limit(1u));
    if(res.empty())
        return {};

    const auto time    = res.front()._when.value();
    const auto systime = system_clock::to_time_t(time);
    auto lastLogin     = Wt::WDateTime();
    lastLogin.setTime_t(systime);
    return lastLogin;
}

void PgUserAuth::logout(const User & user) {
    const tao::json::value data{{"status"s, "success"}};

    db(insert_into(t_user_history)
         .set(t_user_history.user_id = std::atoi(user.id().c_str()),
           t_user_history.action_id  = select_logout_action_id, //
           t_user_history.data       = tao::json::to_string(data)));
}

AbstractUserDatabase::Transaction * PgUserAuth::startTransaction() {
    return new TransactionGuard< decltype(db) >(db);
}
}
