// SPDX-FileCopyrightText: 2010 Jeremy Lainé <jeremy.laine@m4x.org>
// SPDX-FileCopyrightText: 2019 Niels Ole Salscheider <ole@salscheider.org>
// SPDX-FileCopyrightText: 2025 Linus Jahn <lnj@kaidan.im>
//
// SPDX-License-Identifier: LGPL-2.1-or-later

#include "QXmppCallManager.h"

#include "QXmppCall.h"
#include "QXmppCallManager_p.h"
#include "QXmppCall_p.h"
#include "QXmppClient.h"
#include "QXmppConstants_p.h"
#include "QXmppDiscoveryManager.h"
#include "QXmppExternalServiceDiscoveryIq.h"
#include "QXmppIqHandling.h"
#include "QXmppJingleIq.h"
#include "QXmppRosterManager.h"
#include "QXmppTask.h"
#include "QXmppUtils.h"
#include "QXmppUtils_p.h"

#include "Algorithms.h"
#include "Async.h"
#include "GstWrapper.h"
#include "Iq.h"
#include "StringLiterals.h"
#include "XmlWriter.h"

#include <gst/gst.h>

#include <QDomElement>

using namespace QXmpp;
using namespace QXmpp::Private;

QXmppTask<ServicesResult> QXmpp::Private::requestExternalServices(QXmppClient *client, const QString &jid)
{
    QXmppExternalServiceDiscoveryIq request;
    request.setType(QXmppIq::Get);
    request.setTo(jid);

    co_return parseIq<QXmppExternalServiceDiscoveryIq>(co_await client->sendIq(std::move(request)).withContext(client), [](auto &&iq) -> ServicesResult {
        return iq.externalServices();
    });
}

QXmppTask<ServiceResult> QXmpp::Private::requestCredentials(QXmppClient *client, const QString &jid, const QString &type, const QString &host)
{
    struct ExtDiscoCredentials {
        QXmppExternalService service;

        void parse(const QDomElement &el)
        {
            auto credentialsEl = el.firstChildElement();
            service = parseOptionalChildElement<QXmppExternalService>(credentialsEl)
                          .value_or(QXmppExternalService());
        }
        void toXml(XmlWriter &w) const
        {
            w.write(Element { { u"credentials", ns_external_service_discovery }, service });
        }
    };

    QXmppExternalService s;
    s.setHost(host);
    s.setType(type);

    CompatIq iq { GetIq<ExtDiscoCredentials> {
        generateSequentialStanzaId(),
        {},
        jid,
        {},
        { std::move(s) },
    } };

    return chain<ServiceResult>(client->sendIq(std::move(iq)), client, [](QXmppClient::IqResult result) {
        return map<ServiceResult>(
            [](ExtDiscoCredentials &&c) { return c.service; },
            parseIqResponseFlat<ExtDiscoCredentials>(std::move(result)));
    });
}

