// SPDX-FileCopyrightText: 2009 Manjeet Dahiya <manjeetdahiya@gmail.com>
// SPDX-FileCopyrightText: 2009 Ian Reinhart Geiser <geiseri@kde.org>
//
// SPDX-License-Identifier: LGPL-2.1-or-later

#include "QXmppAsync_p.h"
#include "QXmppClient_p.h"
#include "QXmppDiscoveryManager.h"
#include "QXmppDiscoveryManager_p.h"
#include "QXmppMessage.h"
#include "QXmppPacket_p.h"
#include "QXmppRosterManager.h"
#include "QXmppRpcManager.h"
#include "QXmppStreamManagement_p.h"
#include "QXmppVCardManager.h"
#include "QXmppVersionManager.h"

#include "Async.h"
#include "StringLiterals.h"

#include <QEventLoop>
#include <QTimer>

using namespace QXmpp;
using namespace QXmpp::Private;

// Client

/*!
    \brief You need to implement this method to process incoming XMPP
    stanzas.

    You should return true if the stanza was handled and no further
    processing should occur, or false to let other extensions process
    the stanza.

    End-to-end encrypted stanzas are not passed to this overload, for that
    purpose use the new overload instead.

    \deprecated This is deprecated since QXmpp 1.5. Please use
    QXmppClientExtension::handleStanza(const QDomElement &stanza,
    const std::optional<QXmppE2eeMetadata> &e2eeMetadata).
    Currently both methods are called by the client, so only implement one!
*/
bool QXmppClientExtension::handleStanza(const QDomElement &)
{
    return false;
}

/*!
    Returns the reference to the QXmppRosterManager object of the connected client.
    Use this to get the list of friends in the roster and their presence information.

    \deprecated This method is deprecated since QXmpp 1.1. Use
    \c QXmppClient::findExtension<QXmppRosterManager>() instead.
*/
QXmppRosterManager &QXmppClient::rosterManager()
{
    return *findExtension<QXmppRosterManager>();
}

/*!
    Returns the reference to QXmppVCardManager, implementation of \xep{0054}{vcard-temp}.

    \deprecated This method is deprecated since QXmpp 1.1. Use
    \c QXmppClient::findExtension<QXmppVCardManager>() instead.
*/
QXmppVCardManager &QXmppClient::vCardManager()
{
    return *findExtension<QXmppVCardManager>();
}

/*!
    Returns the reference to QXmppVersionManager, implementation of \xep{0092}{Software Version}.

    \deprecated This method is deprecated since QXmpp 1.1. Use
    \c QXmppClient::findExtension<QXmppVersionManager>() instead.
*/
QXmppVersionManager &QXmppClient::versionManager()
{
    return *findExtension<QXmppVersionManager>();
}

/*!
    After successfully connecting to the server use this function to send
    stanzas to the server. This function can solely be used to send various kind
    of stanzas to the server. QXmppStanza is a parent class of all the stanzas
    QXmppMessage, QXmppPresence, QXmppIq, QXmppBind, QXmppRosterIq, QXmppSession
    and QXmppVCard.

    This function does not end-to-end encrypt the packets.

    \a packet is a valid XMPP stanza. It can be an iq, a message or a presence stanza.

    Returns true if the packet was sent, false otherwise.

    Following code snippet illustrates how to send a message using this function:
    \code
    QXmppMessage message(from, to, message);
    client.sendPacket(message);
    \endcode

    \deprecated
*/
bool QXmppClient::sendPacket(const QXmppNonza &packet)
{
    return d->stream->streamAckManager().sendPacketCompat(packet);
}

/*!
    Utility function to send message to all the resources associated with the
    specified bareJid. If there are no resources available, that is the contact
    is offline or not present in the roster, it will still send a message to
    the bareJid.

    \note Usage of this method is discouraged because most modern clients use
    carbon messages (\xep{0280}{Message Carbons}) and MAM (\xep{0313}{Message
    Archive Management}) and so could possibly receive messages multiple times
    or not receive them at all.
    \c QXmppClient::sendPacket() should be used instead with a \c QXmppMessage.

    \a bareJid is the bareJid of the receiving entity. \a message is the message
    string to be sent.

    \deprecated
*/
void QXmppClient::sendMessage(const QString &bareJid, const QString &message)
{
    QXmppRosterManager *rosterManager = findExtension<QXmppRosterManager>();

    const QStringList resources = rosterManager
        ? rosterManager->getResources(bareJid)
        : QStringList();

    if (!resources.isEmpty()) {
        for (const auto &resource : resources) {
            send(QXmppMessage({}, bareJid + u"/"_s + resource, message));
        }
    } else {
        send(QXmppMessage({}, bareJid, message));
    }
}

