// SPDX-FileCopyrightText: 2009 Manjeet Dahiya <manjeetdahiya@gmail.com>
// SPDX-FileCopyrightText: 2019 Linus Jahn <lnj@kaidan.im>
// SPDX-FileCopyrightText: 2023 Melvin Keskin <melvo@olomono.de>
//
// SPDX-License-Identifier: LGPL-2.1-or-later

#include "QXmppClient.h"

#include "QXmppAsync_p.h"
#include "QXmppClientExtension.h"
#include "QXmppClient_p.h"
#include "QXmppConstants_p.h"
#include "QXmppDiscoveryIq.h"
#include "QXmppDiscoveryManager.h"
#include "QXmppE2eeExtension.h"
#include "QXmppE2eeMetadata.h"
#include "QXmppEntityTimeManager.h"
#include "QXmppLogger.h"
#include "QXmppMessage.h"
#include "QXmppMessageHandler.h"
#include "QXmppPacket_p.h"
#include "QXmppPromise.h"
#include "QXmppRosterManager.h"
#include "QXmppStreamManagement_p.h"
#include "QXmppTask.h"
#include "QXmppUtils.h"
#include "QXmppVCardManager.h"
#include "QXmppVersionManager.h"

#include "Algorithms.h"
#include "Async.h"
#include "StringLiterals.h"
#include "XmppSocket.h"

#include <chrono>

#include <QDomElement>
#include <QSslSocket>
#include <QTimer>

using namespace std::chrono_literals;
using namespace QXmpp::Private;
using MessageEncryptResult = QXmppE2eeExtension::MessageEncryptResult;
using IqEncryptResult = QXmppE2eeExtension::IqEncryptResult;
using IqDecryptResult = QXmppE2eeExtension::IqDecryptResult;

static bool isIqResponse(const QDomElement &el)
{
    auto type = el.attribute(u"type"_s);
    return el.tagName() == u"iq" && (type == u"result" || type == u"error");
}

QXmppClientPrivate::QXmppClientPrivate(QXmppClient *qq)
    : clientPresence(QXmppPresence::Available),
      logger(nullptr),
      stream(nullptr),
      encryptionExtension(nullptr),
      receivedConflict(false),
      reconnectionTries(0),
      reconnectionTimer(nullptr),
      q(qq)
{
}

void QXmppClientPrivate::resendPresence()
{
    auto presence = clientPresence;
    q->send(std::move(presence));
}

void QXmppClientPrivate::addProperCapability(QXmppPresence &presence)
{
    if (auto *discoManager = q->findExtension<QXmppDiscoveryManager>()) {
        presence.setCapabilityHash(u"sha-1"_s);
        presence.setCapabilityNode(discoManager->clientCapabilitiesNode());
        presence.setCapabilityVer(discoManager->buildClientInfo().calculateEntityCapabilitiesHash());
    }
}

std::chrono::milliseconds QXmppClientPrivate::getNextReconnectTime() const
{
    if (reconnectionTries < 5) {
        return 10s;
    } else if (reconnectionTries < 10) {
        return 20s;
    } else if (reconnectionTries < 15) {
        return 40s;
    } else {
        return 60s;
    }
}

QStringList QXmppClientPrivate::discoveryFeatures()
{
    return {
        // XEP-0004: Data Forms
        staticString(ns_data),
        // XEP-0059: Result Set Management
        staticString(ns_rsm),
        // XEP-0066: Out of Band Data
        staticString(ns_oob),
        // XEP-0071: XHTML-IM
        staticString(ns_xhtml_im),
        // XEP-0085: Chat State Notifications
        staticString(ns_chat_states),
        // XEP-0115: Entity Capabilities
        staticString(ns_capabilities),
        // XEP-0249: Direct MUC Invitations
        staticString(ns_conference),
        // XEP-0308: Last Message Correction
        staticString(ns_message_correct),
        // XEP-0333: Chat Markers
        staticString(ns_chat_markers),
        // XEP-0334: Message Processing Hints
        staticString(ns_message_processing_hints),
        // XEP-0359: Unique and Stable Stanza IDs
        staticString(ns_sid),
        // XEP-0367: Message Attaching
        staticString(ns_message_attaching),
        // XEP-0380: Explicit Message Encryption
        staticString(ns_eme),
        // XEP-0382: Spoiler messages
        staticString(ns_spoiler),
        // XEP-0428: Fallback Indication
        staticString(ns_fallback_indication),
        // XEP-0444: Message Reactions
        staticString(ns_reactions),
    };
}

