// SPDX-FileCopyrightText: 2019 Linus Jahn <lnj@kaidan.im>
//
// SPDX-License-Identifier: LGPL-2.1-or-later

#include "QXmppUploadRequestManager.h"

#include "QXmppClient.h"
#include "QXmppConstants_p.h"
#include "QXmppDataForm.h"
#include "QXmppDiscoveryIq.h"
#include "QXmppDiscoveryManager.h"
#include "QXmppHttpUploadIq.h"
#include "QXmppUtils.h"
#include "QXmppUtils_p.h"

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

#include <QDomElement>
#include <QFileInfo>
#include <QMimeDatabase>

using namespace QXmpp;
using namespace QXmpp::Private;

class QXmppUploadServicePrivate : public QSharedData
{
public:
    QString jid;
    qint64 sizeLimit = -1;
};

QXmppUploadService::QXmppUploadService()
    : d(new QXmppUploadServicePrivate)
{
}

/*! Copy constructor */
QXmppUploadService::QXmppUploadService(const QXmppUploadService &) = default;

QXmppUploadService::~QXmppUploadService() = default;

/*! Equal operator */
QXmppUploadService &QXmppUploadService::operator=(const QXmppUploadService &) = default;

/*! Returns the JID of the HTTP File Upload service. */
QString QXmppUploadService::jid() const
{
    return d->jid;
}

/*!
    Sets the JID of the HTTP File Upload service.

    \a jid.
*/
void QXmppUploadService::setJid(const QString &jid)
{
    d->jid = jid;
}

/*!
    Returns the size limit of files that can be uploaded to this upload
    service.

    A size limit of -1 means that there is no file size limit or it could not
    be determined.
*/
qint64 QXmppUploadService::sizeLimit() const
{
    return d->sizeLimit;
}

/*!
    Sets the size limit of files that can be uploaded to this upload service.

    \a sizeLimit.
*/
void QXmppUploadService::setSizeLimit(qint64 sizeLimit)
{
    d->sizeLimit = sizeLimit;
}

class QXmppUploadRequestManagerPrivate : public QSharedData
{
public:
    QList<QXmppUploadService> uploadServices;
};

/*!
    \typedef QXmppUploadRequestManager::SlotResult

    Contains the requested upload slot from the service or a QXmppStanza::Error
    in case the request failed.

    \since QXmpp 1.5
*/

QXmppUploadRequestManager::QXmppUploadRequestManager()
    : d(std::make_unique<QXmppUploadRequestManagerPrivate>())
{
}

QXmppUploadRequestManager::~QXmppUploadRequestManager() = default;

/*!
    Requests an upload slot from the server.

    \a file is the info of the file to be uploaded.
    \a uploadService is the HTTP File Upload service that is used to request
    the upload slot. If this is empty, the first discovered one is used.

    Returns the id of the sent IQ. If sendPacket() isn't successful or no
    upload service has been discovered yet, an empty string will be returned.
*/
QString QXmppUploadRequestManager::requestUploadSlot(const QFileInfo &file,
                                                     const QString &uploadService)
{
    return requestUploadSlot(file, file.fileName(), uploadService);
}

/*!
    Requests an upload slot from the server.

    \a file is the info of the file to be uploaded.
    \a customFileName is used instead of the file's actual name for requesting the upload slot.
    \a uploadService is the HTTP File Upload service that is used to request
    the upload slot. If this is empty, the first discovered one is used.

    Returns the id of the sent IQ. If sendPacket() isn't successful or no
    upload service has been discovered yet, an empty string will be returned.
*/
QString QXmppUploadRequestManager::requestUploadSlot(const QFileInfo &file,
                                                     const QString &customFileName,
                                                     const QString &uploadService)
{
    return requestUploadSlot(customFileName, file.size(),
                             QMimeDatabase().mimeTypeForFile(file),
                             uploadService);
}

/*!
    Requests an upload slot from the server.

    \a fileName is the name of the file to be uploaded. This may be used by
    the server to generate the URL.
    \a fileSize is the size of the file to be uploaded. The server may reject
    too large files.
    \a mimeType is the content-type of the file. This can be used by the
    server to set the HTTP MIME-type of the URL.
    \a uploadService is the HTTP File Upload service that is used to request
    the upload slot. If this is empty, the first discovered one is used.

    Returns the id of the sent IQ. If sendPacket() isn't successful or no
    upload service has been discovered yet, an empty string will be returned.
*/
QString QXmppUploadRequestManager::requestUploadSlot(const QString &fileName,
                                                     qint64 fileSize,
                                                     const QMimeType &mimeType,
                                                     const QString &uploadService)
{
    if (!serviceFound() && uploadService.isEmpty()) {
        return {};
    }

    QXmppHttpUploadRequestIq iq;
    if (uploadService.isEmpty()) {
        iq.setTo(d->uploadServices.first().jid());
    } else {
        iq.setTo(uploadService);
    }
    iq.setType(QXmppIq::Get);
    iq.setFileName(fileName);
    iq.setSize(fileSize);
    iq.setContentType(mimeType);

    return client()->sendLegacyId(iq);
}