// DiscoveryManager

QT_WARNING_PUSH
QT_WARNING_DISABLE_DEPRECATED
/*!
    Requests information from the specified XMPP entity \a jid, optionally
    restricted to a target node \a node.

    \deprecated Use info()
*/
QString QXmppDiscoveryManager::requestInfo(const QString &jid, const QString &node)
{
    QXmppDiscoveryIq request;
    request.setType(QXmppIq::Get);
    request.setQueryType(QXmppDiscoveryIq::InfoQuery);
    request.setTo(jid);
    if (!node.isEmpty()) {
        request.setQueryNode(node);
    }
    return client()->sendLegacyId(request);
}

/*!
    Requests items from the specified XMPP entity \a jid, optionally restricted
    to a target node \a node.

    \deprecated Use items()
*/
QString QXmppDiscoveryManager::requestItems(const QString &jid, const QString &node)
{
    QXmppDiscoveryIq request;
    request.setType(QXmppIq::Get);
    request.setQueryType(QXmppDiscoveryIq::ItemsQuery);
    request.setTo(jid);
    if (!node.isEmpty()) {
        request.setQueryNode(node);
    }
    return client()->sendLegacyId(request);
}

/*!
    \typedef QXmppDiscoveryManager::InfoResult

    Contains the discovery information result in the form of an QXmppDiscoveryIq
    or (in case the request did not succeed) a QXmppStanza::Error.

    \since QXmpp 1.5
*/

/*!
    \typedef QXmppDiscoveryManager::ItemsResult

    Contains a list of service discovery items or (in case the request did not
    succeed) a QXmppStanza::Error.

    \since QXmpp 1.5
*/

/*!
    Requests information from the specified XMPP entity \a jid, optionally
    restricted to a target node \a node.

    \deprecated Use info()
    \since QXmpp 1.5
*/
QXmppTask<QXmppDiscoveryManager::InfoResult> QXmppDiscoveryManager::requestDiscoInfo(const QString &jid, const QString &node)
{
    QXmppDiscoveryIq request;
    request.setType(QXmppIq::Get);
    request.setQueryType(QXmppDiscoveryIq::InfoQuery);
    request.setTo(jid);
    request.setQueryNode(node);

    auto infoResult = co_await info(jid, node, CachePolicy::Strict);
    if (hasError(infoResult)) {
        co_return getError(std::move(infoResult));
    }
    auto &info = getValue(infoResult);

    QXmppDiscoveryIq iq;
    iq.setQueryNode(info.node());
    iq.setQueryType(QXmppDiscoveryIq::InfoQuery);
    iq.setFeatures(info.features());
    iq.setIdentities(info.identities());
    iq.setDataForms(info.dataForms());
    co_return iq;
}

/*!
    Requests items from the specified XMPP entity \a jid, optionally restricted
    to a target node \a node.

    \deprecated Use items()
    \since QXmpp 1.5
*/
QXmppTask<QXmppDiscoveryManager::ItemsResult> QXmppDiscoveryManager::requestDiscoItems(const QString &jid, const QString &node)
{
    return items(jid, node, CachePolicy::Strict);
}

/*!
    Returns the client's full capabilities.

    \deprecated Use buildClientInfo()
*/
QXmppDiscoveryIq QXmppDiscoveryManager::capabilities()
{
    auto info = buildClientInfo();

    QXmppDiscoveryIq iq;
    iq.setType(QXmppIq::Result);
    iq.setQueryType(QXmppDiscoveryIq::InfoQuery);
    iq.setFeatures(info.features());
    iq.setIdentities(info.identities());
    iq.setDataForms(info.dataForms());
    return iq;
}
QT_WARNING_POP

/*!
    Sets the category of the local XMPP client.

    You can find a list of valid categories at:
    http://xmpp.org/registrar/disco-categories.html

    \deprecated Use setIdentities(), this function will remove other identities if set.
*/
void QXmppDiscoveryManager::setClientCategory(const QString &category)
{
    if (d->identities.size() != 1) {
        d->identities = { d->defaultIdentity() };
    }
    d->identities.first().setCategory(category);
}

/*!
    Sets the type of the local XMPP client.

    You can find a list of valid types at:
    http://xmpp.org/registrar/disco-categories.html

    \deprecated Use setIdentities(), this function will remove other identities if set.
*/
void QXmppDiscoveryManager::setClientType(const QString &type)
{
    if (d->identities.size() != 1) {
        d->identities = { d->defaultIdentity() };
    }
    d->identities.first().setType(type);
}