QXmppTask<StunTurnResult> QXmpp::Private::requestStunTurnConfig(QXmppClient *client, QXmppLoggable *q)
{
    QXmppPromise<StunTurnResult> p;
    auto task = p.task();
    requestExternalServices(client, client->configuration().domain()).then(q, [p = std::move(p), client, q](const auto &result) mutable {
        if (auto *error = std::get_if<QXmppError>(&result)) {
            qWarning() << u"Could not fetch STUN/TURN external services from server: %1"_s.arg(error->description);
            p.finish(*error);
            return;
        }

        const auto &services = std::get<QList<QXmppExternalService>>(result);
        // STUN (all)
        auto stunServers = transformFilter<QList<StunServerConfig>>(services, [](const auto &service) -> std::optional<StunServerConfig> {
            if (service.type() == u"stun" && service.port()) {
                return StunServerConfig {
                    { QHostAddress(service.host()), quint16(service.port().value()) },
                    service.expires(),
                };
            }
            return {};
        });

        // TURN (only one; prefer with credentials)
        std::optional<TurnServerConfig> turnServer;
        bool credentialsAvailable = false;
        for (const auto &service : std::as_const(services)) {
            if (service.type() == u"turn" && service.port()) {
                turnServer = {
                    TurnServer {
                        QHostAddress(service.host()),
                        quint16(service.port().value()),
                        service.username().value_or(QString()),
                        service.password().value_or(QString()),
                    },
                    service.expires(),
                };
                credentialsAvailable = service.username() && service.password();

                // prefer TURN server with credentials
                if (credentialsAvailable) {
                    break;
                }
            }
        }

        // request TURN credentials (if not provided already)
        if (turnServer && !credentialsAvailable) {
            // request credentials for TURN server
            chain(requestCredentials(client, client->configuration().domain(), u"turn"_s, turnServer->server.host.toString()),
                  q,
                  std::move(p),
                  [q, stunServers = std::move(stunServers)](auto &&result) mutable -> StunTurnResult {
                      if (auto *error = std::get_if<QXmppError>(&result)) {
                          qWarning() << u"Could not fetch credentials for TURN server: %1"_s.arg(error->description);
                          return StunTurnConfig { std::move(stunServers), {} };
                      }

                      auto &&service = std::get<QXmppExternalService>(std::move(result));
                      if (!service.username() || !service.password() || !service.port()) {
                          qWarning() << u"Server did not return credentials for TURN server upon explicit request"_s;
                          return StunTurnConfig { std::move(stunServers), {} };
                      }

                      return StunTurnConfig {
                          std::move(stunServers),
                          TurnServerConfig {
                              TurnServer {
                                  QHostAddress(service.host()),
                                  quint16(service.port().value()),
                                  service.username().value(),
                                  service.password().value(),
                              },
                              service.expires(),
                          },
                      };
                  });
        } else {
            p.finish(StunTurnConfig { std::move(stunServers), std::move(turnServer) });
        }
    });
    return task;
}

QXmppCallManagerPrivate::QXmppCallManagerPrivate(QXmppCallManager *qq)
    : q(qq)
{
    // Initialize GStreamer
    if (!gst_is_initialized()) {
        gst_init(nullptr, nullptr);
    }

    supportsDtls = checkGstFeature("dtlsdec"_L1) && checkGstFeature("dtlsenc"_L1);
}

void QXmppCallManagerPrivate::addCall(QXmppCall *call)
{
    calls.append(call);
    QObject::connect(call, &QObject::destroyed, q, &QXmppCallManager::onCallDestroyed);
}

QList<StunServer> QXmppCallManagerPrivate::stunServers() const
{
    if (stunTurnServers) {
        return transform<QList<StunServer>>(stunTurnServers->stun, [](const auto &c) {
                   return c.server;
               }) +
            fallbackStunServers;
    }
    return fallbackStunServers;
}

std::optional<TurnServer> QXmppCallManagerPrivate::turnServer() const
{
    if (stunTurnServers && stunTurnServers->turn) {
        return stunTurnServers->turn->server;
    }
    return fallbackTurnServer;
}

/*!
    \fn void QXmppCallManager::callReceived(std::unique_ptr<QXmppCall> &call)

    This signal is emitted when an incoming \a call is received.

    You can take over ownership of the call by moving out the unique pointer. However, this is
    only possible for one slot connected to this signal, all other slots after that will receive a
    nullptr.
    \code
    std::vector<std::unique_ptr<QXmppCall>> myActiveCalls;
    connect(manager, &QXmppCallManager::callReceived, this, [&](std::unique_ptr<QXmppCall> &call) {
    // take over ownership
    myActiveCalls.push_back(std::move(call));
    // call is now nullptr
    });
    \endcode
    Note that you do not need to continue to use a unique pointer for memory management, you can
    also use QObject-parent ownership or another ownership model.

    \note If you do not take ownership of the call, the call manager will automatically decline
    the call.

    \note Incoming calls need to be accepted or rejected using QXmppCall::accept() or
    QXmppCall::hangUp().

    \since QXmpp 1.14, previously this signal had a different signature.
*/

/*!
    Constructs a QXmppCallManager object to handle incoming and outgoing
    Voice-Over-IP calls.
*/
QXmppCallManager::QXmppCallManager()
    : d(std::make_unique<QXmppCallManagerPrivate>(this))
{
}

/*! Destroys the QXmppCallManager object. */
QXmppCallManager::~QXmppCallManager() = default;

