// SPDX-FileCopyrightText: 2020 Melvin Keskin <melvo@olomono.de>
// SPDX-FileCopyrightText: 2020 Jonah Brüchert <jbb@kaidan.im>
// SPDX-FileCopyrightText: 2020 Linus Jahn <lnj@kaidan.im>
// SPDX-FileCopyrightText: 2020 Mathis Brüchert <mbblp@protonmail.ch>
// SPDX-FileCopyrightText: 2024 Filipe Azevedo <pasnox@gmail.com>
//
// SPDX-License-Identifier: GPL-3.0-or-later

#include "AccountManager.h"

// Qt
#include <QMutexLocker>
#include <QSettings>
#include <QStringBuilder>
// QXmpp
#include <QXmppSasl2UserAgent.h>
#include <QXmppUtils.h>
// Kaidan
#include "AccountDb.h"
#include "EncryptionController.h"
#include "Globals.h"
#include "Kaidan.h"
#include "MessageDb.h"
#include "RegistrationManager.h"
#include "RosterDb.h"
#include "ServerFeaturesCache.h"
#include "Settings.h"
#include "SystemUtils.h"
#include "VCardCache.h"

AccountManager *AccountManager::s_instance = nullptr;

AccountManager *AccountManager::instance()
{
    return s_instance;
}

AccountManager::AccountManager(Settings *settings, VCardCache *cache, QObject *parent)
    : QObject(parent)
    , m_settings(settings)
    , m_port(PORT_AUTODETECT)
{
    Q_ASSERT(!s_instance);
    s_instance = this;

    connect(cache, &VCardCache::vCardChanged, this, [this](const QString &jid) {
        if (m_jid == jid) {
            Q_EMIT displayNameChanged();
        }
    });
}

const Account &AccountManager::account() const
{
    return m_account;
}

void AccountManager::setContactNotificationRule(const QString &jid, Account::ContactNotificationRule rule)
{
    await(AccountDb::instance()->updateAccount(jid,
                                               [rule](Account &account) {
                                                   account.contactNotificationRule = rule;
                                               }),
          this,
          [this, rule]() {
              m_account.contactNotificationRule = rule;
              Q_EMIT accountChanged();
          });
}

void AccountManager::setGroupChatNotificationRule(const QString &jid, Account::GroupChatNotificationRule rule)
{
    await(AccountDb::instance()->updateAccount(jid,
                                               [rule](Account &account) {
                                                   account.groupChatNotificationRule = rule;
                                               }),
          this,
          [this, rule]() {
              m_account.groupChatNotificationRule = rule;
              Q_EMIT accountChanged();
          });
}

void AccountManager::setGeoLocationMapPreviewEnabled(const QString &jid, bool geoLocationMapPreviewEnabled)
{
    await(AccountDb::instance()->updateAccount(jid,
                                               [geoLocationMapPreviewEnabled](Account &account) {
                                                   account.geoLocationMapPreviewEnabled = geoLocationMapPreviewEnabled;
                                               }),
          this,
          [this, geoLocationMapPreviewEnabled]() {
              m_account.geoLocationMapPreviewEnabled = geoLocationMapPreviewEnabled;
              Q_EMIT accountChanged();
          });
}

void AccountManager::setGeoLocationMapService(const QString &jid, Account::GeoLocationMapService geoLocationMapService)
{
    await(AccountDb::instance()->updateAccount(jid,
                                               [geoLocationMapService](Account &account) {
                                                   account.geoLocationMapService = geoLocationMapService;
                                               }),
          this,
          [this, geoLocationMapService]() {
              m_account.geoLocationMapService = geoLocationMapService;
              Q_EMIT accountChanged();
          });
}

QString AccountManager::jid()
{
    QMutexLocker locker(&m_mutex);
    return m_jid;
}

void AccountManager::setJid(const QString &jid)
{
    QMutexLocker locker(&m_mutex);

    if (m_jid != jid) {
        m_jid = jid;
        m_hasNewCredentials = true;

        // If a server's JID is set during registration, it should not be added as an account.
        // Thus, the JID must contain a local part (user).
        //
        // If the JID is cleared, there is no corresponding account.
        // Thus, the JID must not be empty.
        if (!QXmppUtils::jidToUser(jid).isEmpty()) {
            AccountDb::instance()->addAccount(jid);

            await(AccountDb::instance()->account(jid), this, [this](Account &&account) {
                m_account = std::move(account);
                Q_EMIT accountChanged();
            });

            await(AccountDb::instance()->fetchHttpUploadLimit(jid), this, [](qint64 &&limit) {
                Kaidan::instance()->serverFeaturesCache()->setHttpUploadLimit(limit);
            });
        }

        locker.unlock();
        Q_EMIT jidChanged();
    }
}

void AccountManager::setJidResourcePrefix(const QString &jidResourcePrefix)
{
    m_jidResourcePrefix = jidResourcePrefix;
    m_jidResource = generateJidResourceWithRandomSuffix();
}

