// SPDX-FileCopyrightText: 2023 Melvin Keskin <melvo@olomono.de>
//
// SPDX-License-Identifier: LGPL-2.1-or-later

#include "QXmppMixManager.h"

#include "QXmppAccountMigrationManager.h"
#include "QXmppClient.h"
#include "QXmppConstants_p.h"
#include "QXmppDiscoveryIq.h"
#include "QXmppDiscoveryManager.h"
#include "QXmppGlobal.h"
#include "QXmppMessage.h"
#include "QXmppMixInfoItem.h"
#include "QXmppMixInvitation.h"
#include "QXmppMixIq.h"
#include "QXmppMixIq_p.h"
#include "QXmppPubSubEvent.h"
#include "QXmppPubSubManager.h"
#include "QXmppRosterManager.h"
#include "QXmppUtils.h"
#include "QXmppUtils_p.h"

#include "Algorithms.h"
#include "Async.h"
#include "Enums.h"
#include "StringLiterals.h"
#include "XmlWriter.h"

#include <QDomElement>

using namespace QXmpp;
using namespace QXmpp::Private;

using enum QXmppMixManager::Support;

class QXmppMixManagerPrivate
{
public:
    QXmppPubSubManager *pubSubManager = nullptr;
    QXmppDiscoveryManager *discoveryManager = nullptr;
    QXmppMixManager::Support participantSupport;
    QXmppMixManager::Support messageArchivingSupport;
    QProperty<QList<QXmppMixManager::Service>> services;
    std::optional<QXmppDiscoServicesWatch> servicesWatch;
    QPropertyNotifier servicesNotifier;
};

namespace QXmpp::Private {

struct MixData {
    struct Item {
        QString jid;
        QString nick;

        static constexpr std::tuple XmlTag = { u"item", ns_qxmpp_export };

        void parse(const QDomElement &element)
        {
            jid = element.attribute(u"jid"_s);
            nick = element.attribute(u"nick"_s);
        }

        void toXml(XmlWriter &w) const
        {
            w.write(Element {
                u"item",
                OptionalAttribute { u"jid", jid },
                OptionalAttribute { u"nick", nick },
            });
        }
    };

    using Items = QList<Item>;

    Items items;

    static std::variant<MixData, QXmppError> fromDom(const QDomElement &el)
    {
        if (el.tagName() != u"mix" || el.namespaceURI() != ns_qxmpp_export) {
            return QXmppError { u"Invalid element."_s, {} };
        }

        return MixData {
            parseChildElements<QList<Item>>(el),
        };
    }

    void toXml(XmlWriter &w) const
    {
        w.write(Element { u"mix", items });
    }
};

static void serializeMixData(const MixData &d, QXmlStreamWriter &writer)
{
    XmlWriter(&writer).write(d);
}

}  // namespace QXmpp::Private

/*!
    \class QXmppMixManager
    \inmodule QXmpp

    This class manages group chat communication as specified in the following XEPs:
    \list
        \li \xep{0369}{Mediated Information eXchange (MIX)}
        \li \xep{0405}{Mediated Information eXchange (MIX): Participant Server Requirements}
        \li \xep{0406}{Mediated Information eXchange (MIX): MIX Administration}
        \li \xep{0407}{Mediated Information eXchange (MIX): Miscellaneous Capabilities}
    \endlist

    In order to use this manager, make sure to add all managers needed by this manager:
    \code
    client->addNewExtension<QXmppDiscoveryManager>();
    client->addNewExtension<QXmppPubSubManager>();
    \endcode

    Afterwards, you need to add this manager to the client:
    \code
    auto *manager = client->addNewExtension<QXmppMixManager>();
    \endcode

    If you want to be informed about updates of the channel (e.g., its configuration or allowed
    JIDs), make sure to subscribe to the corresponding nodes.

    In order to send a message to a MIX channel, you have to set the type QXmppMessage::GroupChat.

    Example for an unencrypted message:
    \code
    message->setType(QXmppMessage::GroupChat);
    message->setTo("group@mix.example.org")
    client->send(std::move(message));
    \endcode

    Example for an encrypted message decryptable by Alice and Bob:
    \code
    message->setType(QXmppMessage::GroupChat);
    message->setTo("group@mix.example.org")

    QXmppSendStanzaParams params;
    params.setEncryptionJids({ "alice@example.org", "bob@example.com" })

    client->sendSensitive(std::move(message), params);
    \endcode

    If you receive a message, you can check whether it contains an invitation to a MIX channel and
    use that invitation to join the channel:
    \code
    if (auto invitation = message.mixInvitation()) {
    manager->joinChannel(*invitation, nickname, nodes);
    }
    \endcode

    In order to invite someone to a MIX channel that allows the invitee to participate, you can
    create an invitation on your own.

    That can be done in the following cases:
    \list
        \li Everybody is allowed to participate in the channel.
        \li The invitee is explicitly allowed to participate in the channel.
            That is particularly relevant if the channel does not support invitations received via
            requestInvitation() but the inviter is permitted to allow JIDs via allowJid().
            In that case, the invitee's JID has to be allowed before the invitee tries to join the
            channel.
    \endlist

    Invitations are sent as regular messages.
    They are not meant to be read by a human.
    Instead, the receiving client needs to support it.
    But you can add an appropriate text to the body of the invitation message to enable human users
    of clients that do not support that feature to join the channel manually.
    For example, you could add the JID of the channel or an XMPP URI to the body.

    If the body (i.e., the invitation text) is empty, the message would neither be delivered to all
    clients via \xep{0280}{Message Carbons} nor delivered to clients which are currently offline via
    \xep{0313}{Message Archive Management}.
    To enforce that behavior, set a corresponding message type and message processing hint:
    \code
    QXmppMixInvitation invitation;

    invitation.setInviterJid(client->configuration().jidBare());
    invitation.setInviteeJid(inviteeJid);
    invitation.setChannelJid(channelJid);

    QXmppMessage message;

    message.setTo(inviteeJid);
    message.setMixInvitation(invitation);

    if (messageBody.isEmpty()) {
    message.setType(QXmppMessage::Chat);
    message.addHint(QXmppMessage::Store);
    } else {
    message.setBody(messageBody);
    }

    client()->sendSensitive(std::move(message));
    \endcode

    In order to invite someone to a MIX channel that does not yet allow the invitee to participate,
    you can request an invitation from the channel (if permitted to do so):
    \code
    manager->requestInvitation().then(this, [](QXmppMixManager::InvitationResult &&result) mutable {
    if (QXmpp::hasError(result)) {
    // Handle the error.
    } else {
    auto invitation = QXmpp::getValue(std::move(result));
    // Create and send the invitation.
    }
    });
    \endcode

    \ingroup Managers

    \since QXmpp 1.7
*/