void QXmppClientPrivate::onErrorOccurred(const QString &text, const QXmppOutgoingClient::ConnectionError &err, QXmppClient::Error oldError)
{
    // Skip stream errors that are valid during special procedures such as account
    // creation/deletion.
    if (const auto streamError = std::get_if<QXmpp::StreamError>(&err);
        streamError && ignoredStreamErrors.contains(*streamError)) {
        return;
    }

    if (q->configuration().autoReconnectionEnabled()) {
        if (oldError == QXmppClient::XmppStreamError) {
            // if we receive a resource conflict, inhibit reconnection
            if (stream->xmppStreamError() == QXmppStanza::Error::Conflict) {
                receivedConflict = true;
            }
        } else if (oldError == QXmppClient::SocketError && !receivedConflict) {
            // schedule reconnect
            reconnectionTimer->start(getNextReconnectTime());
        } else if (oldError == QXmppClient::KeepAliveError) {
            // if we got a keepalive error, reconnect in one second
            reconnectionTimer->start(1s);
        }
    }

    // notify managers
    Q_EMIT q->error(oldError);
    Q_EMIT q->errorOccurred(QXmppError { text, into<std::any>(std::move(err)) });
}

namespace QXmpp::Private::StanzaPipeline {

bool process(const QList<QXmppClientExtension *> &extensions, const QDomElement &element, const std::optional<QXmppE2eeMetadata> &e2eeMetadata)
{
    const bool unencrypted = !e2eeMetadata.has_value();
    for (auto *extension : extensions) {
        // e2e encrypted stanzas are not passed to the old handleStanza() overload, because such
        // managers are likely not handling the encrypted contents correctly (e.g. sending
        // unencrypted replies and thereby leaking information).
        if (extension->handleStanza(element, e2eeMetadata) ||
            (unencrypted && extension->handleStanza(element))) {
            return true;
        }
    }
    return false;
}

}  // namespace QXmpp::Private::StanzaPipeline

namespace QXmpp::Private::MessagePipeline {

bool process(QXmppClient *, const QList<QXmppClientExtension *> &extensions, QXmppMessage &&message)
{
    for (auto *extension : extensions) {
        if (auto *messageHandler = dynamic_cast<QXmppMessageHandler *>(extension)) {
            if (messageHandler->handleMessage(message)) {
                return true;
            }
        }
    }
    return false;
}

bool process(QXmppClient *client, const QList<QXmppClientExtension *> &extensions, QXmppE2eeExtension *e2eeExt, const QDomElement &element)
{
    if (element.tagName() != u"message") {
        return false;
    }
    QXmppMessage message;
    if (e2eeExt) {
        message.parse(element, e2eeExt->isEncrypted(element) ? ScePublic : SceAll);
    } else {
        message.parse(element);
    }
    return process(client, extensions, std::move(message));
}

}  // namespace QXmpp::Private::MessagePipeline

/*!
    \typedef QXmppClient::IqResult

    Result of an IQ request, either contains the QDomElement of the IQ reponse (in case of an
    'result' IQ type) or it contains an QXmppError with a QXmppStanza::Error (on type 'error') or
    a QXmpp::SendError.

    \since QXmpp 1.5
*/

/*!
    \typedef QXmppClient::EmptyResult

    Result of a generic request without a return value. Contains Success in case
    everything went well. If the returned IQ contained an error a
    QXmppStanza::Error is reported.

    \since QXmpp 1.5
*/