/*!
    Requests an upload slot from the server.

    \a file is the info of the file to be uploaded.
    \a uploadService is the HTTP File Upload service that is used to request
    the upload slot. If this is empty, the first discovered one is used.

    \since QXmpp 1.5
*/
auto QXmppUploadRequestManager::requestSlot(const QFileInfo &file,
                                            const QString &uploadService) -> QXmppTask<SlotResult>
{
    return requestSlot(file, file.fileName(), uploadService);
}

/*!
    Requests an upload slot from the server.

    \a file is the info of the file to be uploaded.
    \a customFileName is used instead of the file's actual name for requesting the upload slot.
    \a uploadService is the HTTP File Upload service that is used to request
    the upload slot. If this is empty, the first discovered one is used.

    \since QXmpp 1.5
*/
auto QXmppUploadRequestManager::requestSlot(const QFileInfo &file,
                                            const QString &customFileName,
                                            const QString &uploadService) -> QXmppTask<SlotResult>
{
    return requestSlot(customFileName, file.size(),
                       QMimeDatabase().mimeTypeForFile(file),
                       uploadService);
}

/*!
    Requests an upload slot from the server.

    \a fileName is the name of the file to be uploaded. This may be used by
    the server to generate the URL.
    \a fileSize is the size of the file to be uploaded. The server may reject
    too large files.
    \a mimeType is the content-type of the file. This can be used by the
    server to set the HTTP MIME-type of the URL.
    \a uploadService is the HTTP File Upload service that is used to request
    the upload slot. If this is empty, the first discovered one is used.

    \since QXmpp 1.5
*/
auto QXmppUploadRequestManager::requestSlot(const QString &fileName,
                                            qint64 fileSize,
                                            const QMimeType &mimeType,
                                            const QString &uploadService)
    -> QXmppTask<SlotResult>
{
    if (!serviceFound() && uploadService.isEmpty()) {
        co_return QXmppError { u"Couldn't request upload slot: No service found."_s, {} };
    }

    QXmppHttpUploadRequestIq iq;
    if (uploadService.isEmpty()) {
        iq.setTo(d->uploadServices.first().jid());
    } else {
        iq.setTo(uploadService);
    }
    iq.setType(QXmppIq::Get);
    iq.setFileName(fileName);
    iq.setSize(fileSize);
    iq.setContentType(mimeType);

    co_return parseIq<QXmppHttpUploadSlotIq>(co_await client()->sendIq(std::move(iq)));
}

/*! Returns true if an HTTP File Upload service has been discovered. */
bool QXmppUploadRequestManager::serviceFound() const
{
    return !d->uploadServices.isEmpty();
}

/*! Returns all discovered HTTP File Upload services. */
QList<QXmppUploadService> QXmppUploadRequestManager::uploadServices() const
{
    return d->uploadServices;
}

bool QXmppUploadRequestManager::handleStanza(const QDomElement &element)
{
    if (isIqElement<QXmppHttpUploadSlotIq>(element)) {
        QXmppHttpUploadSlotIq slot;
        slot.parse(element);

        Q_EMIT slotReceived(slot);
        return true;
    } else if (isIqElement<QXmppHttpUploadRequestIq>(element)) {
        QXmppHttpUploadRequestIq requestError;
        requestError.parse(element);

        Q_EMIT requestFailed(requestError);
        return true;
    }
    return false;
}

void QXmppUploadRequestManager::handleDiscoInfo(const QXmppDiscoveryIq &iq)
{
    if (!iq.features().contains(ns_http_upload)) {
        return;
    }

    const auto identities = iq.identities();
    for (const auto &identity : identities) {
        if (identity.category() == u"store" && identity.type() == u"file") {
            QXmppUploadService service;
            service.setJid(iq.from());

            if (auto form = iq.dataForm(ns_http_upload)) {
                if (auto maxSizeValue = form->fieldValue(u"max-file-size")) {
                    if (auto maxSize = parseInt<uint64_t>(maxSizeValue->toString())) {
                        service.setSizeLimit(*maxSize);
                    }
                }
            }

            d->uploadServices.append(service);
            Q_EMIT serviceFoundChanged();
            return;
        }
    }
}

void QXmppUploadRequestManager::onRegistered(QXmppClient *client)
{
    // connect to service discovery manager
    auto *disco = client->findExtension<QXmppDiscoveryManager>();
    if (disco) {
        // scan info of all entities for upload services
        QT_WARNING_PUSH
        QT_WARNING_DISABLE_DEPRECATED
        connect(disco, &QXmppDiscoveryManager::infoReceived,
                this, &QXmppUploadRequestManager::handleDiscoInfo);
        QT_WARNING_POP

        // on client disconnect remove all upload services
        connect(client, &QXmppClient::disconnected, this, [this]() {
            d->uploadServices.clear();
            Q_EMIT serviceFoundChanged();
        });
    }
}

void QXmppUploadRequestManager::onUnregistered(QXmppClient *client)
{
    // TODO: Proper clean up of connections (currently no issue because extensions are deleted
    // on removal)
}