/*!
    \property QXmppMixManager::participantSupport

    \sa QXmppMixManager::participantSupport()
*/

/*!
    \property QXmppMixManager::messageArchivingSupport

    \sa QXmppMixManager::messageArchivingSupport()
*/

/*!
    \property QXmppMixManager::services

    \sa QXmppMixManager::services()
*/

/*!
    \enum QXmppMixManager::Support
    \brief Describes whether a feature is supported by the server.

    Server support for a feature.

    The information is cached until a new connection is established. That makes
    it possible to retrieve the latest state even while the client is
    disconnected.

    \value Unknown Whether the server supports the feature is not known.
    That means, there is no corresponding information from the server (yet).
    \value Unsupported The server does not support the feature.
    \value Supported The server supports the feature.
*/

/*!
    \struct QXmppMixManager::Service
    \inmodule QXmpp

    Service providing MIX channels and corresponding nodes.
*/

/*!
    \variable QXmppMixManager::Service::jid

    JID of the service.
*/

/*!
    \variable QXmppMixManager::Service::channelsSearchable

    Whether the service can be searched for channels.
*/

/*!
    \variable QXmppMixManager::Service::channelCreationAllowed

    Whether channels can be created on the service.
*/

bool QXmppMixManager::Service::operator==(const Service &other) const
{
    return jid == other.jid &&
        channelsSearchable == other.channelsSearchable &&
        channelCreationAllowed == other.channelCreationAllowed;
}

/*!
    \struct QXmppMixManager::Subscription
    \inmodule QXmpp

    Subscription to nodes of a MIX channel.
*/

/*!
    \variable QXmppMixManager::Subscription::additions

    Nodes belonging to the channel that are subscribed to.
*/

/*!
    \variable QXmppMixManager::Subscription::removals

    Nodes belonging to the channel that are unsubscribed from.
*/

/*!
    \struct QXmppMixManager::Participation
    \inmodule QXmpp

    Participation in a channel.
*/

/*!
    \variable QXmppMixManager::Participation::participantId

    ID of the user within the channel.
*/

/*!
    \variable QXmppMixManager::Participation::nickname

    Nickname of the user within the channel.

    If the server modified the desired nickname, this is the modified one.
*/

/*!
    \variable QXmppMixManager::Participation::subscriptions

    Nodes belonging to the joined channel that are subscribed to.

    If not all desired nodes could be subscribed, this contains only the
    subscribed nodes.
*/

/*!
    \typedef QXmppMixManager::Jid

    JID of a user or domain.
*/

/*!
    \typedef QXmppMixManager::ChannelJid

    JID of a MIX channel.
*/

/*!
    \typedef QXmppMixManager::Nickname

    Nickname of the user within a MIX channel.

    If the server modified the desired nickname, this is the modified one.
*/

/*!
    \typedef QXmppMixManager::CreationResult

    Contains the JID of the created MIX channel or a QXmppError on failure.
*/

/*!
    \typedef QXmppMixManager::ChannelJidResult

    Contains the JIDs of all discoverable MIX channels of a MIX service or a QXmppError if it
    failed.
*/

/*!
    \typedef QXmppMixManager::ChannelNodeResult

    Contains all nodes of the requested MIX channel that can be subscribed by the user or a
    QXmppError on failure.
*/

/*!
    \typedef QXmppMixManager::ConfigurationResult

    Contains the configuration of the MIX channel or a QXmppError on failure.
*/

/*!
    \typedef QXmppMixManager::InformationResult

    Contains the information of the MIX channel or a QXmppError on failure.
*/

/*!
    \typedef QXmppMixManager::JoiningResult

    Contains the result of the joined MIX channel or a QXmppError on failure.
*/

/*!
    \typedef QXmppMixManager::NicknameResult

    Contains the new nickname within a joined MIX channel or a QXmppError on failure.
*/

/*!
    \typedef QXmppMixManager::InvitationResult

    Contains the requested invitation to a MIX channel or a QXmppError on failure.
*/

/*!
    \typedef QXmppMixManager::SubscriptionResult

    Contains the result of the subscribed/unsubscribed nodes belonging to a MIX channel or a
    QXmppError on failure.
*/

/*!
    \typedef QXmppMixManager::JidResult

    Contains the JIDs of users or domains that are allowed to participate resp. banned from
    participating in a MIX channel or a QXmppError on failure.
*/

/*!
    \typedef QXmppMixManager::ParticipantResult

    Contains the participants of a MIX channel or a QXmppError on failure.
*/

constexpr QStringView MIX_SERVICE_DISCOVERY_NODE = u"mix";

/*! Constructs a MIX manager. */
QXmppMixManager::QXmppMixManager()
    : d(new QXmppMixManagerPrivate())
{
    QXmppExportData::registerExtension<MixData, MixData::fromDom, serializeMixData>(u"mix", ns_qxmpp_export);
}