/*!
    Sets STUN servers that are used as a fallback, additionally to the ones provided by the XMPP
    server via \xep{0215}{External Service Discovery}.

    STUN is used to determine server-reflexive addresses and ports.

    \since QXmpp 1.14

    \a servers.
*/
void QXmppCallManager::setFallbackStunServers(const QList<StunServer> &servers)
{
    // Check for invalid addresses
    uint invalidServers = 0;
    for (const auto &server : servers) {
        invalidServers += server.host.isNull();
    }
    if (invalidServers) {
        warning(u"Fallback STUN servers contains %1 invalid servers with a null host address."_s
                    .arg(invalidServers));
    }
    d->fallbackStunServers = servers;
}

/*!
    Set a TURN server that is used as a fallback, if the XMPP server does not provide any TURN
    server via \xep{0215}{External Service Discovery}.

    TURN is used to relay packets in double-NAT configurations.

    \since QXmpp 1.14

    \a server.
*/
void QXmppCallManager::setFallbackTurnServer(const std::optional<TurnServer> &server)
{
    if (server) {
        if (server->host.isNull()) {
            warning(u"Fallback TURN server has invalid null host address."_s);
        }
    }
    d->fallbackTurnServer = server;
}

QStringList QXmppCallManager::discoveryFeatures() const
{
    QStringList features = {
        staticString(ns_jingle),      // XEP-0166: Jingle
        staticString(ns_jingle_rtp),  // XEP-0167: Jingle RTP Sessions
        staticString(ns_jingle_rtp_audio),
        staticString(ns_jingle_rtp_video),
        staticString(ns_jingle_ice_udp),  // XEP-0176: Jingle ICE-UDP Transport Method
    };
    if (d->supportsDtls) {
        features.append(staticString(ns_jingle_dtls));  // XEP-0320: Use of DTLS-SRTP in Jingle Sessions
    }
    return features;
}

bool QXmppCallManager::handleStanza(const QDomElement &element)
{
    return handleIqRequests<QXmppJingleIq>(element, client(), [this](auto &&iq) {
        return handleIq(std::move(iq));
    });
}

void QXmppCallManager::onRegistered(QXmppClient *client)
{
    connect(client, &QXmppClient::disconnected, this, &QXmppCallManager::onDisconnected);
    connect(client, &QXmppClient::presenceReceived, this, &QXmppCallManager::onPresenceReceived);
}

void QXmppCallManager::onUnregistered(QXmppClient *client)
{
    disconnect(client, nullptr, this, nullptr);
}