/*!
    Creates a QXmppClient object.

    \a initialExtensions can be used to set the initial set of extensions.
    \a parent is passed to the QObject's constructor. The default value is 0.

    \since QXmpp 1.6
*/
QXmppClient::QXmppClient(InitialExtensions initialExtensions, QObject *parent)
    : QXmppLoggable(parent),
      d(new QXmppClientPrivate(this))
{
    d->stream = new QXmppOutgoingClient(this);
    d->addProperCapability(d->clientPresence);

    connect(d->stream, &QXmppOutgoingClient::elementReceived,
            this, &QXmppClient::_q_elementReceived);

    connect(d->stream, &QXmppOutgoingClient::messageReceived,
            this, &QXmppClient::messageReceived);

    connect(d->stream, &QXmppOutgoingClient::presenceReceived,
            this, &QXmppClient::presenceReceived);

    connect(d->stream, &QXmppOutgoingClient::iqReceived,
            this, &QXmppClient::iqReceived);

    connect(d->stream, &QXmppOutgoingClient::sslErrors,
            this, &QXmppClient::sslErrors);

    connect(d->stream, &QXmppOutgoingClient::connected,
            this, &QXmppClient::_q_streamConnected);

    connect(d->stream, &QXmppOutgoingClient::disconnected,
            this, &QXmppClient::_q_streamDisconnected);

    connect(d->stream, &QXmppOutgoingClient::errorOccurred, this, [this](const auto &text, const auto &error, auto oldError) {
        d->onErrorOccurred(text, error, oldError);
    });

    connect(&d->stream->xmppSocket(), &XmppSocket::internalSocketStateChanged, this, &QXmppClient::onInternalSocketStateChanged);

    // reconnection
    d->reconnectionTimer = new QTimer(this);
    d->reconnectionTimer->setSingleShot(true);
    connect(d->reconnectionTimer, &QTimer::timeout,
            this, &QXmppClient::_q_reconnect);

    // logging
    setLogger(QXmppLogger::getLogger());

    switch (initialExtensions) {
    case NoExtensions:
        break;
    case BasicExtensions:
        addNewExtension<QXmppRosterManager>(this);
        addNewExtension<QXmppVCardManager>();
        addNewExtension<QXmppVersionManager>();
        addNewExtension<QXmppEntityTimeManager>();
        addNewExtension<QXmppDiscoveryManager>();
        break;
    }
}

/*!
    Creates a QXmppClient object. \a parent is passed to the QObject's constructor.
*/
QXmppClient::QXmppClient(QObject *parent)
    : QXmppClient(BasicExtensions, parent)
{
}

QXmppClient::~QXmppClient() = default;

/*!
    \fn template <typename T, typename... Args> T *QXmppClient::addNewExtension(Args... args)

    Creates a new extension (forwarding \a args to its constructor) and adds it to the client.

    Returns the newly created extension.

    \since QXmpp 1.5
*/

/*! Registers a new \a extension with the client. Returns true on success. */
bool QXmppClient::addExtension(QXmppClientExtension *extension)
{
    return insertExtension(d->extensions.size(), extension);
}

/*! Registers a new \a extension with the client at the given \a index. Returns true on success. */
bool QXmppClient::insertExtension(int index, QXmppClientExtension *extension)
{
    if (d->extensions.contains(extension)) {
        qWarning("Cannot add extension, it has already been added");
        return false;
    }

    extension->setParent(this);
    d->extensions.insert(index, extension);
    extension->setClient(this);
    return true;
}

/*!
    Returns whether the extension was unregistered. Unregisters the given extension from the client. If the extension
    is found, it will be destroyed.

    \a extension.
*/
bool QXmppClient::removeExtension(QXmppClientExtension *extension)
{
    if (d->extensions.contains(extension)) {
        d->extensions.removeAll(extension);
        extension->setClient(nullptr);
        delete extension;
        return true;
    } else {
        qWarning("Cannot remove extension, it was never added");
        return false;
    }
}

/*!
    Returns the currently used encryption extension.

    \since QXmpp 1.5
*/
QXmppE2eeExtension *QXmppClient::encryptionExtension() const
{
    return d->encryptionExtension;
}

/*!
    Sets the extension to be used for end-to-end-encryption.

    \since QXmpp 1.5

    \a extension.
*/
void QXmppClient::setEncryptionExtension(QXmppE2eeExtension *extension)
{
    d->encryptionExtension = extension;
}

/*! Returns a list containing all the client's extensions. */
QList<QXmppClientExtension *> QXmppClient::extensions() const
{
    return d->extensions;
}

/*! Returns a modifiable reference to the current configuration of QXmppClient. */
QXmppConfiguration &QXmppClient::configuration()
{
    return d->stream->configuration();
}