QXmppMixManager::~QXmppMixManager() = default;

QStringList QXmppMixManager::discoveryFeatures() const
{
    return { staticString(ns_mix) };
}

/*!
    Returns the server's support for MIX channel participants as specified in
    \xep{0405}{Mediated Information eXchange (MIX): Participant Server Requirements}.

    If the server supports it, the server interacts between a client and a MIX channel that the user
    participates in.
    E.g., the server adds the MIX channel to the user's roster after joining it.

    Returns the server support for MIX channel participants.
*/
QXmppMixManager::Support QXmppMixManager::participantSupport() const
{
    return d->participantSupport;
}

/*!
    \fn QXmppMixManager::participantSupportChanged()

    Emitted when the server's support for MIX channel participants changed.
*/

/*!
    Returns the server's support for archiving messages via \xep{0313}{Message Archive Management}
    of MIX channels the user participates in as specified in
    \xep{0405}{Mediated Information eXchange (MIX): Participant Server Requirements}.
*/
QXmppMixManager::Support QXmppMixManager::messageArchivingSupport() const
{
    return d->messageArchivingSupport;
}

/*!
    \fn QXmppMixManager::messageArchivingSupportChanged()

    Emitted when the server's support for archiving MIX messages changed.
*/

/*!
    Returns the services providing MIX on the own server.

    Such services provide MIX channels and their nodes.
    It interacts directly with clients or with their servers.
*/
QList<QXmppMixManager::Service> QXmppMixManager::services() const
{
    return d->services;
}

/*!
    \fn QXmppMixManager::servicesChanged()

    Emitted when the services providing MIX on the own server changed.
*/

/*!
    Creates a MIX channel.

    If no channel ID is passed, the channel is created with an ID provided by the MIX service.
    Furthermore, the channel cannot be discovered by anyone.
    A channel with the mentioned properties is called an "ad-hoc channel".

    The channel ID is the local part of the channel JID.
    The MIX service JID is the domain part of the channel JID.
    Example: "channel" is the channel ID and "mix.example.org" the service JID of the channel JID
    "channel@mix.example.org".

    \a serviceJid is the JID of the service. \a channelId is the ID of the
    channel (default: provided by the server).

    Returns the result of the action.
*/
QXmppTask<QXmppMixManager::CreationResult> QXmppMixManager::createChannel(const QString &serviceJid, const QString &channelId)
{
    QXmppMixIq iq;
    iq.setType(QXmppIq::Set);
    iq.setTo(serviceJid);
    iq.setActionType(QXmppMixIq::Create);
    iq.setChannelId(channelId);

    co_return parseIq<QXmppMixIq>(co_await client()->sendIq(std::move(iq)), [](QXmppMixIq &&iq) -> CreationResult {
        return iq.channelJid().isEmpty() ? iq.channelId() + u'@' + iq.from() : iq.channelJid();
    });
}

/*!
    Requests the JIDs of all discoverable MIX channels of a MIX service.

    \a serviceJid is the JID of the service that provides the channels.

    Returns the result of the action.
*/
QXmppTask<QXmppMixManager::ChannelJidResult> QXmppMixManager::requestChannelJids(const QString &serviceJid)
{
    co_return map<ChannelJidResult>(
        [](QList<QXmppDiscoItem> &&items) {
            return transform<QList<ChannelJid>>(items, &QXmppDiscoItem::jid);
        },
        co_await d->discoveryManager->items(serviceJid));
}

/*!
    Requests all nodes of the MIX channel with JID \a channelJid that can be
    subscribed by the user.

    Returns the result of the action.
*/
QXmppTask<QXmppMixManager::ChannelNodeResult> QXmppMixManager::requestChannelNodes(const QString &channelJid)
{
    co_return mapSuccess(
        co_await d->discoveryManager->items(channelJid, MIX_SERVICE_DISCOVERY_NODE.toString())
            .withContext(this),
        [](QList<QXmppDiscoItem> &&items) -> QXmppMixConfigItem::Nodes {
            return Enums::fromStrings<QXmppMixConfigItem::Node>(
                transform<QList<QString>>(std::move(items), &QXmppDiscoItem::node));
        });
}

/*!
    Requests the configuration of the MIX channel with JID \a channelJid.

    Returns the result of the action.
*/
QXmppTask<QXmppMixManager::ConfigurationResult> QXmppMixManager::requestChannelConfiguration(const QString &channelJid)
{
    co_return mapSuccess(
        co_await d->pubSubManager->requestItems<QXmppMixConfigItem>(channelJid, ns_mix_node_config.toString())
            .withContext(this),
        [](const QXmppPubSubManager::Items<QXmppMixConfigItem> &items) {
            return items.items.first();
        });
}

/*!
    Updates the configuration of a MIX channel.

    In order to use this method, retrieve the current configuration via
    requestChannelConfiguration() first, change the desired attributes and pass the configuration to
    this method.

    \a channelJid is the JID of the channel whose configuration is to be
    updated. \a configuration is the new configuration of the channel.

    Returns the result of the action.
*/
QXmppTask<QXmppClient::EmptyResult> QXmppMixManager::updateChannelConfiguration(const QString &channelJid, QXmppMixConfigItem configuration)
{
    configuration.setFormType(QXmppDataForm::Submit);
    co_return mapToSuccess(co_await d->pubSubManager->publishItem(channelJid, ns_mix_node_config.toString(), configuration)
                               .withContext(this));
}

/*!
    \fn QXmppMixManager::channelConfigurationUpdated(const QString &channelJid, const QXmppMixConfigItem &configuration)

    Emitted when the configuration of a MIX channel is updated.

    \a channelJid is the JID of the channel whose configuration is updated.
    \a configuration is the new channel configuration.
*/

