#pragma clang diagnostic push
#pragma ide diagnostic ignored "readability-make-member-function-const"
#pragma ide diagnostic ignored "readability-convert-member-functions-to-static"

#include <chrono>
#include <iomanip>

#include <botan/argon2.h>
#include <botan/uuid.h>
#include <botan/totp.h>

#if defined(BOTAN_HAS_SYSTEM_RNG)
#include <botan/system_rng.h>
#else
#include <botan/auto_rng.h>
#endif

#include <jwt-cpp/traits/kazuho-picojson/traits.h>
#include <jwt-cpp/jwt.h>
#include <SMTPMail.h>

#include "controllers/controllers.h"
#include "db/db.h"


namespace api {
#if defined(BOTAN_HAS_SYSTEM_RNG)
    std::unique_ptr<Botan::RNG> auth::rng = std::make_unique<Botan::System_RNG>();
#else
    std::unique_ptr<Botan::RNG> auth::rng = std::make_unique<Botan::AutoSeeded_RNG>();
#endif

    bool auth::verify2fa(const db::User& user, uint32_t totp) {
        size_t allowed_skew = db::User_getEnumTfaType(user) == db::tfaTypes::TOTP ? 0 : 10;
        const auto& totp_secret = (const std::vector<uint8_t>&) user.getValueOfTfaSecret();
        return Botan::TOTP(Botan::OctetString(totp_secret)).verify_totp(totp, std::chrono::system_clock::now(), allowed_skew);
    }

    void auth::send_mail(const db::User& user) {
        std::time_t t = std::time(nullptr);
        const auto& totp_secret = (const std::vector<uint8_t>&) user.getValueOfTfaSecret();
        char totp[16];
        std::snprintf(totp, 16, "%06d", Botan::TOTP(Botan::OctetString(totp_secret)).generate_totp(t));

        auto config = drogon::app().getCustomConfig();

        drogon::app().getPlugin<SMTPMail>()->sendEmail(
                config["smtp_server"].asString(),
                (uint16_t)config["smtp_port"].asUInt64(),
                "fileserver@mattv.de",
                user.getValueOfName(),
                "MFileserver - Email 2fa code",
                "Your code is: " + std::string(totp) +"\r\nIt is valid for 5 Minutes",
                config["smtp_user"].asString(),
                config["smtp_password"].asString(),
                false
        );
    }

    std::string auth::get_token(const db::User& user) {
        auto db = drogon::app().getDbClient();

        db::MapperToken token_mapper(db);
        const auto iat = std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now().time_since_epoch());
        const auto exp = iat + std::chrono::hours{24};

        db::Token new_token;
        new_token.setOwnerId(user.getValueOfId());
        new_token.setExp(exp.count());

        token_mapper.insert(new_token);

        return jwt::create<jwt::traits::kazuho_picojson>()
                .set_type("JWT")
                .set_payload_claim("sub", picojson::value((int64_t)user.getValueOfId()))
                .set_payload_claim("jti", picojson::value((int64_t)new_token.getValueOfId()))
                .set_issued_at(std::chrono::system_clock::from_time_t(iat.count()))
                .set_expires_at(std::chrono::system_clock::from_time_t(exp.count()))
                .sign(jwt::algorithm::hs256{jwt_secret});
    }

    void auth::generate_root(db::User& user) {
        db::MapperUser user_mapper(drogon::app().getDbClient());

        auto node = fs::create_node("", user, false, std::nullopt, true);
        user.setRootId(std::get<db::INode>(node).getValueOfId());
        user_mapper.update(user);
    }

    void auth::revoke_all(const db::User& user) {
        db::MapperToken token_mapper(drogon::app().getDbClient());
         token_mapper.deleteBy(db::Criteria(db::Token::Cols::_owner_id, db::CompareOps::EQ, user.getValueOfId()));
    }
}

#pragma clang diagnostic pop