/*!
    Sets the name of the local XMPP client.

    \deprecated Use setIdentities(), this function will remove other identities if set.
*/
void QXmppDiscoveryManager::setClientName(const QString &name)
{
    if (d->identities.size() != 1) {
        d->identities = { d->defaultIdentity() };
    }
    d->identities.first().setName(name);
}

/*!
    Returns the category of the local XMPP client.

    By default this is "client".

    \deprecated Use identities()
*/
QString QXmppDiscoveryManager::clientCategory() const
{
    if (d->identities.isEmpty()) {
        return {};
    }
    return d->identities.constFirst().category();
}

/*!
    Returns the type of the local XMPP client.

    With Qt builds for Android, Blackberry, iOS or Windows Phone this is set to
    "phone", otherwise it defaults to "pc".

    \deprecated Use identities()
*/
QString QXmppDiscoveryManager::clientType() const
{
    if (d->identities.isEmpty()) {
        return {};
    }
    return d->identities.constFirst().type();
}

/*!
    Returns the name of the local XMPP client.

    By default this is "QXmpp x.y.z".

    \deprecated Use identities()
*/
QString QXmppDiscoveryManager::clientApplicationName() const
{
    if (d->identities.isEmpty()) {
        return {};
    }
    return d->identities.constFirst().name();
}

/*!
    Returns the client's extended information form, as defined by \xep{0128}{Service Discovery Extensions}.

    \deprecated Use infoForms()
*/
QXmppDataForm QXmppDiscoveryManager::clientInfoForm() const
{
    return d->dataForms.isEmpty() ? QXmppDataForm() : d->dataForms.constFirst();
}

/*!
    Sets the client's extended information form, as defined by \xep{0128}{Service Discovery Extensions}.

    \deprecated Use setInfoForms()
*/
void QXmppDiscoveryManager::setClientInfoForm(const QXmppDataForm &form)
{
    d->dataForms = { form };
}

// RemoteMethod

QT_WARNING_PUSH
QT_WARNING_DISABLE_DEPRECATED

QXmppRemoteMethod::QXmppRemoteMethod(const QString &jid, const QString &method, const QVariantList &args, QXmppClient *client) : QObject(client), m_client(client)
{
    m_payload.setTo(jid);
    m_payload.setFrom(client->configuration().jid());
    m_payload.setMethod(method);
    m_payload.setArguments(args);
}

QXmppRemoteMethodResult QXmppRemoteMethod::call()
{
    // FIXME : spinning an event loop is a VERY bad idea, it can cause
    // us to lose incoming packets
    QEventLoop loop(this);
    connect(this, &QXmppRemoteMethod::callDone, &loop, &QEventLoop::quit);
    QTimer::singleShot(30000, &loop, &QEventLoop::quit);  // Timeout in case the other end hangs...

    auto payload = m_payload;
    m_client->send(std::move(m_payload));

    loop.exec(QEventLoop::ExcludeUserInputEvents | QEventLoop::WaitForMoreEvents);
    return m_result;
}

void QXmppRemoteMethod::gotError(const QXmppRpcErrorIq &iq)
{
    if (iq.id() == m_payload.id()) {
        m_result.hasError = true;
        m_result.errorMessage = iq.error().text();
        m_result.code = iq.error().type();
        Q_EMIT callDone();
    }
}

void QXmppRemoteMethod::gotResult(const QXmppRpcResponseIq &iq)
{
    if (iq.id() == m_payload.id()) {
        m_result.hasError = false;
        // FIXME: we don't handle multiple responses
        const auto values = iq.values();
        m_result.result = values.first();
        Q_EMIT callDone();
    }
}

// RpcManager

/*! Constructs a QXmppRpcManager. */
QXmppRpcManager::QXmppRpcManager()
{
}

/*! Adds a local \a interface which can be queried using RPC. */
void QXmppRpcManager::addInvokableInterface(QXmppInvokable *interface)
{
    m_interfaces[QString::fromUtf8(interface->metaObject()->className())] = interface;
}