/*!
    Requests the information of the MIX channel with JID \a channelJid.

    Returns the result of the action.
*/
QXmppTask<QXmppMixManager::InformationResult> QXmppMixManager::requestChannelInformation(const QString &channelJid)
{
    co_return mapSuccess(
        co_await d->pubSubManager->requestItems<QXmppMixInfoItem>(channelJid, ns_mix_node_info.toString()).withContext(this),
        [](QXmppPubSubManager::Items<QXmppMixInfoItem> &&items) {
            return items.items.first();
        });
}

/*!
    Updates the information of a MIX channel.

    In order to use this method, retrieve the current information via requestChannelInformation()
    first, change the desired attributes and pass the information to this method.

    \a channelJid is the JID of the channel whose information is to be updated.
    \a information is the new information of the channel.

    Returns the result of the action.
*/
QXmppTask<QXmppClient::EmptyResult> QXmppMixManager::updateChannelInformation(const QString &channelJid, QXmppMixInfoItem information)
{
    information.setFormType(QXmppDataForm::Submit);
    co_return mapToSuccess(co_await d->pubSubManager->publishItem(channelJid, ns_mix_node_info.toString(), information).withContext(this));
}

/*!
    \fn QXmppMixManager::channelInformationUpdated(const QString &channelJid, const QXmppMixInfoItem &information)

    Emitted when the information of a MIX channel is updated.

    \a channelJid is the JID of the channel whose information is updated.
    \a information is the new channel information.
*/

/*!
    Joins a MIX channel to become a participant of it.

    \a channelJid is the JID of the channel being joined. \a nickname is the
    nickname of the user which is usually required by the server (default: no
    nickname is set). \a nodes are the nodes of the channel that are
    subscribed to for receiving their updates (default: all nodes are
    subcribed to).

    Returns the result of the action.
*/
QXmppTask<QXmppMixManager::JoiningResult> QXmppMixManager::joinChannel(const QString &channelJid, const QString &nickname, QXmppMixConfigItem::Nodes nodes)
{
    return joinChannel(prepareJoinIq(channelJid, nickname, nodes));
}

/*!
    Joins a MIX channel via an invitation to become a participant of it.

    \a invitation is the invitation to the channel. \a nickname is the
    nickname of the user which is usually required by the server (default: no
    nickname is set). \a nodes are the nodes of the channel that are
    subscribed to for receiving their updates (default: all nodes are
    subcribed to).

    Returns the result of the action.
*/
QXmppTask<QXmppMixManager::JoiningResult> QXmppMixManager::joinChannel(const QXmppMixInvitation &invitation, const QString &nickname, QXmppMixConfigItem::Nodes nodes)
{
    auto iq = prepareJoinIq(invitation.channelJid(), nickname, nodes);

    // Submit the invitation only if it was generated by the channel and thus needed to join.
    if (!invitation.token().isEmpty()) {
        iq.setInvitation(invitation);
    }

    return joinChannel(std::move(iq));
}

/*!
    Updates the nickname within a channel.

    If the update succeeded, the new nickname is returned which may differ from the requested one.

    \a channelJid is the JID of the channel. \a nickname is the nickname to be
    set.

    Returns the result of the action.
*/
QXmppTask<QXmppMixManager::NicknameResult> QXmppMixManager::updateNickname(const QString &channelJid, const QString &nickname)
{
    QXmppMixIq iq;
    iq.setType(QXmppIq::Set);
    iq.setTo(channelJid);
    iq.setActionType(QXmppMixIq::SetNick);
    iq.setNick(nickname);

    co_return parseIq<QXmppMixIq>(co_await client()->sendIq(std::move(iq)), [](QXmppMixIq &&iq) -> NicknameResult {
        return iq.nick();
    });
}

/*!
    Updates the subscriptions to nodes of a MIX channel.

    \a channelJid is the JID of the channel. \a subscriptionAdditions are the
    nodes to subscribe to. \a subscriptionRemovals are the nodes to
    unsubscribe from.

    Returns the result of the action.
*/
QXmppTask<QXmppMixManager::SubscriptionResult> QXmppMixManager::updateSubscriptions(const QString &channelJid, QXmppMixConfigItem::Nodes subscriptionAdditions, QXmppMixConfigItem::Nodes subscriptionRemovals)
{
    QXmppMixSubscriptionUpdateIq iq;
    iq.setType(QXmppIq::Set);
    iq.setTo(channelJid);
    iq.setAdditions(subscriptionAdditions);
    iq.setRemovals(subscriptionRemovals);

    co_return mapSuccess(
        parseIq<QXmppMixSubscriptionUpdateIq>(co_await client()->sendIq(std::move(iq))),
        [](QXmppMixSubscriptionUpdateIq &&iq) {
            return Subscription { iq.additions(), iq.removals() };
        });
}

/*!
    Requests an invitation to a MIX channel that the invitee is not yet allowed to participate in.

    The invitee can use the invitation to join the channel.

    That invitation mechanism avoids storing allowed JIDs for an indefinite time if the
    corresponding user never joins the channel.
    By using this method, there is no need to allow the invitee to participate in the channel via
    allowJid().

    This method can be used in the following cases:
    \list
        \li The inviter is an administrator of the channel.
        \li The inviter is a participant of the channel and the channel allows all participants to
            invite new users.
    \endlist

    \a channelJid is the JID of the channel that the invitee is invited to.
    \a inviteeJid is the JID of the invitee.

    Returns the result of the action.
*/
QXmppTask<QXmppMixManager::InvitationResult> QXmppMixManager::requestInvitation(const QString &channelJid, const QString &inviteeJid)
{
    QXmppMixInvitationRequestIq iq;
    iq.setType(QXmppIq::Get);
    iq.setTo(channelJid);
    iq.setInviteeJid(inviteeJid);

    co_return mapSuccess(
        parseIq<QXmppMixInvitationResponseIq>(co_await client()->sendIq(std::move(iq))),
        [](QXmppMixInvitationResponseIq &&iq) {
            return iq.invitation();
        });
}

