/*
 * Fooyin
 * Copyright © 2024, Luke Taylor <luket@pm.me>
 *
 * Fooyin is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Fooyin is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Fooyin.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

#include "scrobblerservice.h"
#include "scrobblerauthsession.h"
#include "settings/scrobblersettings.h"

#include <core/network/networkaccessmanager.h>
#include <core/scripting/trackqueryfilter.h>
#include <utils/fypaths.h>
#include <utils/settings/settingsmanager.h>

#include <QApplication>
#include <QClipboard>
#include <QDesktopServices>
#include <QJsonArray>
#include <QJsonObject>
#include <QJsonParseError>
#include <QLoggingCategory>
#include <QMessageBox>
#include <QNetworkReply>
#include <QPointer>
#include <QPushButton>
#include <QTcpServer>
#include <QUrlQuery>

Q_LOGGING_CATEGORY(SCROBBLER, "fy.scrobbler")

using namespace Qt::StringLiterals;

constexpr auto MinScrobbleDelay        = 5000;
constexpr auto MinScrobbleDelayOnError = 30000;

namespace {
bool canBeScrobbled(const Fooyin::Track& track)
{
    return track.isValid() && !track.isRemote() && track.hasArtists() && !track.title().isEmpty()
        && track.duration() >= 30000;
}
} // namespace

namespace Fooyin::Scrobbler {
ScrobblerService::ScrobblerService(ServiceDetails details, NetworkAccessManager* network, SettingsManager* settings,
                                   QObject* parent)
    : QObject{parent}
    , m_network{network}
    , m_settings{settings}
    , m_details{std::move(details)}
    , m_authSession{nullptr}
    , m_cache{nullptr}
    , m_submitError{false}
    , m_timestamp{0}
    , m_scrobbled{false}
    , m_submitted{false}
{ }

ScrobblerService::~ScrobblerService()
{
    deleteAll();
}

bool ScrobblerService::isEnabled() const
{
    return m_details.isEnabled;
}

QString ScrobblerService::name() const
{
    return m_details.name;
}

QUrl ScrobblerService::url() const
{
    return m_details.url;
}

QUrl ScrobblerService::authUrl() const
{
    return {};
}

QString ScrobblerService::username() const
{
    return {};
}

bool ScrobblerService::requiresAuthentication() const
{
    return false;
}

bool ScrobblerService::isAuthenticated() const
{
    return true;
}

bool ScrobblerService::isCustom() const
{
    return m_details.isCustom();
}

ServiceDetails ScrobblerService::details() const
{
    return m_details;
}

void ScrobblerService::updateDetails(const ServiceDetails& details)
{
    const bool wasActive = isEnabled() && isAuthenticated();

    if(isCustom() && details.name != m_details.name) {
        deleteSession();
    }
    m_details = details;

    const bool isActive = isEnabled() && isAuthenticated();
    if(!isActive) {
        if(m_submitTimer.isActive()) {
            m_submitTimer.stop();
        }
        if(!m_replies.empty() || m_authSession || m_submitted) {
            deleteAll();
            setSubmitted(false);
        }
    }
    else if(!wasActive) {
        doDelayedSubmit();
    }
}

void ScrobblerService::initialise()
{
    if(!m_cache) {
        m_cache = new ScrobblerCache(Utils::cachePath() + "/"_L1 + name().toLower() + ".cache"_L1, m_settings, this);
    }
}

void ScrobblerService::authenticate()
{
    if(!m_authSession) {
        m_authSession = new ScrobblerAuthSession(this);
        QObject::connect(m_authSession, &ScrobblerAuthSession::tokenReceived, this,
                         [this](const QString& token) { requestAuth(token); });
    }

    QUrlQuery query;
    setupAuthQuery(m_authSession, query);

    QUrl url{authUrl()};
    url.setQuery(query);

    const QString messageTitle = tr("Authentication");
    const QString messageSubtitle
        = tr("Open url in web browser?") + u"<br /><br /><a href=\"%1\">%1</a><br />"_s.arg(url.toString());

    const QPointer<QMessageBox> message
        = new QMessageBox(QMessageBox::Information, messageTitle, messageSubtitle, QMessageBox::Cancel);
    message->setAttribute(Qt::WA_DeleteOnClose);
    message->setTextFormat(Qt::RichText);

    auto* openButton = new QPushButton(tr("Open"), message);
    message->addButton(openButton, QMessageBox::AcceptRole);
    auto* copyButton = new QPushButton(tr("Copy URL"), message);
    message->addButton(copyButton, QMessageBox::ActionRole);

    QObject::connect(openButton, &QPushButton::clicked, this, [url]() { QDesktopServices::openUrl(url); });
    QObject::connect(copyButton, &QPushButton::clicked, this,
                     [url]() { QApplication::clipboard()->setText(url.toString()); });

    QObject::connect(this, &ScrobblerService::authenticationFinished, this, [this, message](const bool success) {
        if(success) {
            if(message) {
                message->deleteLater();
            }
            cleanupAuth();
        }
    });
    QObject::connect(message, &QMessageBox::finished, this, [this, url](const int result) {
        switch(result) {
            case(QMessageBox::Cancel):
                cleanupAuth();
                Q_EMIT authenticationFinished(false);
            default:
                break;
        }
    });

    message->show();
}

void ScrobblerService::saveSession() { }

void ScrobblerService::loadSession() { }

void ScrobblerService::deleteSession() { }

void ScrobblerService::logout() { }

void ScrobblerService::saveCache()
{
    if(m_cache) {
        m_cache->writeCache();
    }
}

void ScrobblerService::resumePendingSubmissions()
{
    if(!m_cache) {
        return;
    }

    const int pendingCount = m_cache->count();
    if(pendingCount == 0) {
        return;
    }

    if(!isEnabled()) {
        qCDebug(SCROBBLER) << "Pending scrobbles will not resume for disabled service" << name() << "count"
                           << pendingCount;
        return;
    }

    if(!isAuthenticated()) {
        qCDebug(SCROBBLER) << "Pending scrobbles will not resume until authentication succeeds for" << name() << "count"
                           << pendingCount;
        return;
    }

    qCDebug(SCROBBLER) << "Resuming pending scrobbles for" << name() << "count" << pendingCount;
    doDelayedSubmit();
}

void ScrobblerService::restartScrobbleSession(const Track& track)
{
    m_currentTrack = track;
    m_timestamp    = static_cast<uint64_t>(QDateTime::currentSecsSinceEpoch());
    m_scrobbled    = false;
}

void ScrobblerService::updateNowPlaying(const Track& track)
{
    restartScrobbleSession(track);

    if(!shouldUpdateNowPlaying(track)) {
        return;
    }

    updateNowPlaying();
}

void ScrobblerService::refreshNowPlaying()
{
    if(!shouldUpdateNowPlaying(m_currentTrack)) {
        return;
    }

    updateNowPlaying();
}

void ScrobblerService::scrobble(const Track& track)
{
    if(!settings()->value<Settings::Scrobbler::ScrobblingEnabled>()) {
        return;
    }

    if(!canBeScrobbled(track)) {
        return;
    }

    if(m_scrobbled) {
        return;
    }

    if(track.id() != m_currentTrack.id() || track.uniqueFilepath() != m_currentTrack.uniqueFilepath()) {
        return;
    }

    if(m_settings->value<Settings::Scrobbler::EnableScrobbleFilter>()) {
        if(!allowedByFilter(track)) {
            return;
        }
    }

    setScrobbled(true);
    cache()->add(track, m_timestamp);

    if(!isAuthenticated()) {
        return;
    }

    doDelayedSubmit(true);
}

QString ScrobblerService::tokenSetting() const
{
    return {};
}

QUrl ScrobblerService::tokenUrl() const
{
    return {};
}

void ScrobblerService::setupAuthQuery(ScrobblerAuthSession* /*session*/, QUrlQuery& /*query*/) { }