/*!
    Attempts to connect to the XMPP server. Server details and other configurations
    are specified using the config parameter. Use signals connected(), error(QXmppClient::Error)
    and disconnected() to know the status of the connection.

    \a config specifies the configuration object for connecting the XMPP server.
    This contains the host name, user, password etc. See QXmppConfiguration for details.
    \a initialPresence is the initial presence which will be set for this user
    after establishing the session. The default value is QXmppPresence::Available.
*/
void QXmppClient::connectToServer(const QXmppConfiguration &config,
                                  const QXmppPresence &initialPresence)
{
    // reset package cache from last connection
    if (d->stream->configuration().jidBare() != config.jidBare()) {
        d->stream->streamAckManager().resetCache();
    }

    d->stream->configuration() = config;
    d->clientPresence = initialPresence;
    d->addProperCapability(d->clientPresence);

    d->stream->connectToHost();
}

/*!
    Overloaded function to simply connect to an XMPP server with a \a jid and \a password for
    the account.
*/
void QXmppClient::connectToServer(const QString &jid, const QString &password)
{
    QXmppConfiguration config;
    config.setJid(jid);
    config.setPassword(password);
    connectToServer(config);
}

/*!
    Sends a packet and reports the result via QXmppTask.

    If stream management is enabled, the task continues to be active until the
    server acknowledges the packet. On success, QXmpp::SendSuccess with
    acknowledged == true is reported and the task finishes.

    If connection errors occur, the packet is resent if possible. If
    reconnecting is not possible, an error is reported.

    Returns a QXmppTask that makes it possible to track the state of the packet.

    \since QXmpp 1.5

    \a params and \a stanza.
*/
QXmppTask<QXmpp::SendResult> QXmppClient::sendSensitive(QXmppStanza &&stanza, const std::optional<QXmppSendStanzaParams> &params)
{
    if (d->encryptionExtension) {
        if (dynamic_cast<QXmppMessage *>(&stanza)) {
            return d->sendEncryptedMessage(d->encryptionExtension->encryptMessage(dynamic_cast<QXmppMessage &&>(stanza), params));
        } else if (dynamic_cast<QXmppIq *>(&stanza)) {
            return d->sendEncryptedIq(d->encryptionExtension->encryptIq(dynamic_cast<QXmppIq &&>(stanza), params));
        }
    }
    return d->stream->streamAckManager().send(stanza);
}

QXmppTask<QXmpp::SendResult> QXmppClientPrivate::sendEncryptedMessage(QXmppTask<QXmppE2eeExtension::MessageEncryptResult> task)
{
    auto result = co_await task.withContext(q);
    if (std::holds_alternative<QXmppError>(result)) {
        co_return std::get<QXmppError>(std::move(result));
    }
    QByteArray xml;
    QXmlStreamWriter writer(&xml);
    std::get<std::unique_ptr<QXmppMessage>>(result)->toXml(&writer, QXmpp::ScePublic);

    co_return co_await stream->streamAckManager().send(QXmppPacket(xml, true));
}

QXmppTask<QXmpp::SendResult> QXmppClientPrivate::sendEncryptedIq(QXmppTask<QXmppE2eeExtension::IqEncryptResult> task)
{
    auto result = co_await task.withContext(q);
    if (std::holds_alternative<QXmppError>(result)) {
        co_return std::get<QXmppError>(std::move(result));
    }
    co_return co_await stream->streamAckManager().send(QXmppPacket(*std::get<std::unique_ptr<QXmppIq>>(result)));
}

/*!
    Sends a packet always without end-to-end-encryption.

    This does the same as send(), but does not do any end-to-end encryption on
    the \a stanza.

    Returns a QXmppTask that makes it possible to track the state of the packet.

    \since QXmpp 1.5
*/
QXmppTask<QXmpp::SendResult> QXmppClient::send(QXmppStanza &&stanza, const std::optional<QXmppSendStanzaParams> &)
{
    return d->stream->streamAckManager().send(stanza);
}

/*!
    Sends the stanza with the same encryption as \a e2eeMetadata.

    When there is no e2eeMetadata given this always sends the stanza without
    end-to-end encryption.
    Intended to be used for replies to IQs and messages.

    \since QXmpp 1.5

    \a params and \a stanza.
*/
QXmppTask<QXmpp::SendResult> QXmppClient::reply(QXmppStanza &&stanza, const std::optional<QXmppE2eeMetadata> &e2eeMetadata, const std::optional<QXmppSendStanzaParams> &params)
{
    // This should pick the right e2ee manager as soon as multiple encryptions
    // in parallel are supported.
    if (e2eeMetadata) {
        return sendSensitive(std::move(stanza), params);
    }
    return send(std::move(stanza), params);
}