/*!
    Requests all JIDs which are allowed to participate in the MIX channel with
    JID \a channelJid.

    The JIDs can specify users (e.g., "alice@example.org") or groups of users (e.g., "example.org")
    for allowing all users to participate that have a JID containing the specified domain.

    Returns the result of the action.
*/
QXmppTask<QXmppMixManager::JidResult> QXmppMixManager::requestAllowedJids(const QString &channelJid)
{
    return requestJids(channelJid, ns_mix_node_allowed.toString());
}

/*!
    Allows a JID to participate in a MIX channel.

    The JID can specify a user (e.g., "alice@example.org") or a group of users (e.g., "example.org")
    for allowing all users to participate that have a JID containing the specified domain.

    Allowing a JID is only needed if the channel does not allow anyone to participate.
    That is the case when QXmppMixConfigItem::Node::AllowedJids exists for the channel.
    Use requestChannelConfiguration() and QXmppMixConfigItem::nodes() to determine that.
    Call updateChannelConfiguration() and QXmppMixConfigItem::setNodes() to update it accordingly.
    In order to allow all JIDs to participate in a channel, you need to remove
    QXmppMixConfigItem::Node::AllowedJids from the channel's nodes.

    \a channelJid is the JID of the channel. \a jid is the bare JID to be
    allowed.

    Returns the result of the action.
*/
QXmppTask<QXmppClient::EmptyResult> QXmppMixManager::allowJid(const QString &channelJid, const QString &jid)
{
    return addJidToNode(channelJid, ns_mix_node_allowed.toString(), jid);
}

/*!
    \fn QXmppMixManager::jidAllowed(const QString &channelJid, const QString &jid)

    Emitted when a JID is allowed to participate in a MIX channel.

    That happens if allowJid() was successful or if another resource or user did that.

    \a channelJid is the JID of the channel. \a jid is the allowed bare JID.
*/

/*!
    \fn QXmppMixManager::allJidsAllowed(const QString &channelJid)

    Emitted when all JIDs are allowed to participate in a MIX channel.

    That happens if QXmppMixConfigItem::Node::AllowedJids is removed from a channel.

    \a channelJid is the JID of the channel.
*/

/*!
    Disallows a formerly allowed JID to participate in a MIX channel.

    Only allowed JIDs can be disallowed via this method.
    In order to disallow other JIDs, use banJid().

    \a channelJid is the JID of the channel. \a jid is the bare JID to be
    disallowed.

    Returns the result of the action.
*/
QXmppTask<QXmppClient::EmptyResult> QXmppMixManager::disallowJid(const QString &channelJid, const QString &jid)
{
    return d->pubSubManager->retractItem(channelJid, ns_mix_node_allowed.toString(), jid);
}

/*!
    \fn QXmppMixManager::jidDisallowed(const QString &channelJid, const QString &jid)

    Emitted when a fomerly allowed JID is disallowed to participate in a MIX channel anymore.

    That happens if disallowJid() was successful or if another resource or user did that.

    \a channelJid is the JID of the channel. \a jid is the disallowed bare
    JID.
*/

/*!
    Disallows all formerly allowed JIDs to participate in the MIX channel
    with JID \a channelJid.

    Only allowed JIDs can be disallowed via this method.
    In order to disallow other JIDs, use banJid().

    Returns the result of the action.
*/
QXmppTask<QXmppClient::EmptyResult> QXmppMixManager::disallowAllJids(const QString &channelJid)
{
    return d->pubSubManager->purgeItems(channelJid, ns_mix_node_allowed.toString());
}

/*!
    \fn QXmppMixManager::allJidsDisallowed(const QString &channelJid)

    Emitted when no JID is allowed to participate in a MIX channel anymore.

    That happens if disallowAllJids() was successful or if another resource or user did that.

    \a channelJid is the JID of the channel.
*/

/*!
    Requests all JIDs which are not allowed to participate in the MIX channel
    with JID \a channelJid.

    Returns the result of the action.
*/
QXmppTask<QXmppMixManager::JidResult> QXmppMixManager::requestBannedJids(const QString &channelJid)
{
    return requestJids(channelJid, ns_mix_node_banned.toString());
}

/*!
    Bans a JID from participating in a MIX channel.

    The JID can specify a user (e.g., "alice@example.org") or a group of users (e.g., "example.org")
    for banning all users that have a JID containing the specified domain.

    Before calling this, make sure that QXmppMixConfigItem::Node::BannedJids exists for the channel.
    Use requestChannelConfiguration() and QXmppMixConfigItem::nodes() to determine that.
    Call updateChannelConfiguration() and QXmppMixConfigItem::setNodes() to update it accordingly.

    \a channelJid is the JID of the channel. \a jid is the bare JID to be
    banned.

    Returns the result of the action.
*/
QXmppTask<QXmppClient::EmptyResult> QXmppMixManager::banJid(const QString &channelJid, const QString &jid)
{
    return addJidToNode(channelJid, ns_mix_node_banned.toString(), jid);
}

/*!
    \fn QXmppMixManager::jidBanned(const QString &channelJid, const QString &jid)

    Emitted when a JID is banned from participating in a MIX channel.

    That happens if banJid() was successful or if another resource or user did that.

    \a channelJid is the JID of the channel. \a jid is the banned bare JID.
*/

/*!
    Unbans a formerly banned JID from participating in a MIX channel.

    \a channelJid is the JID of the channel. \a jid is the bare JID to be
    unbanned.

    Returns the result of the action.
*/
QXmppTask<QXmppClient::EmptyResult> QXmppMixManager::unbanJid(const QString &channelJid, const QString &jid)
{
    return d->pubSubManager->retractItem(channelJid, ns_mix_node_banned.toString(), jid);
}