void ScrobblerService::requestAuth(const QString& /*token*/) { }

void ScrobblerService::authFinished(QNetworkReply* /*reply*/) { }

Track ScrobblerService::currentTrack() const
{
    return m_currentTrack;
}

NetworkAccessManager* ScrobblerService::network() const
{
    return m_network;
}

ScrobblerAuthSession* ScrobblerService::authSession() const
{
    return m_authSession;
}

ScrobblerCache* ScrobblerService::cache() const
{
    return m_cache;
}

SettingsManager* ScrobblerService::settings() const
{
    return m_settings;
}

ServiceDetails& ScrobblerService::detailsRef()
{
    return m_details;
}

ScriptParser* ScrobblerService::scriptParser()
{
    return &m_scriptParser;
}

QNetworkReply* ScrobblerService::addReply(QNetworkReply* reply)
{
    return m_replies.emplace_back(reply);
}

bool ScrobblerService::removeReply(QNetworkReply* reply)
{
    if(std::erase(m_replies, reply) == 0) {
        return false;
    }

    QObject::disconnect(reply, nullptr, this, nullptr);
    reply->deleteLater();
    return true;
}

bool ScrobblerService::shouldUpdateNowPlaying(const Track& track)
{
    if(!settings()->value<Settings::Scrobbler::ScrobblingEnabled>()) {
        return false;
    }

    if(!isAuthenticated() || !canBeScrobbled(track)) {
        return false;
    }

    if(m_settings->value<Settings::Scrobbler::EnableScrobbleFilter>() && !allowedByFilter(track)) {
        return false;
    }

    return true;
}