/*!
    Sends an IQ packet and returns the response asynchronously.

    This is useful for further processing and parsing of the returned
    QDomElement. If you don't expect a special response, you may want use
    sendGenericIq().

    IQs of type 'error' are parsed automatically and returned as QXmppError with a contained
    QXmppStanza::Error.

    This does not do any end-to-encryption on the IQ.

    \sa sendSensitiveIq()

    \since QXmpp 1.5

    \a iq.
*/
QXmppTask<QXmppClient::IqResult> QXmppClient::sendIq(QXmppIq &&iq, const std::optional<QXmppSendStanzaParams> &)
{
    return d->stream->sendIq(std::move(iq));
}

/*!
    Tries to encrypt and send an IQ packet and returns the response
    asynchronously.

    This can be used for sensitive IQ requests performed from client to client.
    Most IQ requests like service discovery requests cannot be end-to-end
    encrypted or it only makes little sense to do so. This is why the default
    sendIq() does not do any additional end-to-end encryption.

    IQs of type 'error' are parsed automatically and returned as QXmppError with a contained
    QXmppStanza::Error.

    \since QXmpp 1.5

    \a params and \a iq.
*/
QXmppTask<QXmppClient::IqResult> QXmppClient::sendSensitiveIq(QXmppIq &&iq, const std::optional<QXmppSendStanzaParams> &params)
{
    return d->encryptionExtension ? d->sendSensitiveIq(std::move(iq), params) : d->stream->sendIq(std::move(iq));
}

QXmppTask<QXmppClient::IqResult> QXmppClientPrivate::sendSensitiveIq(QXmppIq iq, std::optional<QXmppSendStanzaParams> params)
{
    auto encryptResult = co_await encryptionExtension->encryptIq(std::move(iq), params);

    if (std::holds_alternative<QXmppError>(encryptResult)) {
        co_return std::get<QXmppError>(std::move(encryptResult));
    }

    auto encryptedIq = std::get<std::unique_ptr<QXmppIq>>(std::move(encryptResult));
    auto sendResult = co_await stream->sendIq(std::move(*encryptedIq)).withContext(q);

    if (std::holds_alternative<QXmppError>(sendResult)) {
        co_return std::get<QXmppError>(std::move(sendResult));
    }
    auto responseIqElement = std::get<QDomElement>(sendResult);

    // iq sent, response received
    if (!isIqResponse(responseIqElement)) {
        co_return QXmppError { u"Invalid IQ response received."_s, QXmpp::SendError::EncryptionError };
    }
    if (!encryptionExtension) {
        co_return QXmppError { u"No decryption extension found."_s, QXmpp::SendError::EncryptionError };
    }

    // try to decrypt the result (should be encrypted)
    auto decryptResult = co_await encryptionExtension->decryptIq(responseIqElement);

    co_return map<QXmppClient::IqResult>(
        [&](QXmppE2eeExtension::NotEncrypted) -> QXmppClient::IqResult {
            // the IQ response from the other entity was not encrypted
            // then report IQ response without modifications
            // TODO: should we return an QXmppError instead?
            return responseIqElement;
        },
        std::move(decryptResult));
}

/*!
    Sends an IQ \a iq and returns possible stanza errors.

    If you want to parse a special IQ response in the result case, you can use
    sendIq() and parse the returned QDomElement.

    Returns QXmpp::Success (on response type 'result') or the contained
    QXmppStanza::Error (on response type 'error').

    \since QXmpp 1.5
*/
QXmppTask<QXmppClient::EmptyResult>
QXmppClient::sendGenericIq(QXmppIq &&iq, const std::optional<QXmppSendStanzaParams> &)
{
    co_return mapToSuccess(co_await sendIq(std::move(iq)));
}