/*!
    \fn QXmppMixManager::jidUnbanned(const QString &channelJid, const QString &jid)

    Emitted when a formerly banned JID is unbanned from participating in a MIX channel.

    That happens if unbanJid() was successful or if another resource or user did that.

    \a channelJid is the JID of the channel. \a jid is the unbanned bare JID.
*/

/*!
    Unbans all formerly banned JIDs from participating in the MIX channel
    with JID \a channelJid.

    Returns the result of the action.
*/
QXmppTask<QXmppClient::EmptyResult> QXmppMixManager::unbanAllJids(const QString &channelJid)
{
    return d->pubSubManager->purgeItems(channelJid, ns_mix_node_banned.toString());
}

/*!
    \fn QXmppMixManager::allJidsUnbanned(const QString &channelJid)

    Emitted when all JIDs are unbanned from participating in a MIX channel.

    That happens if unbanAllJids() was successful or if another resource or user did that.
    Furthermore, that happens if QXmppMixConfigItem::Node::BannedJids is removed from a channel.

    \a channelJid is the JID of the channel.
*/

/*!
    Requests all participants of the MIX channel with JID \a channelJid.

    In the case of a channel that not everybody is allowed to participate in, the participants are a
    subset of the allowed JIDs.

    Returns the result of the action.
*/
QXmppTask<QXmppMixManager::ParticipantResult> QXmppMixManager::requestParticipants(const QString &channelJid)
{
    co_return mapSuccess(
        co_await d->pubSubManager->requestItems<QXmppMixParticipantItem>(channelJid, ns_mix_node_participants.toString())
            .withContext(this),
        [](QXmppPubSubManager::Items<QXmppMixParticipantItem> items) {
            return items.items;
        });
}

/*!
    \fn QXmppMixManager::participantReceived(const QString &channelJid, const QXmppMixParticipantItem &participant)

    Emitted when a user joined a MIX channel or a participant is updated.

    \a channelJid is the JID of the channel that the user joined or whose
    participant is updated. \a participant is the new or updated participant.
*/

/*!
    \fn QXmppMixManager::participantLeft(const QString &channelJid, const QString &participantId)

    Emitted when a participant left the MIX channel.

    \a channelJid is the JID of the channel that is left by the participant.
    \a participantId is the ID of the left participant.
*/

/*!
    Leaves the MIX channel with JID \a channelJid.

    Returns the result of the action.
*/
QXmppTask<QXmppClient::EmptyResult> QXmppMixManager::leaveChannel(const QString &channelJid)
{
    QXmppMixIq iq;
    iq.setType(QXmppIq::Set);
    iq.setTo(client()->configuration().jidBare());
    iq.setActionType(QXmppMixIq::ClientLeave);
    iq.setChannelJid(channelJid);

    return client()->sendGenericIq(std::move(iq));
}

/*!
    Deletes the MIX channel with JID \a channelJid.

    Returns the result of the action.
*/
QXmppTask<QXmppClient::EmptyResult> QXmppMixManager::deleteChannel(const QString &channelJid)
{
    QXmppMixIq iq;
    iq.setType(QXmppIq::Set);
    iq.setTo(QXmppUtils::jidToDomain(channelJid));
    iq.setActionType(QXmppMixIq::Destroy);
    iq.setChannelId(QXmppUtils::jidToUser(channelJid));

    return client()->sendGenericIq(std::move(iq));
}

/*!
    \fn QXmppMixManager::channelDeleted(const QString &channelJid)

    Emitted when a MIX channel is deleted.

    \a channelJid is the JID of the deleted channel.
*/

void QXmppMixManager::onRegistered(QXmppClient *client)
{
    d->discoveryManager = client->findExtension<QXmppDiscoveryManager>();
    Q_ASSERT_X(d->discoveryManager, "QXmppMixManager", "QXmppDiscoveryManager is missing");

    d->servicesWatch = d->discoveryManager->discoverServices(
        QXmpp::Disco::Category::Conference,
        QXmpp::Disco::Type::Mix,
        { staticString(ns_mix) });

    d->services.setBinding([this]() -> QList<Service> {
        QList<Service> result;
        for (const auto &s : d->servicesWatch->services().value()) {
            const auto &features = s.info.features();
            result.append(Service {
                s.jid,
                contains(features, ns_mix_searchable),
                contains(features, ns_mix_create_channel),
            });
        }
        return result;
    });

    d->servicesNotifier = d->services.addNotifier([this]() {
        Q_EMIT servicesChanged();
    });

    connect(client, &QXmppClient::connected, this, [this, client]() {
        if (client->streamManagementState() == QXmppClient::NewStream) {
            setParticipantSupport(Unknown);
            setMessageArchivingSupport(Unknown);
            updateSupport();
        }
    });

    d->pubSubManager = client->findExtension<QXmppPubSubManager>();
    Q_ASSERT_X(d->pubSubManager, "QXmppMixManager", "QXmppPubSubManager is missing");

    // data import/export
    if (auto manager = client->findExtension<QXmppAccountMigrationManager>()) {
        auto rosterManager = client->findExtension<QXmppRosterManager>();
        Q_ASSERT_X(rosterManager, "QXmppMixManager", "QXmppRosterManager is missing");

        auto importData = [this, client, manager](const MixData &data) -> QXmppTask<std::variant<Success, QXmppError>> {
            auto tasks = transform<std::vector<QXmppTask<JoiningResult>>>(data.items, [&](const auto &item) {
                return joinChannel(item.jid, item.nick.isEmpty() ? client->configuration().user() : item.nick);
            });
            for (auto &task : tasks) {
                auto result = co_await task;

                // We do not break import/export on mix errors, we only notify about it
                if (hasError(result)) {
                    Q_EMIT manager->errorOccurred(getError(result));
                }
            }
            co_return Success();
        };

        using ExportResult = std::variant<MixData, QXmppError>;
        auto exportData = [this, client, manager, rosterManager]() -> QXmppTask<ExportResult> {
            auto rosterResult = co_await rosterManager->requestRoster().withContext(this);

            if (hasError(rosterResult)) {
                co_return getError(std::move(rosterResult));
            }

            const auto iq = getValue(std::move(rosterResult));
            auto mixItems = iq.items();
            removeIf(mixItems, [](const auto &item) { return !item.isMixChannel(); });

            // TODO: Directly request pubsub item with id=item.mixParticipantId() instead of requesting all participants

            auto tasks = transform<std::vector<std::tuple<QXmppTask<ParticipantResult>, QXmppRosterIq::Item>>>(
                std::move(mixItems), [&](auto &&item) {
                    return std::tuple { requestParticipants(item.bareJid()), std::move(item) };
                });

            MixData result;
            result.items.reserve(tasks.size());

            for (auto &[task, item] : tasks) {
                auto participantsResult = co_await task;

                // We do not break import/export on mix errors, we only notify about it
                if (auto *error = std::get_if<QXmppError>(&participantsResult)) {
                    Q_EMIT manager->errorOccurred(*error);
                } else {
                    auto &participants = std::get<QList<QXmppMixParticipantItem>>(participantsResult);

                    if (auto participant = find(participants, item.mixParticipantId(), &QXmppMixParticipantItem::id)) {
                        result.items.push_back(MixData::Item { item.bareJid(), participant->nick() });
                    }
                }
            }
            co_return result;
        };

        manager->registerExportData<MixData>(importData, exportData);
    }
}