/*! Invokes a remote interface using RPC. */
void QXmppRpcManager::invokeInterfaceMethod(const QXmppRpcInvokeIq &iq)
{
    QXmppStanza::Error error;

    const QStringList methodBits = iq.method().split(u'.');
    if (methodBits.size() != 2) {
        return;
    }
    const QString interface = methodBits.first();
    const QString method = methodBits.last();
    QXmppInvokable *iface = m_interfaces.value(interface);
    if (iface) {
        if (iface->isAuthorized(iq.from())) {

            if (iface->interfaces().contains(method)) {
                QVariant result = iface->dispatch(method.toLatin1(),
                                                  iq.arguments());
                QXmppRpcResponseIq resultIq;
                resultIq.setId(iq.id());
                resultIq.setTo(iq.from());
                resultIq.setValues(QVariantList() << result);
                client()->send(std::move(resultIq));
                return;
            } else {
                error.setType(QXmppStanza::Error::Cancel);
                error.setCondition(QXmppStanza::Error::ItemNotFound);
            }
        } else {
            error.setType(QXmppStanza::Error::Auth);
            error.setCondition(QXmppStanza::Error::Forbidden);
        }
    } else {
        error.setType(QXmppStanza::Error::Cancel);
        error.setCondition(QXmppStanza::Error::ItemNotFound);
    }
    QXmppRpcErrorIq errorIq;
    errorIq.setId(iq.id());
    errorIq.setTo(iq.from());
    errorIq.setQuery(iq);
    errorIq.setError(error);
    client()->send(std::move(errorIq));
}

/*!
    Calls remote method \a interface on entity \a jid with the supplied
    arguments \a arg1 through \a arg10.

    \note This method blocks until the response is received, and it may
    cause XMPP stanzas to be lost!

    \a arg3, \a arg9, \a arg6, \a arg4, \a arg7, \a arg8, \a arg5, and \a arg2.
*/
QXmppRemoteMethodResult QXmppRpcManager::callRemoteMethod(const QString &jid,
                                                          const QString &interface,
                                                          const QVariant &arg1,
                                                          const QVariant &arg2,
                                                          const QVariant &arg3,
                                                          const QVariant &arg4,
                                                          const QVariant &arg5,
                                                          const QVariant &arg6,
                                                          const QVariant &arg7,
                                                          const QVariant &arg8,
                                                          const QVariant &arg9,
                                                          const QVariant &arg10)
{
    QVariantList args;
    if (arg1.isValid()) {
        args << arg1;
    }
    if (arg2.isValid()) {
        args << arg2;
    }
    if (arg3.isValid()) {
        args << arg3;
    }
    if (arg4.isValid()) {
        args << arg4;
    }
    if (arg5.isValid()) {
        args << arg5;
    }
    if (arg6.isValid()) {
        args << arg6;
    }
    if (arg7.isValid()) {
        args << arg7;
    }
    if (arg8.isValid()) {
        args << arg8;
    }
    if (arg9.isValid()) {
        args << arg9;
    }
    if (arg10.isValid()) {
        args << arg10;
    }

    bool check;
    Q_UNUSED(check)

    QXmppRemoteMethod method(jid, interface, args, client());
    check = connect(this, SIGNAL(rpcCallResponse(QXmppRpcResponseIq)),
                    &method, SLOT(gotResult(QXmppRpcResponseIq)));
    Q_ASSERT(check);
    check = connect(this, SIGNAL(rpcCallError(QXmppRpcErrorIq)),
                    &method, SLOT(gotError(QXmppRpcErrorIq)));
    Q_ASSERT(check);

    return method.call();
}

QStringList QXmppRpcManager::discoveryFeatures() const
{
    return {
        // XEP-0009: Jabber-RPC
        ns_rpc.toString(),
    };
}

QList<QXmppDiscoIdentity> QXmppRpcManager::discoveryIdentities() const
{
    QXmppDiscoIdentity identity;
    identity.setCategory(u"automation"_s);
    identity.setType(u"rpc"_s);
    return QList<QXmppDiscoIdentity>() << identity;
}

bool QXmppRpcManager::handleStanza(const QDomElement &element)
{
    // XEP-0009: Jabber-RPC
    if (QXmppRpcInvokeIq::isRpcInvokeIq(element)) {
        QXmppRpcInvokeIq rpcIqPacket;
        rpcIqPacket.parse(element);
        invokeInterfaceMethod(rpcIqPacket);
        return true;
    } else if (QXmppRpcResponseIq::isRpcResponseIq(element)) {
        QXmppRpcResponseIq rpcResponseIq;
        rpcResponseIq.parse(element);
        Q_EMIT rpcCallResponse(rpcResponseIq);
        return true;
    } else if (QXmppRpcErrorIq::isRpcErrorIq(element)) {
        QXmppRpcErrorIq rpcErrorIq;
        rpcErrorIq.parse(element);
        Q_EMIT rpcCallError(rpcErrorIq);
        return true;
    }
    return false;
}

QT_WARNING_POP

#include "moc_QXmppRemoteMethod.cpp"
#include "moc_QXmppRpcManager.cpp"