/*!
    Disconnects the client and the current presence of client changes to
    QXmppPresence::Unavailable and status text changes to "Logged out".

    \note Make sure that the clientPresence is changed to
    QXmppPresence::Available, if you are again calling connectToServer() after
    calling the disconnectFromServer() function.
*/
void QXmppClient::disconnectFromServer()
{
    // cancel reconnection
    d->reconnectionTimer->stop();

    d->clientPresence.setType(QXmppPresence::Unavailable);
    d->clientPresence.setStatusText(u"Logged out"_s);
    if (d->stream->isConnected()) {
        d->resendPresence();
    }

    d->stream->disconnectFromHost();
}

/*! Returns true if the client has authenticated with the XMPP server. */
bool QXmppClient::isAuthenticated() const
{
    return d->stream->isAuthenticated();
}

/*! Returns true if the client is connected to the XMPP server. */
bool QXmppClient::isConnected() const
{
    return d->stream->isConnected();
}

/*!
    Returns true if the current client state is "active", false if it is
    "inactive". See \xep{0352}{Client State Indication} for details.

    \since QXmpp 1.0
*/
bool QXmppClient::isActive() const
{
    return d->stream->csiManager().state() == CsiManager::Active;
}

/*!
    Sets the client state as described in \xep{0352}{Client State Indication}.

    Since QXmpp 1.8, the state is restored across reconnects. QXmpp will re-send the state of
    'inactive' on connection if that was set before. Stream resumptions are also handled.

    \since QXmpp 1.0

    \a active.
*/
void QXmppClient::setActive(bool active)
{
    d->stream->csiManager().setState(active ? CsiManager::Active : CsiManager::Inactive);
}

/*!
    Returns the current \xep{0198}{Stream Management} state of the connection.

    Upon connection of the client this can be used to check whether the
    previous stream has been resumed.

    \since QXmpp 1.4
*/
QXmppClient::StreamManagementState QXmppClient::streamManagementState() const
{
    if (d->stream->c2sStreamManager().enabled()) {
        if (d->stream->c2sStreamManager().streamResumed()) {
            return ResumedStream;
        }
        return NewStream;
    }
    return NoStreamManagement;
}

QXmppOutgoingClient *QXmppClient::stream() const
{
    return d->stream;
}

QXmppClient::State QXmppClient::state() const
{
    if (d->stream->isConnected()) {
        return QXmppClient::ConnectedState;
    }
    auto state = d->stream->xmppSocket().internalSocket()->state();
    if (state != QAbstractSocket::UnconnectedState && state != QAbstractSocket::ClosingState) {
        return QXmppClient::ConnectingState;
    } else {
        return QXmppClient::DisconnectedState;
    }
}

/*! Returns the client's current presence. */
QXmppPresence QXmppClient::clientPresence() const
{
    return d->clientPresence;
}

/*!
    Changes the presence of the connected client.

    The connection to the server will be updated accordingly:

    \list
    \li If the presence type is QXmppPresence::Unavailable, the connection
    to the server will be closed.
    \li Otherwise, the connection to the server will be established
    as needed.
    \endlist

    \a presence is a QXmppPresence object.
*/
void QXmppClient::setClientPresence(const QXmppPresence &presence)
{
    d->clientPresence = presence;
    d->addProperCapability(d->clientPresence);

    if (presence.type() == QXmppPresence::Unavailable) {
        // cancel reconnection
        d->reconnectionTimer->stop();

        // NOTE: we can't call disconnect() because it alters
        // the client presence
        if (d->stream->isConnected()) {
            d->resendPresence();
        }

        d->stream->disconnectFromHost();
    } else if (d->stream->isConnected()) {
        d->resendPresence();
    } else {
        connectToServer(d->stream->configuration(), presence);
    }
}

/*! Returns the socket error if error() is QXmppClient::SocketError. */
QAbstractSocket::SocketError QXmppClient::socketError()
{
    return d->stream->xmppSocket().internalSocket()->error();
}

/*! Returns the human-readable description of the last socket error if error() is QXmppClient::SocketError. */
QString QXmppClient::socketErrorString() const
{
    return d->stream->xmppSocket().internalSocket()->errorString();
}

/*! Returns the XMPP stream error if QXmppClient::Error is QXmppClient::XmppStreamError. */
QXmppStanza::Error::Condition QXmppClient::xmppStreamError()
{
    return d->stream->xmppStreamError();
}