void QXmppMixManager::onUnregistered(QXmppClient *client)
{
    d->servicesNotifier = {};
    d->services = QList<Service>();
    d->servicesWatch = {};
    setParticipantSupport(Unknown);
    setMessageArchivingSupport(Unknown);
    disconnect(client, &QXmppClient::connected, this, nullptr);

    if (auto manager = client->findExtension<QXmppAccountMigrationManager>()) {
        manager->unregisterExportData<MixData>();
    }
}

bool QXmppMixManager::handlePubSubEvent(const QDomElement &element, const QString &pubSubService, const QString &nodeName)
{
    if (nodeName == ns_mix_node_allowed && QXmppPubSubEvent<QXmppPubSubBaseItem>::isPubSubEvent(element)) {
        QXmppPubSubEvent<QXmppPubSubBaseItem> event;
        event.parse(element);

        switch (event.eventType()) {
        // Items have been published.
        case QXmppPubSubEventBase::Items: {
            const auto items = event.items();
            for (const auto &item : items) {
                Q_EMIT jidAllowed(pubSubService, item.id());
            }
            break;
        }
        // Specific items are deleted.
        case QXmppPubSubEventBase::Retract: {
            const auto ids = event.retractIds();
            for (const auto &id : ids) {
                Q_EMIT jidDisallowed(pubSubService, id);
            }
            break;
        }
        // All items are deleted.
        case QXmppPubSubEventBase::Purge:
            Q_EMIT allJidsDisallowed(pubSubService);
            break;
        // The whole node is deleted.
        case QXmppPubSubEventBase::Delete:
            Q_EMIT allJidsAllowed(pubSubService);
            break;
        case QXmppPubSubEventBase::Configuration:
        case QXmppPubSubEventBase::Subscription:
            break;
        }

        return true;
    }

    if (nodeName == ns_mix_node_banned && QXmppPubSubEvent<QXmppPubSubBaseItem>::isPubSubEvent(element)) {
        QXmppPubSubEvent<QXmppPubSubBaseItem> event;
        event.parse(element);

        switch (event.eventType()) {
        // Items have been published.
        case QXmppPubSubEventBase::Items: {
            const auto items = event.items();
            for (const auto &item : items) {
                Q_EMIT jidBanned(pubSubService, item.id());
            }
            break;
        }
        // Specific items are deleted.
        case QXmppPubSubEventBase::Retract: {
            const auto ids = event.retractIds();
            for (const auto &id : ids) {
                Q_EMIT jidUnbanned(pubSubService, id);
            }
            break;
        }
        // All items are deleted.
        case QXmppPubSubEventBase::Purge:
        // The whole node is deleted.
        case QXmppPubSubEventBase::Delete:
            Q_EMIT allJidsUnbanned(pubSubService);
            break;
        case QXmppPubSubEventBase::Configuration:
        case QXmppPubSubEventBase::Subscription:
            break;
        }

        return true;
    }

    if (nodeName == ns_mix_node_config && QXmppPubSubEvent<QXmppMixConfigItem>::isPubSubEvent(element)) {
        QXmppPubSubEvent<QXmppMixConfigItem> event;
        event.parse(element);

        switch (event.eventType()) {
        case QXmppPubSubEventBase::Items: {
            const auto item = event.items().constFirst();
            Q_EMIT channelConfigurationUpdated(pubSubService, item);
            break;
        }
        case QXmppPubSubEventBase::Retract:
        case QXmppPubSubEventBase::Purge:
        case QXmppPubSubEventBase::Delete:
        case QXmppPubSubEventBase::Configuration:
        case QXmppPubSubEventBase::Subscription:
            break;
        }

        return true;
    }

    if (nodeName == ns_mix_node_info && QXmppPubSubEvent<QXmppMixInfoItem>::isPubSubEvent(element)) {
        QXmppPubSubEvent<QXmppMixInfoItem> event;
        event.parse(element);

        switch (event.eventType()) {
        case QXmppPubSubEventBase::Items: {
            const auto item = event.items().constFirst();
            Q_EMIT channelInformationUpdated(pubSubService, item);
            break;
        }
        case QXmppPubSubEventBase::Retract:
        case QXmppPubSubEventBase::Purge:
        case QXmppPubSubEventBase::Delete:
        case QXmppPubSubEventBase::Configuration:
        case QXmppPubSubEventBase::Subscription:
            break;
        }

        return true;
    }

    if (nodeName == ns_mix_node_participants && QXmppPubSubEvent<QXmppMixParticipantItem>::isPubSubEvent(element)) {
        QXmppPubSubEvent<QXmppMixParticipantItem> event;
        event.parse(element);

        switch (event.eventType()) {
        // Items have been published.
        case QXmppPubSubEventBase::Items: {
            const auto items = event.items();
            for (const auto &item : items) {
                Q_EMIT participantReceived(pubSubService, item);
            }
            break;
        }
        // Specific items are deleted.
        case QXmppPubSubEventBase::Retract: {
            const auto ids = event.retractIds();
            for (const auto &id : ids) {
                Q_EMIT participantLeft(pubSubService, id);
            }
            break;
        }
        // All items are deleted.
        case QXmppPubSubEventBase::Purge:
        // The whole node is deleted.
        case QXmppPubSubEventBase::Delete:
            Q_EMIT channelDeleted(pubSubService);
            break;
        case QXmppPubSubEventBase::Configuration:
        case QXmppPubSubEventBase::Subscription:
            break;
        }

        return true;
    }

    return false;
}

