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

#include "../logging_p.h"

KEYSMITH_LOGGER(logger, ".model.accounts")

namespace model
{
    qint64 millisecondsLeftForToken(const QDateTime &epoch, uint timeStep, const std::function<qint64(void)> &clock)
    {
        QDateTime now = QDateTime::fromMSecsSinceEpoch(clock());
        if (!epoch.isValid() || !now.isValid() || timeStep == 0) {
            qCDebug(logger) << "Unable to compute milliseconds left: invalid arguments";
            return -1;
        }

        /*
         * Avoid integer overflow by casting to the wider type first before multiplying.
         * Not likely to happen 'in the wild', but good practice nevertheless
         */
        qint64 step = ((qint64) timeStep) * 1000LL;

        qint64 diff = epoch.msecsTo(now);

        /*
         * Compensate for the fact that % operator is not the same as mathematical mod in case diff is negative.
         * diff is negative when the given epoch is in the 'future' compared to the current clock value.
         */
        return diff < 0 ? - (diff % step) : step - (diff % step);
    }

    AccountView::AccountView(accounts::Account *model, QObject *parent) :
        QObject(parent), m_model(model)
    {
        QObject::connect(model, &accounts::Account::tokenChanged, this, &AccountView::tokenChanged);
        QObject::connect(this, &AccountView::remove, model, &accounts::Account::remove);
        QObject::connect(this, &AccountView::recompute, model, &accounts::Account::recompute);
        QObject::connect(this, &AccountView::advanceCounter, model, &accounts::Account::advanceCounter);
        QObject::connect(this, &AccountView::setCounter, model, &accounts::Account::setCounter);
    }

    bool AccountView::isHotp(void) const
    {
        return m_model->algorithm() == accounts::Account::Hotp;
    }

    bool AccountView::isTotp(void) const
    {
        return m_model->algorithm() == accounts::Account::Totp;
    }

    QString AccountView::name(void) const
    {
        return m_model->name();
    }

    QString AccountView::token(void) const
    {
        return m_model->token();
    }

    QString AccountView::issuer(void) const
    {
        return m_model->issuer();
    }

    quint64 AccountView::counter(void) const
    {
        return m_model->counter();
    }

    QDateTime AccountView::epoch(void) const
    {
        return m_model->epoch();
    }

    uint AccountView::timeStep(void) const
    {
        return m_model->timeStep();
    }

    uint AccountView::offset(void) const
    {
        return m_model->offset().value_or(0);
    }

    int AccountView::tokenLength(void) const
    {
        return m_model->tokenLength();
    }

    QString AccountView::hash(void) const
    {
        switch (m_model->hash()) {
        case accounts::Account::Sha1:
            return QStringLiteral("SHA1");
        case accounts::Account::Sha256:
            return QStringLiteral("SHA256");
        case accounts::Account::Sha512:
            return QStringLiteral("SHA512");
        }
        return QString();
    }

    qint64 AccountView::millisecondsLeftForToken(void) const
    {
        if (!isTotp()) {
            qCDebug(logger) << "Unable to compute milliseconds left for token, wrong account type:" << m_model->algorithm();
            return -1;
        }

        return model::millisecondsLeftForToken(m_model->epoch(), m_model->timeStep());
    }

    SimpleAccountListModel::SimpleAccountListModel(accounts::AccountStorage *storage, QObject *parent) :
        QAbstractListModel(parent), m_storage(storage), m_has_error(false), m_index(QVector<QString>())
    {
        QObject::connect(storage, &accounts::AccountStorage::added, this, &SimpleAccountListModel::added);
        QObject::connect(storage, &accounts::AccountStorage::removed, this, &SimpleAccountListModel::removed);
        QObject::connect(storage, &accounts::AccountStorage::error, this, &SimpleAccountListModel::handleError);
        QObject::connect(storage, &accounts::AccountStorage::loaded, this, &SimpleAccountListModel::loadedChanged);

        beginResetModel();
        const auto accounts = m_storage->accounts();
        for (const QString &name : accounts) {
            populate(name, createView(name));
        }
        m_has_error = storage->hasError();
        endResetModel();
    }


    AccountView * SimpleAccountListModel::createView(const QString &name)
    {
        accounts::Account * existingAccount = m_storage->get(name);
        if (!existingAccount) {
            qCDebug(logger) << "Account storage did not yield a valid account object for account name";
            return nullptr;
        }

        return new AccountView(existingAccount, this);
    }

    void SimpleAccountListModel::populate(const QString &name, AccountView *account)
    {
        if (!account) {
            qCDebug(logger) << "Not populating account without a valid account view object";
            return;
        }

        m_index.append(name);
        m_accounts[name] = account;
        Q_EMIT account->recompute();
    }

