#include "musicbrainz/web/musicbrainzrecordingstask.h"

#include <QXmlStreamReader>

#include "defs_urls.h"
#include "moc_musicbrainzrecordingstask.cpp"
#include "musicbrainz/musicbrainzxml.h"
#include "network/httpstatuscode.h"
#include "util/assert.h"
#include "util/logger.h"
#include "util/thread_affinity.h"
#include "util/versionstore.h"

namespace mixxx {

namespace {

const Logger kLogger("MusicBrainzRecordingsTask");

const QUrl kBaseUrl = QStringLiteral("https://musicbrainz.org/");

const QString kRequestPath = QStringLiteral("/ws/2/recording/");

const QByteArray kUserAgentRawHeaderKey = "User-Agent";

// MusicBrainz allows only a single request per second on average
// to avoid rate limiting.
// See: <https://musicbrainz.org/doc/MusicBrainz_API/Rate_Limiting>
constexpr Duration kMinDurationBetweenRequests = Duration::fromMillis(1000);

QString userAgentRawHeaderValue() {
    return VersionStore::applicationName() +
            QStringLiteral("/") +
            VersionStore::version() +
            QStringLiteral(" ( ") +
            // QStringLiteral(MIXXX_WEBSITE_URL) fails to compile on Fedora 36 with GCC 12.0.x
            MIXXX_WEBSITE_URL +
            QStringLiteral(" )");
}

QUrlQuery createUrlQuery() {
    typedef QPair<QString, QString> Param;
    QList<Param> params;
    params << Param("inc", "artists+artist-credits+releases+release-groups+media");

    QUrlQuery query;
    query.setQueryItems(params);
    return query;
}

QNetworkRequest createNetworkRequest(
        const QUuid& recordingId) {
    DEBUG_ASSERT(kBaseUrl.isValid());
    DEBUG_ASSERT(!recordingId.isNull());
    QUrl url = kBaseUrl;
    url.setPath(kRequestPath + recordingId.toString(QUuid::WithoutBraces));
    url.setQuery(createUrlQuery());
    DEBUG_ASSERT(url.isValid());
    QNetworkRequest networkRequest(url);
    // https://musicbrainz.org/doc/XML_Web_Service/Rate_Limiting#Provide_meaningful_User-Agent_strings
    // HTTP request headers must be latin1.
    networkRequest.setRawHeader(
            kUserAgentRawHeaderKey,
            userAgentRawHeaderValue().toLatin1());
    return networkRequest;
}

} // anonymous namespace

MusicBrainzRecordingsTask::MusicBrainzRecordingsTask(
        QNetworkAccessManager* networkAccessManager,
        const QList<QUuid>& recordingIds,
        QObject* parent)
        : network::WebTask(
                  networkAccessManager,
                  parent),
          m_queuedRecordingIds(recordingIds),
          m_parentTimeoutMillis(0) {
    musicbrainz::registerMetaTypesOnce();
}

QNetworkReply* MusicBrainzRecordingsTask::doStartNetworkRequest(
        QNetworkAccessManager* networkAccessManager,
        int parentTimeoutMillis) {
    DEBUG_ASSERT_QOBJECT_THREAD_AFFINITY(this);
    DEBUG_ASSERT(networkAccessManager);

    m_parentTimeoutMillis = parentTimeoutMillis;

    VERIFY_OR_DEBUG_ASSERT(!m_queuedRecordingIds.isEmpty()) {
        kLogger.warning()
                << "Nothing to do";
        return nullptr;
    }
    const auto recordingId = m_queuedRecordingIds.takeFirst();
    DEBUG_ASSERT(!recordingId.isNull());

    const QNetworkRequest networkRequest =
            createNetworkRequest(recordingId);

    if (kLogger.traceEnabled()) {
        kLogger.trace()
                << "GET"
                << networkRequest.url();
    }
    m_lastRequestSentAt.start();
    return networkAccessManager->get(networkRequest);
}

void MusicBrainzRecordingsTask::doNetworkReplyFinished(
        QNetworkReply* pFinishedNetworkReply,
        network::HttpStatusCode statusCode) {
    DEBUG_ASSERT_QOBJECT_THREAD_AFFINITY(this);

    const QByteArray body = pFinishedNetworkReply->readAll();
    QXmlStreamReader reader(body);

    // HTTP status of successful results:
    // 200: Found
    // 301: Found, but UUID moved permanently in database
    // 404: Not found in database, i.e. empty result
    if (statusCode != 200 &&
            statusCode != 301 &&
            statusCode != 404) {
        kLogger.info()
                << "GET reply"
                << "statusCode:" << statusCode
                << "body:" << body;
        auto error = musicbrainz::Error(reader);
        if (error.code) {
            emitFailed(
                    network::WebResponse(
                            pFinishedNetworkReply->url(),
                            pFinishedNetworkReply->request().url(),
                            statusCode),
                    error.code,
                    error.message);
            return;
        }
        WebTask::onNetworkError(pFinishedNetworkReply, statusCode);
        return;
    }

    const auto [trackReleases, success] = musicbrainz::parseRecordings(reader);
    for (auto&& trackRelease : trackReleases) {
        // In case of a response with status 301 (Moved Permanently)
        // the actual recording id might differ from the requested id.
        // To avoid requesting recording ids twice we need to remember
        // all recording ids.
        m_finishedRecordingIds.insert(trackRelease.recordingId);
        m_trackReleases.insert(trackRelease.trackReleaseId, trackRelease);
    }

    if (m_queuedRecordingIds.isEmpty()) {
        if (!success && m_trackReleases.isEmpty()) {
            // this error is only fatal if we have no tracks at all
            kLogger.warning()
                    << "Failed to parse XML response";
            emitFailed(
                    network::WebResponse(
                            pFinishedNetworkReply->url(),
                            pFinishedNetworkReply->request().url(),
                            statusCode),
                    -1,
                    QStringLiteral("Failed to parse XML response"));
            return;
        }

        // Finished all recording ids
        m_finishedRecordingIds.clear();
        auto trackReleases = m_trackReleases.values();
        m_trackReleases.clear();
        emitSucceeded(trackReleases);
        return;
    }

    // Continue with next recording id
    DEBUG_ASSERT(!m_queuedRecordingIds.isEmpty());

    // Ensure that at least kMinDurationBetweenRequests has passed
    // since the last request before starting the next request.
    // This is achieved by adjusting the start delay adaptively.
    const Duration elapsedSinceLastRequestSent = m_lastRequestSentAt.elapsed();
    const Duration delayBeforeNextRequest =
            kMinDurationBetweenRequests -
            std::min(kMinDurationBetweenRequests, elapsedSinceLastRequestSent);
    emit currentRecordingFetchedFromMusicBrainz();
    slotStart(m_parentTimeoutMillis, delayBeforeNextRequest.toIntegerMillis());
}

void MusicBrainzRecordingsTask::onNetworkError(
        QNetworkReply* pFinishedNetworkReply,
        network::HttpStatusCode statusCode) {
    doNetworkReplyFinished(pFinishedNetworkReply, statusCode);
}

void MusicBrainzRecordingsTask::emitSucceeded(
        const QList<musicbrainz::TrackRelease>& trackReleases) {
    VERIFY_OR_DEBUG_ASSERT(
            isSignalFuncConnected(&MusicBrainzRecordingsTask::succeeded)) {
        kLogger.warning()
                << "Unhandled succeeded signal";
        deleteLater();
        return;
    }
    emit succeeded(trackReleases);
}

void MusicBrainzRecordingsTask::emitFailed(
        const network::WebResponse& response,
        int errorCode,
        const QString& errorMessage) {
    VERIFY_OR_DEBUG_ASSERT(
            isSignalFuncConnected(&MusicBrainzRecordingsTask::failed)) {
        kLogger.warning()
                << "Unhandled failed signal"
                << response
                << errorCode
                << errorMessage;
        deleteLater();
        return;
    }
    emit failed(
            response,
            errorCode,
            errorMessage);
}

} // namespace mixxx