/*!
    Pepares an IQ stanza for joining a MIX channel.

    \a channelJid is the JID of the channel being joined. \a nickname is the
    nickname of the user which is usually required by the server (default: no
    nickname is set). \a nodes are the nodes of the channel that are
    subscribed to for receiving their updates (default: all nodes are
    subcribed to).

    Returns the prepared MIX join IQ stanza.
*/
QXmppMixIq QXmppMixManager::prepareJoinIq(const QString &channelJid, const QString &nickname, QXmppMixConfigItem::Nodes nodes)
{
    QXmppMixIq iq;
    iq.setType(QXmppIq::Set);
    iq.setTo(client()->configuration().jidBare());
    iq.setActionType(QXmppMixIq::ClientJoin);
    iq.setChannelJid(channelJid);
    iq.setNick(nickname);
    iq.setSubscriptions(nodes);

    return iq;
}

/*!
    Joins a MIX channel using the IQ stanza \a iq for joining a channel.

    Returns the result of the action.
*/
QXmppTask<QXmppMixManager::JoiningResult> QXmppMixManager::joinChannel(QXmppMixIq &&iq)
{
    co_return mapSuccess(
        parseIq<QXmppMixIq>(co_await client()->sendIq(std::move(iq))),
        [](QXmppMixIq iq) {
            return Participation { iq.participantId(), iq.nick(), iq.subscriptions() };
        });
}

/*!
    Requests all JIDs of a node belonging to a MIX.

    This is only used for nodes storing items with IDs representing JIDs.

    \a channelJid is the JID of the channel. \a node is the node to be
    queried.

    Returns the result of the action.
*/
QXmppTask<QXmppMixManager::JidResult> QXmppMixManager::requestJids(const QString &channelJid, const QString &node)
{
    co_return mapSuccess(
        co_await d->pubSubManager->requestItems(channelJid, node).withContext(this),
        [](QXmppPubSubManager::Items<QXmppPubSubBaseItem> items) {
            return transform<QList<Jid>>(items.items, [](const QXmppPubSubBaseItem &item) {
                return item.id();
            });
        });
}

/*!
    Adds a JID to a node of a MIX channel.

    This is only used for nodes storing items with IDs representing JIDs.

    \a channelJid is the JID of the channel. \a node is the node to which the
    JID is added. \a jid is the JID to be added.

    Returns the result of the action.
*/
QXmppTask<QXmppClient::EmptyResult> QXmppMixManager::addJidToNode(const QString &channelJid, const QString &node, const QString &jid)
{
    co_return mapToSuccess(co_await d->pubSubManager->publishItem(channelJid, node, QXmppPubSubBaseItem { jid }).withContext(this));
}

void QXmppMixManager::updateSupport()
{
    const auto ownJid = client()->configuration().jidBare();

    d->discoveryManager->info(ownJid).then(this, [this, ownJid](Result<QXmppDiscoInfo> &&result) {
        if (hasError(result)) {
            warning(u"Could not retrieve discovery info for %1: %2"_s.arg(ownJid, getError(result).description));
        } else {
            const auto &features = getValue(result).features();

            setParticipantSupport(contains(features, ns_mix_pam) ? Supported : Unsupported);
            setMessageArchivingSupport(contains(features, ns_mix_pam_archiving) ? Supported : Unsupported);
        }
    });
}

/*!
    Sets the server's support for MIX channel participants to
    \a participantSupport as specified in \xep{0405}{Mediated Information
    eXchange (MIX): Participant Server Requirements}.
*/
void QXmppMixManager::setParticipantSupport(Support participantSupport)
{
    if (d->participantSupport != participantSupport) {
        d->participantSupport = participantSupport;
        Q_EMIT participantSupportChanged();
    }
}

/*!
    Sets the server's support for archiving messages via \xep{0313}{Message
    Archive Management} of MIX channels the user participates in to
    \a messageArchivingSupport as specified in \xep{0405}{Mediated
    Information eXchange (MIX): Participant Server Requirements}.
*/
void QXmppMixManager::setMessageArchivingSupport(Support messageArchivingSupport)
{
    if (d->messageArchivingSupport != messageArchivingSupport) {
        d->messageArchivingSupport = messageArchivingSupport;
        Q_EMIT messageArchivingSupportChanged();
    }
}