    bool SimpleAccountListModel::error(void) const
    {
        return m_has_error;
    }

    void SimpleAccountListModel::setError(bool markAsError)
    {
        if (!markAsError && m_storage->hasError()) {
            m_storage->clearError();
        }

        if (markAsError != m_has_error) {
            m_has_error = markAsError;
            Q_EMIT errorChanged();
        }
    }

    void SimpleAccountListModel::handleError(void)
    {
        setError(true);
    }

    bool SimpleAccountListModel::loaded(void) const
    {
        return m_storage->isLoaded();
    }

    accounts::Account::Hash SimpleAccountListModel::toHash(TOTPAlgorithms value)
    {
        switch (value) {
        case TOTPAlgorithms::Sha1:
            return accounts::Account::Hash::Sha1;
        case TOTPAlgorithms::Sha256:
            return accounts::Account::Hash::Sha256;
        case TOTPAlgorithms::Sha512:
            return accounts::Account::Hash::Sha512;
        default:
            Q_ASSERT_X(false, Q_FUNC_INFO, "Unknown/unsupported totp algorithm value");
            return accounts::Account::Hash::Sha1;
        }
    }

    void SimpleAccountListModel::addAccount(AccountInput *input)
    {
        if (!input) {
            qCDebug(logger) << "Not adding account, no input provided";
            return;
        }
        input->createNewAccount(m_storage);
    }

    QHash<int, QByteArray> SimpleAccountListModel::roleNames(void) const
    {
        QHash<int, QByteArray> roles;
        roles[NonStandardRoles::AccountRole] = "account";
        return roles;
    }

    QVariant SimpleAccountListModel::data(const QModelIndex &account, int role) const
    {
        if (!account.isValid()) {
            qCDebug(logger) << "Not returning any data, model index is invalid";
            return QVariant();
        }

        int accountIndex = account.row();
        if (accountIndex < 0 || m_index.size() < accountIndex) {
            qCDebug(logger) << "Not returning any data, model index is out of bounds:" << accountIndex
                << "model size is:" << m_index.size();
            return QVariant();
        }

        if (role != NonStandardRoles::AccountRole) {
            qCDebug(logger) << "Not returning any data, unknown role:" << role;
            return QVariant();
        }

        const QString accountName = m_index.at(accountIndex);
        auto model = m_accounts.value(accountName, nullptr);
        if (!model) {
            qCDebug(logger) << "Not returning any data, unable to find associated account for:" << accountIndex;
            return QVariant();
        }

        return QVariant::fromValue(model);
    }

    int SimpleAccountListModel::rowCount(const QModelIndex &parent) const
    {
        return parent.isValid() ? 0 : m_index.size();
    }

    void SimpleAccountListModel::added(const QString &account)
    {
        auto newAccount = createView(account);
        if (!newAccount) {
            qCDebug(logger) << "Unable to handle added account: unable to construct account view object";
            return;
        }

        if (m_accounts.contains(account)) {
            qCDebug(logger) << "Added account already/still part of the model: requesting removal of the old one from the model first";
            removed(account);
        }

        int accountIndex = m_index.size();
        qCDebug(logger) << "Adding (new) account to the model at position:" << accountIndex;

        beginInsertRows(QModelIndex(), accountIndex, accountIndex);
        populate(account, newAccount);
        endInsertRows();
    }

    void SimpleAccountListModel::removed(const QString &account)
    {
        int accountIndex = m_index.indexOf(account);
        if (accountIndex < 0) {
            qCDebug(logger) << "Unable to handle account removal: account not part of the model";
            return;
        }

        AccountView *v = nullptr;

        qCDebug(logger) << "Removing (old) account from the model at position:" << accountIndex;
        beginRemoveRows(QModelIndex(), accountIndex, accountIndex);
        m_index.remove(accountIndex);
        v = m_accounts.take(account);
        endRemoveRows();

        if (v) {
            v->deleteLater();
        }
    }

    bool SimpleAccountListModel::isAccountStillAvailable(const QString &name, const QString &issuer) const
    {
        return m_storage && m_storage->isAccountStillAvailable(name, issuer);
    }

    AccountNameValidator::AccountNameValidator(QObject *parent) :
        QValidator(parent), m_validateAvailability(true), m_issuer(std::nullopt), m_accounts(nullptr), m_delegate(nullptr)
    {
    }

    bool AccountNameValidator::validateAvailability(void) const
    {
        return m_validateAvailability;
    }

    void AccountNameValidator::setValidateAvailability(bool enabled)
    {
        if (enabled != m_validateAvailability) {
            m_validateAvailability = enabled;
            Q_EMIT validateAvailabilityChanged();
        }
    }

    QString AccountNameValidator::issuer(void) const
    {
        return m_issuer
            ? *m_issuer
            : QStringLiteral(":: WARNING: dummy invalid issuer; this is meant as a write-only property anyway ::");
    }

