/*
 * SPDX-License-Identifier: GPL-3.0-or-later
 * SPDX-FileCopyrightText: 2020-2021 Johan Ouwerkerk <jm.ouwerkerk@gmail.com>
 */
#include "account_p.h"
#include "validation.h"

#include "../logging_p.h"

#include <QObject>
#include <QTimer>

KEYSMITH_LOGGER(logger, ".accounts.account_p")

namespace accounts
{
QUuid AccountPrivate::id(void) const
{
    return m_id;
}

std::optional<uint> AccountPrivate::offset(void) const
{
    return m_offset;
}

QString AccountPrivate::name(void) const
{
    return m_name;
}

QString AccountPrivate::issuer(void) const
{
    return m_issuer;
}

QString AccountPrivate::token(void) const
{
    return m_token;
}

quint64 AccountPrivate::counter(void) const
{
    return m_counter;
}

QDateTime AccountPrivate::epoch(void) const
{
    return m_epoch;
}

uint AccountPrivate::timeStep(void) const
{
    return m_timeStep;
}

Account::Hash AccountPrivate::hash(void) const
{
    return m_hash;
}

Account::Algorithm AccountPrivate::algorithm(void) const
{
    return m_algorithm;
}

int AccountPrivate::tokenLength(void) const
{
    return m_tokenLength;
}

bool AccountPrivate::checksum(void) const
{
    return m_checksum;
}

void AccountPrivate::setCounter(quint64 counter)
{
    Q_Q(Account);
    if (!m_is_still_alive) {
        qCDebug(logger) << "Will not set counter for account:" << m_id << "Object marked for death";
        return;
    }

    if (!m_storage->isStillOpen()) {
        qCDebug(logger) << "Will not set counter for account:" << m_id << "Storage no longer open";
        return;
    }

    if (m_algorithm != Account::Algorithm::Hotp) {
        qCDebug(logger) << "Will not set counter for account:" << m_id << "Algorithm not applicable:" << m_algorithm;
        return;
    }

    qCDebug(logger) << "Requesting to store updated details for account:" << m_id;
    SaveHotp *job = new SaveHotp(m_storage->settings(), m_id, m_name, m_issuer, m_secret, m_tokenLength, counter, m_offset, m_checksum);
    m_actions->queueAndProceed(job, [counter, job, q, this](void) -> void {
        new HandleCounterUpdate(this, m_storage, counter, job, q);
    });
}

void AccountPrivate::acceptCounter(quint64 counter)
{
    Q_Q(Account);
    if (!m_is_still_alive) {
        qCDebug(logger) << "Ignoring counter update for account:" << m_id << "Object marked for death";
        return;
    }

    if (!m_storage->isStillOpen()) {
        qCDebug(logger) << "Ignoring counter update for account:" << m_id << "Storage no longer open";
        return;
    }

    if (m_algorithm != Account::Algorithm::Hotp) {
        qCDebug(logger) << "Ignoring counter update for account:" << m_id << "Algorithm not applicable:" << m_algorithm;
        return;
    }

    if (m_counter != counter) {
        qCDebug(logger) << "Counter is updated for account:" << m_id;
        m_counter = counter;
        Q_EMIT q->updated();
        recompute();
    }
}

void AccountPrivate::acceptTotpTokens(const QString &token, const QString &nextToken, const QDateTime &validFrom, const QDateTime &validUntil)
{
    if (!m_is_still_alive) {
        qCDebug(logger) << "Ignoring token update for account:" << m_id << "Object marked for death";
        return;
    }

    if (!m_storage->isStillOpen()) {
        qCDebug(logger) << "Ignoring token update for account:" << m_id << "Storage no longer open";
        return;
    }

    m_nextToken = nextToken;
    m_nextTotpValidFrom = validFrom;
    m_nextTotpValidUntil = validUntil;
    setToken(token);
}

void AccountPrivate::acceptHotpTokens(const QString &token, const QString &nextToken, quint64 nextCounter)
{
    if (!m_is_still_alive) {
        qCDebug(logger) << "Ignoring token update for account:" << m_id << "Object marked for death";
        return;
    }

    if (!m_storage->isStillOpen()) {
        qCDebug(logger) << "Ignoring token update for account:" << m_id << "Storage no longer open";
        return;
    }

    m_nextToken = nextToken;
    m_nextCounter = nextCounter;
    setToken(token);
}

void AccountPrivate::setToken(const QString &token)
{
    Q_Q(Account);
    if (m_token != token) {
        qCDebug(logger) << "Token is updated for account:" << m_id;
        m_token = token;
        Q_EMIT q->tokenChanged(m_token);
    }
}

void AccountPrivate::shiftTokens(void)
{
    if (m_nextToken.isEmpty()) {
        qCDebug(logger) << "Not shifting to next token for account:" << m_id << "It is not yet available";
        return;
    }

    QDateTime now = QDateTime::currentDateTime();
    switch (m_algorithm) {
    case Account::Algorithm::Hotp:
        if (m_counter != m_nextCounter) {
            qCDebug(logger) << "Not shifting to next token for account:" << m_id << "It is not valid anymore";
            return;
        }
        break;
    case Account::Algorithm::Totp:
        if (now < m_nextTotpValidFrom || now > m_nextTotpValidUntil) {
            qCDebug(logger) << "Not shifting to next token for account:" << m_id << "It is not valid at this time";
            return;
        }
        break;
    default:
        Q_ASSERT_X(false, Q_FUNC_INFO, "unknown algorithm value");
        return;
    }

    setToken(m_nextToken);
}

void AccountPrivate::recompute(void)
{
    Q_Q(Account);
    if (!m_is_still_alive) {
        qCDebug(logger) << "Will not recompute token for account:" << m_id << "Object marked for death";
        return;
    }

    if (!m_storage->isStillOpen()) {
        qCDebug(logger) << "Will not recompute token for account:" << m_id << "Storage no longer open";
        return;
    }

    shiftTokens();

    qCDebug(logger) << "Requesting recomputed tokens for account:" << m_id;
    ComputeHotp *hotpJob = nullptr;
    ComputeTotp *totpJob = nullptr;

    switch (m_algorithm) {
    case Account::Algorithm::Hotp:
        hotpJob = new ComputeHotp(m_storage->secret(), m_secret, m_tokenLength, m_counter, m_offset, m_checksum);
        m_actions->queueAndProceed(hotpJob, [hotpJob, q, this](void) -> void {
            new HandleTokenUpdate(this, hotpJob, q);
        });
        break;
    case Account::Algorithm::Totp:
        totpJob = new ComputeTotp(m_storage->secret(), m_secret, m_tokenLength, m_epoch, m_timeStep, m_hash);
        m_actions->queueAndProceed(totpJob, [totpJob, q, this](void) -> void {
            new HandleTokenUpdate(this, totpJob, q);
        });
        break;
    default:
        Q_ASSERT_X(false, Q_FUNC_INFO, "unknown algorithm value");
        break;
    }
}

void AccountPrivate::remove(void)
{
    if (!m_is_still_alive) {
        qCDebug(logger) << "Will not remove account:" << m_id << "Object marked for death";
        return;
    }

    if (!m_storage->isStillOpen()) {
        qCDebug(logger) << "Will not remove account:" << m_id << "Storage no longer open";
        return;
    }

    QSet<QString> self;
    self.insert(AccountPrivate::toFullName(m_name, m_issuer));
    m_storage->removeAccounts(self);
}

void AccountPrivate::markForRemoval(void)
{
    m_is_still_alive = false;
}

bool AccountPrivate::isStillAlive(void) const
{
    return m_is_still_alive;
}

AccountPrivate::AccountPrivate(const std::function<Account *(AccountPrivate *)> &account,
                               AccountStoragePrivate *storage,
                               Dispatcher *dispatcher,
                               const QUuid id,
                               const QString &name,
                               const QString &issuer,
                               const secrets::EncryptedSecret &secret,
                               uint tokenLength,
                               quint64 counter,
                               const std::optional<uint> offset,
                               bool addChecksum)
    : q_ptr(account(this))
    , m_storage(storage)
    , m_actions(dispatcher)
    , m_is_still_alive(true)
    , m_algorithm(Account::Algorithm::Hotp)
    , m_id(id)
    , m_token(QString())
    , m_nextToken(QString())
    , m_nextTotpValidFrom(QDateTime::fromMSecsSinceEpoch(0))
    , // not a totp token so does not really matter
    m_nextTotpValidUntil(QDateTime::fromMSecsSinceEpoch(0))
    , // not a totp token so does not really matter
    m_nextCounter(1ULL)
    , m_name(name)
    , m_issuer(issuer)
    , m_secret(secret)
    , m_tokenLength(tokenLength)
    , m_counter(counter)
    , m_offset(offset)
    , m_checksum(addChecksum)
    ,
    // not a totp token so these values don't really matter
    m_epoch(QDateTime::fromMSecsSinceEpoch(0))
    , m_timeStep(30)
    , m_hash(Account::Hash::Sha1)
{
}

AccountPrivate::AccountPrivate(const std::function<Account *(AccountPrivate *)> &account,
                               AccountStoragePrivate *storage,
                               Dispatcher *dispatcher,
                               const QUuid id,
                               const QString &name,
                               const QString &issuer,
                               const secrets::EncryptedSecret &secret,
                               uint tokenLength,
                               const QDateTime &epoch,
                               uint timeStep,
                               Account::Hash hash)
    : q_ptr(account(this))
    , m_storage(storage)
    , m_actions(dispatcher)
    , m_is_still_alive(true)
    , m_algorithm(Account::Algorithm::Totp)
    , m_id(id)
    , m_token(QString())
    , m_nextToken(QString())
    , m_nextTotpValidFrom(epoch)
    , m_nextTotpValidUntil(epoch)
    , m_nextCounter(0ULL)
    , // not a hotp token so does not really matter
    m_name(name)
    , m_issuer(issuer)
    , m_secret(secret)
    , m_tokenLength(tokenLength)
    ,
    // not a hotp token so these values don't really matter
    m_counter(0ULL)
    , m_offset(std::nullopt)
    , m_checksum(false)
    , m_epoch(epoch)
    , m_timeStep(timeStep)
    , m_hash(hash)
{
}

QList<QString> AccountStoragePrivate::activeAccounts(void) const
{
    QList<QString> active;
    if (!m_is_still_open) {
        qCDebug(logger) << "Not returning accounts: account storage no longer open";
        return active;
    }

    const QList<QString> all = m_names.keys();
    for (const QString &account : all) {
        const QUuid id = m_names[account];
        if (m_accountsPrivate[id]->isStillAlive()) {
            active.append(account);
        } else {
            qCDebug(logger) << "Not returning account:" << id << "Object marked for death";
        }
    }

    return active;
}

bool AccountStoragePrivate::isStillOpen(void) const
{
    return m_is_still_open;
}

bool AccountStoragePrivate::isAccountStillAvailable(const QString &account) const
{
    if (!m_is_still_open) {
        qCDebug(logger) << "Pretending no name is available: account storage no longer open";
        return false;
    }

    return !m_names.contains(account);
}

QString AccountPrivate::toFullName(const QString &name, const QString &issuer)
{
    return issuer.isEmpty() ? name : issuer + QLatin1Char(':') + name;
}

bool AccountStoragePrivate::contains(const QString &account) const
{
    /*
     * Pretend an account which is marked for removal is already fully purged
     * This lets the behaviour of get() and contains() be mutually consistent
     * without having to hand out pointers which may be about to go stale in get()
     */
    if (!m_is_still_open) {
        qCDebug(logger) << "Pretending no account exists: account storage no longer open";
        return false;
    }

    if (!m_names.contains(account)) {
        return false;
    }

    const QUuid id = m_names[account];
    if (!m_accountsPrivate[id]->isStillAlive()) {
        qCDebug(logger) << "Pretending account does not exist:" << id << "Object marked for death";
        return false;
    }

    return true;
}

Account *AccountStoragePrivate::get(const QString &account) const
{
    return contains(account) ? m_accounts[m_names[account]] : nullptr;
}

SettingsProvider AccountStoragePrivate::settings(void) const
{
    return m_settings;
}

AccountSecret *AccountStoragePrivate::secret(void) const
{
    return m_secret;
}

void AccountStoragePrivate::removeAccounts(const QSet<QString> &accountNames)
{
    if (!m_is_still_open) {
        qCDebug(logger) << "Not removing accounts: account storage no longer open";
        return;
    }

    QSet<QUuid> ids;
    for (const QString &accountName : accountNames) {
        if (m_names.contains(accountName)) {
            const QUuid id = m_names[accountName];
            AccountPrivate *p = m_accountsPrivate[id];
            /*
             * Avoid doing anything with accounts which are already about to be removed from the maps
             */
            if (p->isStillAlive()) {
                p->markForRemoval();
                ids.insert(id);
            } else {
                qCDebug(logger) << "Not removing account:" << id << "Object marked for death";
            }
        }
    }

    DeleteAccounts *job = new DeleteAccounts(m_settings, ids);
    m_actions->queueAndProceed(job, [&ids, job, this](void) -> void {
        for (const QUuid &id : std::as_const(ids)) {
            Account *account = m_accounts[id];
            QObject::connect(job, &DeleteAccounts::finished, account, &Account::removed);
        }
    });
}

void AccountStoragePrivate::acceptAccountRemoval(const QString &accountName)
{
    if (!m_names.contains(accountName)) {
        qCDebug(logger) << "Not accepting account removal: account name unknown";
        return;
    }

    const QUuid id = m_names[accountName];
    qCDebug(logger) << "Handling account cleanup for account:" << id;

    Account *account = m_accounts[id];
    m_accounts.remove(id);
    m_accountsPrivate.remove(id);
    m_names.remove(accountName);
    m_ids.remove(id);
    QTimer::singleShot(0, account, &accounts::Account::deleteLater);
}

void AccountStoragePrivate::dispose(const std::function<void(Null *)> &handler)
{
    if (!m_is_still_open) {
        qCDebug(logger) << "Not disposing of storage: account storage no longer open";
        return;
    }

    qCDebug(logger) << "Disposing of storage";

    m_is_still_open = false;
    Null *job = new Null();
    m_secret->cancelRequests();
    m_actions->queueAndProceed(job, [job, &handler](void) -> void {
        handler(job);
    });
}

void AccountStoragePrivate::acceptDisposal(void)
{
    Q_Q(AccountStorage);
    if (m_is_still_open) {
        qCDebug(logger) << "Ignoring disposal of storage: account storage is still open";
        return;
    }

    qCDebug(logger) << "Handling storage disposal";

    const auto &names = m_names.keys();
    for (const QString &accountName : names) {
        const QUuid id = m_names[accountName];
        qCDebug(logger) << "Handling account cleanup for account:" << id;

        Account *account = m_accounts[id];
        m_accounts.remove(id);
        m_accountsPrivate.remove(id);
        m_names.remove(accountName);
        m_ids.remove(id);
        QTimer::singleShot(0, account, &accounts::Account::deleteLater);
    }
    QTimer::singleShot(0, m_secret, &accounts::AccountSecret::deleteLater);
    Q_EMIT q->disposed();
}

QUuid AccountStoragePrivate::generateId(const QString &name) const
{
    QUuid attempt = QUuid::createUuidV5(QUuid(), name);
    while (attempt.isNull() || m_ids.contains(attempt)) {
        attempt = QUuid::createUuid();
    }
    return attempt;
}

std::optional<secrets::EncryptedSecret> AccountStoragePrivate::encrypt(const QString &secret) const
{
    if (!m_is_still_open) {
        qCDebug(logger) << "Will not encrypt account secret: storage no longer open";
        return std::nullopt;
    }

    if (!m_secret || !m_secret->key()) {
        qCDebug(logger) << "Will not encrypt account secret: encryption key not available";
        return std::nullopt;
    }

    QScopedPointer<secrets::SecureMemory> decoded(secrets::decodeBase32(secret));
    if (!decoded) {
        qCDebug(logger) << "Will not encrypt account secret: failed to decode base32";
        return std::nullopt;
    }

    return m_secret->encrypt(decoded.data());
}

bool AccountStoragePrivate::validateGenericNewToken(const QString &name, const QString &issuer, const QString &secret, uint tokenLength) const
{
    return checkTokenLength(tokenLength) && checkName(name) && checkIssuer(issuer) && isAccountStillAvailable(AccountPrivate::toFullName(name, issuer))
        && checkSecret(secret);
}

bool AccountStoragePrivate::addHotp(const std::function<void(SaveHotp *)> &handler,
                                    const QString &name,
                                    const QString &issuer,
                                    const QString &secret,
                                    uint tokenLength,
                                    quint64 counter,
                                    const std::optional<uint> offset,
                                    bool checksum)
{
    if (!m_is_still_open) {
        qCDebug(logger) << "Will not add new HOTP account: storage no longer open";
        return false;
    }

    if (!validateGenericNewToken(name, issuer, secret, tokenLength) || !checkOffset(offset, QCryptographicHash::Sha1)) {
        qCDebug(logger) << "Will not add new HOTP account: invalid account details";
        return false;
    }

    std::optional<secrets::EncryptedSecret> encryptedSecret = encrypt(secret);
    if (!encryptedSecret) {
        qCDebug(logger) << "Will not add new HOTP account: failed to encrypt secret";
        return false;
    }

    QUuid id = generateId(AccountPrivate::toFullName(name, issuer));
    qCDebug(logger) << "Requesting to store details for new HOTP account:" << id;

    m_ids.insert(id);
    SaveHotp *job = new SaveHotp(m_settings, id, name, issuer, *encryptedSecret, tokenLength, counter, offset, checksum);
    m_actions->queueAndProceed(job, [job, &handler](void) -> void {
        handler(job);
    });
    return true;
}

bool AccountStoragePrivate::addTotp(const std::function<void(SaveTotp *)> &handler,
                                    const QString &name,
                                    const QString &issuer,
                                    const QString &secret,
                                    uint tokenLength,
                                    uint timeStep,
                                    const QDateTime &epoch,
                                    Account::Hash hash)
{
    if (!m_is_still_open) {
        qCDebug(logger) << "Will not add new TOTP account: storage no longer open";
        return false;
    }

    if (!validateGenericNewToken(name, issuer, secret, tokenLength) || !checkTimeStep(timeStep) || !checkEpoch(epoch)) {
        qCDebug(logger) << "Will not add new TOTP account: invalid account details";
        return false;
    }

    std::optional<secrets::EncryptedSecret> encryptedSecret = encrypt(secret);
    if (!encryptedSecret) {
        qCDebug(logger) << "Will not add new TOTP account: failed to encrypt secret";
        return false;
    }

    QUuid id = generateId(AccountPrivate::toFullName(name, issuer));
    qCDebug(logger) << "Requesting to store details for new TOTP account:" << id;

    m_ids.insert(id);
    SaveTotp *job = new SaveTotp(m_settings, id, name, issuer, *encryptedSecret, tokenLength, timeStep, epoch, hash);
    m_actions->queueAndProceed(job, [job, &handler](void) -> void {
        handler(job);
    });
    return true;
}

void AccountStoragePrivate::unlock(const std::function<void(RequestAccountPassword *)> &handler)
{
    if (!m_is_still_open) {
        qCDebug(logger) << "Will not attempt to unlock accounts: storage no longer open";
        return;
    }

    qCDebug(logger) << "Requesting to unlock account storage";
    RequestAccountPassword *job = new RequestAccountPassword(m_settings, m_secret);
    m_actions->queueAndProceed(job, [job, &handler](void) -> void {
        handler(job);
    });
}

void AccountStoragePrivate::load(const std::function<void(LoadAccounts *)> &handler)
{
    if (!m_is_still_open) {
        qCDebug(logger) << "Will not load accounts: storage no longer open";
        return;
    }

    LoadAccounts *job = new LoadAccounts(m_settings, m_secret);
    m_actions->queueAndProceed(job, [job, &handler](void) -> void {
        handler(job);
    });
}

Account *AccountStoragePrivate::acceptHotpAccount(const QUuid id,
                                                  const QString &name,
                                                  const QString &issuer,
                                                  const secrets::EncryptedSecret &secret,
                                                  uint tokenLength,
                                                  quint64 counter,
                                                  const std::optional<uint> offset,
                                                  bool checksum)
{
    Q_Q(AccountStorage);
    qCDebug(logger) << "Registering HOTP account:" << id;
    const std::function<Account *(AccountPrivate *)> registration([this, q, &id](AccountPrivate *p) -> Account * {
        Account *account = new Account(p, q);
        m_accounts.insert(id, account);
        return account;
    });
    m_ids.insert(id);
    m_names.insert(AccountPrivate::toFullName(name, issuer), id);
    const auto p = new AccountPrivate(registration, this, m_actions, id, name, issuer, secret, tokenLength, counter, offset, checksum);
    m_accountsPrivate.insert(id, p);

    Q_ASSERT_X(m_accounts.contains(id), Q_FUNC_INFO, "account should have been registered");
    return m_accounts[id];
}

Account *AccountStoragePrivate::acceptTotpAccount(const QUuid id,
                                                  const QString &name,
                                                  const QString &issuer,
                                                  const secrets::EncryptedSecret &secret,
                                                  uint tokenLength,
                                                  uint timeStep,
                                                  const QDateTime &epoch,
                                                  Account::Hash hash)
{
    Q_Q(AccountStorage);
    qCDebug(logger) << "Registering TOTP account:" << id;
    const std::function<Account *(AccountPrivate *)> registration([this, q, &id](AccountPrivate *p) -> Account * {
        Account *account = new Account(p, q);
        m_accounts.insert(id, account);
        return account;
    });
    m_ids.insert(id);
    m_names.insert(AccountPrivate::toFullName(name, issuer), id);
    const auto p = new AccountPrivate(registration, this, m_actions, id, name, issuer, secret, tokenLength, epoch, timeStep, hash);
    m_accountsPrivate.insert(id, p);

    Q_ASSERT_X(m_accounts.contains(id), Q_FUNC_INFO, "account should have been registered");
    return m_accounts[id];
}

bool AccountStoragePrivate::isLoaded(void) const
{
    return m_is_loaded;
}

void AccountStoragePrivate::notifyLoaded(void)
{
    Q_Q(AccountStorage);
    m_is_loaded = true;
    Q_EMIT q->loaded();
}

void AccountStoragePrivate::notifyError(void)
{
    Q_Q(AccountStorage);
    m_has_error = true;
    Q_EMIT q->error();
}

void AccountStoragePrivate::clearError(void)
{
    m_has_error = false;
}

bool AccountStoragePrivate::hasError(void) const
{
    return m_has_error;
}

AccountStoragePrivate::AccountStoragePrivate(const SettingsProvider &settings, AccountSecret *secret, AccountStorage *storage, Dispatcher *dispatcher)
    : q_ptr(storage)
    , m_is_loaded(false)
    , m_has_error(false)
    , m_is_still_open(true)
    , m_actions(dispatcher)
    , m_settings(settings)
    , m_secret(secret)
{
}

HandleCounterUpdate::HandleCounterUpdate(AccountPrivate *account, AccountStoragePrivate *storage, quint64 counter, SaveHotp *job, QObject *parent)
    : QObject(parent)
    , m_accept_on_finish(true)
    , m_counter(counter)
    , m_account(account)
    , m_storage(storage)
{
    QObject::connect(job, &SaveHotp::invalid, this, &HandleCounterUpdate::rejected);
    QObject::connect(job, &SaveHotp::finished, this, &HandleCounterUpdate::finished);
}

void HandleCounterUpdate::rejected(void)
{
    m_accept_on_finish = false;
    m_storage->notifyError();
}

void HandleCounterUpdate::finished(void)
{
    if (m_accept_on_finish) {
        m_account->acceptCounter(m_counter);
    } else {
        qCWarning(logger) << "Rejecting counter update for account:" << m_account->id() << "Failed to save the updated account details to storage";
    }
    deleteLater();
}

HandleTokenUpdate::HandleTokenUpdate(AccountPrivate *account, ComputeHotp *job, QObject *parent)
    : QObject(parent)
    , m_account(account)
{
    QObject::connect(job, &ComputeHotp::otp, this, &HandleTokenUpdate::hotp);
    QObject::connect(job, &ComputeHotp::finished, this, &HandleTokenUpdate::deleteLater);
}

HandleTokenUpdate::HandleTokenUpdate(AccountPrivate *account, ComputeTotp *job, QObject *parent)
    : QObject(parent)
    , m_account(account)
{
    QObject::connect(job, &ComputeTotp::otp, this, &HandleTokenUpdate::totp);
    QObject::connect(job, &ComputeTotp::finished, this, &HandleTokenUpdate::deleteLater);
}

void HandleTokenUpdate::totp(const QString &otp, const QString &nextOtp, const QDateTime &validFrom, const QDateTime &validUntil)
{
    m_account->acceptTotpTokens(otp, nextOtp, validFrom, validUntil);
}

void HandleTokenUpdate::hotp(const QString &otp, const QString &nextOtp, quint64 validUntil)
{
    m_account->acceptHotpTokens(otp, nextOtp, validUntil);
}
}

#include "moc_account_p.cpp"