bool ScrobblerService::allowedByFilter(const Track& track)
{
    const QString query = m_settings->value<Settings::Scrobbler::ScrobbleFilter>();
    TrackQueryFilter filter;
    return filter.filter(query, {track}).empty();
}

bool ScrobblerService::extractJsonObj(const QByteArray& data, QJsonObject* obj, QString* errorDesc)
{
    QJsonParseError error;
    const QJsonDocument json = QJsonDocument::fromJson(data, &error);

    if(errorDesc && error.error != QJsonParseError::NoError) {
        *errorDesc = error.errorString();
        return false;
    }

    if(json.isObject()) {
        *obj = json.object();
    }

    return true;
}

void ScrobblerService::handleTestError(const char* error)
{
    qCWarning(SCROBBLER) << error;
    Q_EMIT testApiFinished(false, QString::fromUtf8(error));
}

void ScrobblerService::handleAuthError(const char* error)
{
    qCWarning(SCROBBLER) << error;
    Q_EMIT authenticationFinished(false, QString::fromUtf8(error));
    cleanupAuth();
}

void ScrobblerService::cleanupAuth()
{
    if(m_authSession) {
        QObject::disconnect(m_authSession, nullptr, this, nullptr);
        m_authSession->deleteLater();
        m_authSession = nullptr;
    }
}

void ScrobblerService::deleteAll()
{
    for(auto* reply : m_replies) {
        QObject::disconnect(reply, nullptr, this, nullptr);
        reply->abort();
        reply->deleteLater();
    }

    m_replies.clear();
    cleanupAuth();
}

void ScrobblerService::doDelayedSubmit(bool initial)
{
    const int pendingCount = m_cache ? m_cache->count() : 0;
    if(m_submitted) {
        if(pendingCount > 0) {
            qCDebug(SCROBBLER) << "Scrobble submit already in progress for" << name() << "pending count"
                               << pendingCount;
        }
        return;
    }

    if(pendingCount == 0) {
        return;
    }

    if(!isEnabled()) {
        return;
    }

    if(!isAuthenticated()) {
        qCDebug(SCROBBLER) << "Pending scrobbles will not submit until authentication succeeds for" << name() << "count"
                           << pendingCount;
        return;
    }

    const int scrobbleDelay = m_settings->value<Settings::Scrobbler::ScrobblingDelay>();

    if(initial && !m_submitError && scrobbleDelay <= 0) {
        if(m_submitTimer.isActive()) {
            m_submitTimer.stop();
        }
        qCDebug(SCROBBLER) << "Submitting pending scrobbles immediately for" << name() << "count" << pendingCount;
        submit();
    }
    else if(!m_submitTimer.isActive()) {
        const int delay = std::max(scrobbleDelay, m_submitError ? MinScrobbleDelayOnError : MinScrobbleDelay);
        qCDebug(SCROBBLER) << "Scheduling scrobble submit for" << name() << "count" << pendingCount << "delayMs"
                           << delay << "afterError" << m_submitError;
        m_submitTimer.start(delay, this);
    }
}

void ScrobblerService::setSubmitted(bool submitted)
{
    m_submitted = submitted;
}

void ScrobblerService::setSubmitError(bool error)
{
    m_submitError = error;
}

void ScrobblerService::setScrobbled(bool scrobbled)
{
    m_scrobbled = scrobbled;
}

void ScrobblerService::timerEvent(QTimerEvent* event)
{
    if(event->timerId() == m_submitTimer.timerId()) {
        m_submitTimer.stop();

        if(!isEnabled() || !isAuthenticated()) {
            return;
        }

        submit();
    }
    QObject::timerEvent(event);
}
} // namespace Fooyin::Scrobbler