    void AccountNameValidator::setIssuer(const QString &issuer)
    {
        if (m_issuer && issuer == *m_issuer) {
            qCDebug(logger) << "Ignoring new issuer: same as the current issuer";
            return;
        }

        m_issuer.emplace(issuer);
        Q_EMIT issuerChanged();
    }

    QValidator::State AccountNameValidator::validate(QString &input, int &pos) const
    {
        QValidator::State result = m_delegate.validate(input, pos);
        if (!m_validateAvailability) {
            qCDebug(logger) << "Not validating account availability: explicitly disabled";
            return result;
        }

        if (!m_accounts) {
            qCDebug(logger) << "Unable to validate account name: missing accounts model object";
            return QValidator::Invalid;
        }

        if (!m_issuer) {
            qCDebug(logger) << "Unable to validate account name: missing issuer";
            return QValidator::Invalid;
        }

        return result != QValidator::Acceptable || m_accounts->isAccountStillAvailable(input, *m_issuer)
            ? result
            : QValidator::Intermediate;
    }

    void AccountNameValidator::fixup(QString &input) const
    {
        m_delegate.fixup(input);
    }

    SimpleAccountListModel * AccountNameValidator::accounts(void) const
    {
        return m_accounts;
    }

    void AccountNameValidator::setAccounts(SimpleAccountListModel *accounts)
    {
        if (!accounts) {
            qCDebug(logger) << "Ignoring new accounts model: not a valid object";
            return;
        }

        m_accounts = accounts;
        Q_EMIT accountsChanged();
    }

    SortedAccountsListModel::SortedAccountsListModel(QObject *parent) : QSortFilterProxyModel(parent)
    {
    }

    void SortedAccountsListModel::setSourceModel(QAbstractItemModel *sourceModel)
    {
        SimpleAccountListModel *model = qobject_cast<SimpleAccountListModel*>(sourceModel);
        if (!model) {
            qCDebug(logger) << "Not setting source model: it is not an accounts list model!";
            return;
        }

        QSortFilterProxyModel::setSourceModel(sourceModel);
        qCDebug(logger) << "Updating properties & resorting the model";
        setSortRole(SimpleAccountListModel::NonStandardRoles::AccountRole);
        setDynamicSortFilter(true);
        setSortLocaleAware(true);
        sort(0);
    }

    bool SortedAccountsListModel::lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const
    {
        QAbstractItemModel *source = sourceModel();
        Q_ASSERT_X(source, Q_FUNC_INFO, "should have a source model at this point");

        const SimpleAccountListModel *model = qobject_cast<const SimpleAccountListModel*>(source);
        // useless junk: implement sorting as no-op: claim equality between left & right
        if (!model) {
            qCDebug(logger) << "Short-circuiting lessThan operator: source model is not an accounts list model!";
            return false;
        }

        const QVariant leftValue = model->data(source_left, SimpleAccountListModel::NonStandardRoles::AccountRole);
        const QVariant rightValue = model->data(source_right, SimpleAccountListModel::NonStandardRoles::AccountRole);

        const AccountView * leftAccount = leftValue.isNull() ? nullptr : leftValue.value<AccountView*>();
        const AccountView * rightAccount = rightValue.isNull() ? nullptr : rightValue.value<AccountView*>();

        // useless junk: implement sorting as no-op: claim left == right
        if (!leftAccount && !rightAccount) {
            qCDebug(logger) << "Short-circuiting lessThan operator: both source model indices do not point to accounts";
            return false;
        }

        // Sort actual accounts before useless junk: claim left >= right
        if (!leftAccount) {
            qCDebug(logger) << "Short-circuiting lessThan operator: left source model index does not point to an account";
            return false;
        }

        // Sort actual accounts before useless junk: claim left < right
        if (!rightAccount) {
            qCDebug(logger) << "Short-circuiting lessThan operator: right source model index does not point to an account";
            return true;
        }

        const QString leftIssuer = leftAccount->issuer();
        const QString rightIssuer = rightAccount->issuer();

        // both issuers are null: sort by account name
        if (leftIssuer.isNull() && rightIssuer.isNull()) {
            return leftAccount->name().localeAwareCompare(rightAccount->name()) < 0;
        }

        // Sort accounts without issuer to the top: claim left < right
        if (leftIssuer.isNull()) {
            return true;
        }

        // Sort accounts without issuer to the top: claim left >= right
        if (rightIssuer.isNull()) {
            return false;
        }

        // actual sorting by account issuer, then name
        int issuer = leftIssuer.localeAwareCompare(rightIssuer);
        return issuer == 0 ? leftAccount->name().localeAwareCompare(rightAccount->name()) < 0 : issuer < 0;
    }
}