/*!
    Initiates a new outgoing call to the specified recipient.

    \a jid is the Full-JID of the recipient. \a media specifies whether to initiate with audio
    or audio and video. \a proposedSid is the session ID of the call. If empty, a new session
    ID will be generated. Must be unique.

    \since QXmpp 1.14, previously this function had a different signature.
*/
std::unique_ptr<QXmppCall> QXmppCallManager::call(const QString &jid, Media media, const QString &proposedSid)
{
    auto sid = proposedSid;

    auto errorCall = [&](QXmppError &&error) {
        warning(error.description);
        return std::unique_ptr<QXmppCall> {
            new QXmppCall(jid, sid, QXmppCall::OutgoingDirection, QXmppCall::FinishedState, std::move(error), this)
        };
    };

    if (jid.isEmpty()) {
        return errorCall({ u"Refusing to call an empty jid"_s, {} });
    }

    if (jid == client()->configuration().jid()) {
        return errorCall({ u"Refusing to call self"_s, {} });
    }

    // verify session id is unique
    if (sid.isEmpty()) {
        sid = QXmppUtils::generateStanzaUuid();
    }
    if (contains(d->calls, sid, &QXmppCall::sid)) {
        return errorCall({ u"Call with the same 'sid' already in progress."_s, {} });
    }

    if (d->dtlsRequired && !d->supportsDtls) {
        return errorCall({ u"DTLS encryption for calls is required, but not supported locally."_s, {} });
    }

    auto call = std::unique_ptr<QXmppCall>(new QXmppCall(jid, sid, QXmppCall::OutgoingDirection, this));

    auto *discoManager = client()->findExtension<QXmppDiscoveryManager>();
    Q_ASSERT_X(discoManager != nullptr, "call", "QXmppCallManager requires QXmppDiscoveryManager to be registered.");
    discoManager->info(jid).then(call.get(), [this, call = call.get(), media](auto result) {
        auto failure = [&](QString &&text) {
            warning(text);
            call->d->error = QXmppError { std::move(text), {} };
            call->d->setState(QXmppCall::FinishedState);
        };

        if (auto *error = std::get_if<QXmppError>(&result)) {
            failure(u"Error fetching service discovery features for calling %1: %2"_s
                        .arg(call->jid(), error->description));
            return;
        }

        // determine supported features of remote
        const auto &remoteFeatures = getValue(result).features();
        auto testFeature = [&](QStringView feature, QString &&text) {
            if (contains(remoteFeatures, feature)) {
                return true;
            }
            failure(std::move(text));
            return false;
        };
        if (!testFeature(ns_jingle, u"Remote does not support Jingle"_s) ||
            !testFeature(ns_jingle_rtp, u"Remote does not support Jingle RTP"_s) ||
            !testFeature(ns_jingle_rtp_audio, u"Remote does not support Jingle RTP audio"_s) ||
            !testFeature(ns_jingle_ice_udp, u"Remote does not support Jingle ICE-UDP"_s) ||
            (media == Media::AudioVideo && !testFeature(ns_jingle_rtp_video, u"Remote does not support Jingle RTP video"_s))) {
            return;
        }

        call->d->useDtls = d->supportsDtls && contains(remoteFeatures, ns_jingle_dtls);
        call->d->videoSupported = contains(remoteFeatures, ns_jingle_rtp_video);

        if (!call->d->useDtls && d->dtlsRequired) {
            failure(u"Remote does not support DTLS, but required locally."_s);
            return;
        }

        refreshStunTurnConfig().then(call, [this, call, media] {
            call->d->createStream(u"audio"_s, u"initiator"_s, u"microphone"_s);
            if (media == Media::AudioVideo) {
                call->d->createStream(u"video"_s, u"initiator"_s, u"webcam"_s);
            }

            // register call
            d->addCall(call);

            call->d->sendInvite();
        });
    });

    return call;
}

/*!
    Returns whether the call manager requires encryption using \xep{0320}{Use of DTLS-SRTP in
    Jingle Sessions} for all calls.

    \since QXmpp 1.11
*/
bool QXmppCallManager::dtlsRequired() const
{
    return d->dtlsRequired;
}

/*!
    Sets whether the call manager requires encryption using \xep{0320}{Use of DTLS-SRTP in
    Jingle Sessions} for all calls.

    \since QXmpp 1.11

    \a dtlsRequired.
*/
void QXmppCallManager::setDtlsRequired(bool dtlsRequired)
{
    d->dtlsRequired = dtlsRequired;
}

void QXmppCallManager::onCallDestroyed(QObject *object)
{
    d->calls.removeAll(static_cast<QXmppCall *>(object));
}

// Handles disconnection from server.
void QXmppCallManager::onDisconnected()
{
    for (auto *call : std::as_const(d->calls)) {
        call->d->terminate({ QXmppJingleReason::Gone, {}, {} });
    }
}