QString AccountManager::jidResource() const
{
    return m_jidResourcePrefix.isEmpty() ? generateJidResourceWithRandomSuffix() : m_jidResource;
}

QString AccountManager::password()
{
    QMutexLocker locker(&m_mutex);
    return m_password;
}

void AccountManager::setPassword(const QString &password)
{
    QMutexLocker locker(&m_mutex);

    if (m_password != password) {
        m_password = password;
        m_hasNewCredentials = true;

        locker.unlock();
        Q_EMIT passwordChanged();
    }
}

QString AccountManager::host()
{
    QMutexLocker locker(&m_mutex);
    return m_host;
}

void AccountManager::setHost(const QString &host)
{
    QMutexLocker locker(&m_mutex);

    if (m_host != host) {
        m_host = host;
        m_hasNewConnectionSettings = true;

        locker.unlock();
        Q_EMIT hostChanged();
    }
}

quint16 AccountManager::port()
{
    QMutexLocker locker(&m_mutex);
    return m_port;
}

void AccountManager::setPort(const quint16 port)
{
    QMutexLocker locker(&m_mutex);

    if (m_port != port) {
        m_port = port;
        m_hasNewConnectionSettings = true;

        locker.unlock();
        Q_EMIT portChanged();
    }
}

quint16 AccountManager::portAutodetect() const
{
    return PORT_AUTODETECT;
}

QString AccountManager::displayName()
{
    // no mutex locker required, own attributes are not accessed
    if (const auto vCard = Kaidan::instance()->vCardCache()->vCard(m_jid)) {
        if (const auto nickname = vCard->nickName(); !nickname.isEmpty()) {
            return nickname;
        }
    }

    return QXmppUtils::jidToUser(m_jid);
}

void AccountManager::resetCustomConnectionSettings()
{
    setHost({});
    setPort(PORT_AUTODETECT);
}

bool AccountManager::hasNewCredentials() const
{
    return m_hasNewCredentials;
}

void AccountManager::setHasNewCredentials(bool hasNewCredentials)
{
    m_hasNewCredentials = hasNewCredentials;
}

bool AccountManager::hasEnoughCredentialsForLogin()
{
    return !(jid().isEmpty() || password().isEmpty());
}

bool AccountManager::hasNewConnectionSettings() const
{
    return m_hasNewConnectionSettings;
}

void AccountManager::setHasNewConnectionSettings(bool hasNewConnectionSettings)
{
    m_hasNewConnectionSettings = hasNewConnectionSettings;
}

QXmppSasl2UserAgent AccountManager::userAgent()
{
    auto deviceId = m_settings->userAgentDeviceId();
    if (deviceId.isNull()) {
        deviceId = QUuid::createUuid();
        m_settings->setUserAgentDeviceId(deviceId);
    }
    return QXmppSasl2UserAgent(deviceId, QStringLiteral(APPLICATION_DISPLAY_NAME), SystemUtils::productName());
}

bool AccountManager::loadConnectionData()
{
    if (!hasEnoughCredentialsForLogin()) {
        // Load the credentials from the settings file.
        setJid(m_settings->authJid());
        setPassword(m_settings->authPassword());

        // Use a default prefix for the JID's resource part if no prefix is already set.
        setJidResourcePrefix(m_settings->authJidResourcePrefix());

        // Load the custom connection setings.
        setHost(m_settings->authHost());
        setPort(m_settings->authPort());

        // This method is only used to load old credentials and connection settings.
        // Thus, "m_hasNewCredentials" and "m_hasNewConnectionSettings" which were set to "true" by
        // setting the credentials and connection settings in this method are reset here.
        m_hasNewCredentials = false;
        m_hasNewConnectionSettings = false;

        // If no credentials could be loaded from the settings file, notify the GUI to ask
        // the user for credentials.
        if (!hasEnoughCredentialsForLogin())
            return false;
    }

    return true;
}

void AccountManager::storeJid()
{
    m_settings->setAuthJid(jid());
}

void AccountManager::storePassword()
{
    m_settings->setAuthPassword(password());
}

void AccountManager::storeCustomConnectionSettings()
{
    if (m_host.isEmpty())
        m_settings->resetAuthHost();
    else
        m_settings->setAuthHost(m_host);

    if (m_settings->isDefaultAuthPort())
        m_settings->resetAuthPort();
    else
        m_settings->setAuthPort(m_port);
}

void AccountManager::storeConnectionData()
{
    storeJid();
    storePassword();
    storeCustomConnectionSettings();
}

void AccountManager::deleteAccountFromClient()
{
    m_deletionStates = DeletionState::ToBeDeletedFromClient;

    // If the client is not yet disconnected, disconnect first and delete the account afterwards.
    // Otherwise, delete the account directly from the client.
    runOnThread(
        Kaidan::instance()->client(),
        []() {
            return Kaidan::instance()->client()->xmppClient()->isAuthenticated();
        },
        this,
        [this](bool authenticated) {
            if (authenticated) {
                await(EncryptionController::instance()->reset(), this, []() {
                    runOnThread(Kaidan::instance()->client(), []() {
                        Kaidan::instance()->client()->logOut();
                    });
                });
            } else {
                runOnThread(Kaidan::instance()->client(), []() {
                    Kaidan::instance()->client()->logIn();
                });
            }
        });
}