void QXmppClient::injectIq(const QDomElement &element, const std::optional<QXmppE2eeMetadata> &e2eeMetadata)
{
    if (element.tagName() != u"iq") {
        return;
    }
    if (!StanzaPipeline::process(d->extensions, element, e2eeMetadata)) {
        const auto iqType = element.attribute(u"type"_s);
        if (iqType == u"get" || iqType == u"set") {
            // send error IQ
            using Err = QXmppStanza::Error;

            QXmppIq iq(QXmppIq::Error);
            iq.setTo(element.attribute(u"from"_s));
            iq.setId(element.attribute(u"id"_s));
            const auto errMessage = e2eeMetadata.has_value()
                ? u"Feature not implemented or not supported with end-to-end encryption."_s
                : u"Feature not implemented."_s;
            iq.setError(Err(Err::Cancel, Err::FeatureNotImplemented, errMessage));
            reply(std::move(iq), e2eeMetadata);
        }
        // don't do anything for "result" and "error" IQs
    }
}

/*! Processes the message with message handlers and emits messageReceived as a fallback. */
bool QXmppClient::injectMessage(QXmppMessage &&message)
{
    auto handled = MessagePipeline::process(this, d->extensions, std::move(message));
    if (!handled) {
        // no extension handled the message
        Q_EMIT messageReceived(message);
    }
    return handled;
}

/*!
    Sets stream errors \a errors that are ignored if they occur.

    \since QXmpp 1.9
*/
void QXmppClient::setIgnoredStreamErrors(const QVector<QXmpp::StreamError> &errors)
{
    d->ignoredStreamErrors = errors;
}

/*! Give extensions a chance to handle incoming stanzas. */
void QXmppClient::_q_elementReceived(const QDomElement &element, bool &handled)
{
    // The stanza comes directly from the XMPP stream, so it's not end-to-end
    // encrypted and there's no e2ee metadata (std::nullopt).
    handled = StanzaPipeline::process(d->extensions, element, std::nullopt) ||
        MessagePipeline::process(this, d->extensions, d->encryptionExtension, element);
}

void QXmppClient::_q_reconnect()
{
    if (d->stream->configuration().autoReconnectionEnabled()) {
        debug(u"Reconnecting to server"_s);
        d->stream->connectToHost();
    }
}

void QXmppClient::onInternalSocketStateChanged()
{
    // FIXME: only emit signal if state actually changed
    Q_EMIT stateChanged(state());
}

/*! At connection establishment, send initial presence. */
void QXmppClient::_q_streamConnected(const QXmpp::Private::SessionBegin &session)
{
    d->receivedConflict = false;
    d->reconnectionTries = 0;

    // notify managers
    if (session.fastTokenChanged) {
        Q_EMIT credentialsChanged();
    }
    Q_EMIT connected();
    Q_EMIT stateChanged(QXmppClient::ConnectedState);

    // send initial presence
    if (d->stream->isAuthenticated() && streamManagementState() != ResumedStream) {
        d->resendPresence();
    }
}

void QXmppClient::_q_streamDisconnected()
{
    // notify managers
    Q_EMIT disconnected();
    Q_EMIT stateChanged(QXmppClient::DisconnectedState);
}

QXmppLogger *QXmppClient::logger() const
{
    return d->logger;
}

/*!
    Sets the QXmppLogger associated with the current QXmppClient.

    \a logger.
*/
void QXmppClient::setLogger(QXmppLogger *logger)
{
    if (logger != d->logger) {
        if (d->logger) {
            disconnect(this, &QXmppLoggable::logMessage,
                       d->logger, &QXmppLogger::log);
            disconnect(this, &QXmppLoggable::setGauge,
                       d->logger, &QXmppLogger::setGauge);
            disconnect(this, &QXmppLoggable::updateCounter,
                       d->logger, &QXmppLogger::updateCounter);
        }

        d->logger = logger;
        if (d->logger) {
            connect(this, &QXmppLoggable::logMessage,
                    d->logger, &QXmppLogger::log);
            connect(this, &QXmppLoggable::setGauge,
                    d->logger, &QXmppLogger::setGauge);
            connect(this, &QXmppLoggable::updateCounter,
                    d->logger, &QXmppLogger::updateCounter);
        }

        Q_EMIT loggerChanged(d->logger);
    }
}