auto QXmppCallManager::handleIq(QXmppJingleIq &&iq) -> IncomingIqResult
{
    using Error = QXmppStanza::Error;

    if (iq.type() != QXmppIq::Set) {
        return Error { Error::Cancel, Error::BadRequest, u"Jingle IQ only supports type 'set'."_s };
    }

    switch (iq.action()) {
    case QXmppJingleIq::SessionInitiate: {
        // incoming new call

        // do not ack and use FailedApplication reason to not interfere with other calls with the same ID
        if (iq.sid().isEmpty() || contains(d->calls, iq.sid(), &QXmppCall::sid)) {
            return Error { Error::Cancel, Error::Conflict, u"Invalid 'sid' value."_s };
        }

        // build call
        auto call = std::unique_ptr<QXmppCall>(new QXmppCall(iq.from(), iq.sid(), QXmppCall::IncomingDirection, this));
        const auto contents = iq.contents();

        for (const auto &content : contents) {
            bool dtlsRequested = !content.transportFingerprint().isEmpty();
            call->d->useDtls = d->supportsDtls && dtlsRequested;

            if (dtlsRequested && !d->supportsDtls) {
                call->d->terminate({ QXmppJingleReason::FailedApplication, u"DTLS is not supported."_s, {} }, true);
                return {};
            }
            if (!dtlsRequested && d->dtlsRequired) {
                call->d->terminate({ QXmppJingleReason::FailedApplication, u"DTLS required."_s, {} }, true);
                return {};
            }
        }

        // register call
        d->addCall(call.get());
        debug(u"Adding new incoming call (sid=%1)"_s.arg(iq.sid()));

        // first send IQ ack (task may finish instantly)
        later(this, [this, contents, callPtr = call.release()]() mutable {
            refreshStunTurnConfig().then(this, [this, contents, callPtr]() mutable {
                auto call = std::unique_ptr<QXmppCall>(callPtr);

                for (const auto &content : contents) {
                    // create stream (with up-to-date STUN/TURN credentials)
                    auto *stream = call->d->createStream(content.descriptionMedia(), content.creator(), content.name());
                    if (!stream) {
                        call->d->terminate({ QXmppJingleReason::FailedApplication, {}, {} }, true);
                        return;
                    }

                    // check content description and transport
                    if (!call->d->handleDescription(stream, content) ||
                        !call->d->handleTransport(stream, content)) {

                        // terminate call
                        call->d->terminate({ QXmppJingleReason::FailedApplication, {}, {} }, true);
                        call->terminated();
                        return;
                    }
                }

                // send ringing indication
                auto ringing = call->d->createIq(QXmppJingleIq::SessionInfo);
                ringing.setRtpSessionState(QXmppJingleIq::RtpSessionStateRinging());
                client()->sendIq(std::move(ringing));

                // notify user
                Q_EMIT callReceived(call);

                if (call) {
                    // nobody took over the call

                    // take ownership, delete on finished
                    auto *rawCall = call.release();
                    rawCall->setParent(this);
                    connect(rawCall, &QXmppCall::finished, rawCall, &QObject::deleteLater);

                    // decline call
                    rawCall->d->terminate({ QXmppJingleReason::Decline, {}, {} });
                }
                return;
            });
        });
        return {};
    }
    default: {
        // for all other requests, require a valid call
        auto call = find(d->calls, iq.sid(), &QXmppCall::sid);
        // verify call found AND verify sender is correct
        if (!call || call.value()->jid() != iq.from()) {
            warning(u"Remote party %1 sent a request for an unknown call %2"_s.arg(iq.from(), iq.sid()));
            return Error { Error::Cancel, Error::ItemNotFound, u"Unknown call."_s };
        }
        return into<IncomingIqResult>(call.value()->d->handleRequest(std::move(iq)));
    }
    }
}

void QXmppCallManager::onPresenceReceived(const QXmppPresence &presence)
{
    if (presence.type() != QXmppPresence::Unavailable) {
        return;
    }

    if (auto call = find(std::as_const(d->calls), presence.from(), &QXmppCall::jid)) {
        // the remote party has gone away, terminate call
        auto &callObject = call.value();
        callObject->d->error = { u"Received unavailable presence"_s, {} };
        callObject->d->terminate({ QXmppJingleReason::Gone, {}, {} });
    }
}

QXmppTask<void> QXmppCallManager::refreshStunTurnConfig()
{
    if (d->refreshStunTurnConfigPromise.has_value()) {
        // attach to ongoing task
        return d->refreshStunTurnConfigPromise->generateTask();
    } else {
        if (d->stunTurnServers.has_value()) {
            // TODO: check expiry and refresh credentials
            return makeReadyTask();
        }

        // initial request of STUN/TURN credentials
        d->refreshStunTurnConfigPromise = MultiPromise<void>();
        auto task = d->refreshStunTurnConfigPromise->generateTask();

        requestStunTurnConfig(client(), this).then(this, [this](const auto &result) {
            if (auto *error = std::get_if<QXmppError>(&result)) {
                warning(u"Could not fetch STUN/TURN servers: %1"_s.arg(error->description));
                d->stunTurnServers = StunTurnConfig { {}, {} };
            } else {
                d->stunTurnServers = std::get<StunTurnConfig>(result);
            }

            auto p = std::move(d->refreshStunTurnConfigPromise.value());
            d->refreshStunTurnConfigPromise.reset();
            p.finish();
        });

        return task;
    }
}