void AccountManager::deleteAccountFromClientAndServer()
{
    m_deletionStates = DeletionState::ToBeDeletedFromClient | DeletionState::ToBeDeletedFromServer;

    // If the client is already connected, delete the account directly from the server.
    // Otherwise, connect first and delete the account afterwards.
    runOnThread(
        Kaidan::instance()->client(),
        []() {
            return Kaidan::instance()->client()->xmppClient()->isAuthenticated();
        },
        this,
        [this](bool authenticated) {
            if (authenticated) {
                runOnThread(Kaidan::instance()->client(), []() {
                    Kaidan::instance()->client()->registrationManager()->deleteAccount();
                });
            } else {
                m_deletionStates |= DeletionState::ClientDisconnectedBeforeDeletionFromServer;

                runOnThread(Kaidan::instance()->client(), []() {
                    Kaidan::instance()->client()->logIn();
                });
            }
        });
}

void AccountManager::handleAccountDeletedFromServer()
{
    m_deletionStates = DeletionState::DeletedFromServer;
}

void AccountManager::handleAccountDeletionFromServerFailed(const QXmppStanza::Error &error)
{
    Q_EMIT accountDeletionFromClientAndServerFailed(error.text());

    if (m_deletionStates.testFlag(DeletionState::ClientDisconnectedBeforeDeletionFromServer)) {
        m_deletionStates = DeletionState::NotToBeDeleted;

        runOnThread(Kaidan::instance()->client(), []() {
            Kaidan::instance()->client()->logOut();
        });
    } else {
        m_deletionStates = DeletionState::NotToBeDeleted;
    }
}

bool AccountManager::handleConnected()
{
    // If the account could not be deleted because the client was disconnected, delete it now.
    if (m_deletionStates.testFlag(DeletionState::ToBeDeletedFromClient) && m_deletionStates.testFlag(DeletionState::ToBeDeletedFromServer)) {
        runOnThread(Kaidan::instance()->client(), []() {
            Kaidan::instance()->client()->registrationManager()->deleteAccount();
        });

        return true;
    } else if (m_deletionStates.testFlag(DeletionState::ToBeDeletedFromClient)) {
        deleteAccountFromClient();
        return true;
    }

    return false;
}

void AccountManager::handleDisconnected()
{
    // Delete the account from the client if the account was deleted from the server or the client
    // was connected and had to disconnect first.
    if (m_deletionStates.testFlag(DeletionState::DeletedFromServer)) {
        await(EncryptionController::instance()->resetLocally(), this, [this]() {
            removeAccount(m_jid);
        });
    } else if (m_deletionStates.testFlag(DeletionState::ToBeDeletedFromClient)) {
        removeAccount(m_jid);
    }

    m_deletionStates = DeletionState::NotToBeDeleted;
}

QString AccountManager::generateJidResourceWithRandomSuffix(unsigned int numberOfRandomSuffixCharacters) const
{
    return m_jidResourcePrefix % QLatin1Char('.') % QXmppUtils::generateStanzaHash(numberOfRandomSuffixCharacters);
}

void AccountManager::removeAccount(const QString &accountJid)
{
    MessageDb::instance()->removeAllMessagesFromAccount(accountJid);
    RosterDb::instance()->removeItems(accountJid);
    AccountDb::instance()->removeAccount(accountJid);

    m_settings->remove({
        QStringLiteral(KAIDAN_SETTINGS_AUTH_ONLINE),
        QStringLiteral(KAIDAN_SETTINGS_AUTH_JID),
        QStringLiteral(KAIDAN_SETTINGS_AUTH_JID_RESOURCE_PREFIX),
        QStringLiteral(KAIDAN_SETTINGS_AUTH_PASSWD),
        QStringLiteral(KAIDAN_SETTINGS_AUTH_CREDENTIALS),
        QStringLiteral(KAIDAN_SETTINGS_AUTH_HOST),
        QStringLiteral(KAIDAN_SETTINGS_AUTH_PORT),
        QStringLiteral(KAIDAN_SETTINGS_AUTH_TLS_REQUIREMENT),
        QStringLiteral(KAIDAN_SETTINGS_AUTH_PASSWD_VISIBILITY),
        QStringLiteral(KAIDAN_SETTINGS_AUTH_DEVICE_ID),
        QStringLiteral(KAIDAN_SETTINGS_ENCRYPTION),
        QStringLiteral(KAIDAN_SETTINGS_FAVORITE_EMOJIS),
        QStringLiteral(KAIDAN_SETTINGS_AUTOMATIC_MEDIA_DOWNLOADS_RULE),

    });

    setJid({});
    m_jidResourcePrefix.clear();
    m_jidResource.clear();
    setPassword({});
    resetCustomConnectionSettings();

    Q_EMIT credentialsNeeded();
}

#include "moc_AccountManager.cpp"
