#include "versionmanifestservice.h"

#include "launcherpaths.h"

#include <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QSaveFile>
#include <QTimer>

#include <algorithm>
#include <utility>

namespace {

const QString kManifestUrl =
    QStringLiteral("https://launchermeta.mojang.com/mc/game/version_manifest_v2.json");

// Снапшоты выходят примерно раз в неделю, манифест весит около 250 КБ: шесть
// часов — это не больше четырёх запросов в сутки, новая версия видна в тот же
// день, а десяток открытий списка за сессию не стоит ни одного обращения к сети.
constexpr qint64 kCacheTtlSeconds = 6 * 60 * 60;

constexpr int kTransferTimeoutMs = 20000;

QList<RemoteVersionEntry> parseManifest(const QJsonObject &root)
{
    QList<RemoteVersionEntry> list;
    const QJsonArray versions = root.value(QStringLiteral("versions")).toArray();
    list.reserve(versions.size());

    for (const QJsonValue &value : versions) {
        const QJsonObject object = value.toObject();

        RemoteVersionEntry entry;
        entry.id = object.value(QStringLiteral("id")).toString();
        entry.type = object.value(QStringLiteral("type")).toString();
        entry.url = QUrl(object.value(QStringLiteral("url")).toString());
        entry.sha1 = object.value(QStringLiteral("sha1")).toString();
        entry.releaseTime = QDateTime::fromString(
            object.value(QStringLiteral("releaseTime")).toString(), Qt::ISODate);

        // Описание версии качается по этой ссылке, поэтому чужие схемы не берём.
        if (entry.id.isEmpty() || entry.url.scheme() != QLatin1String("https"))
            continue;
        list << entry;
    }

    // Mojang и так отдаёт новые сверху, но зависеть от этого не стоит.
    std::stable_sort(list.begin(), list.end(),
                     [](const RemoteVersionEntry &a, const RemoteVersionEntry &b) {
                         if (a.releaseTime != b.releaseTime)
                             return a.releaseTime > b.releaseTime;
                         return a.id < b.id;
                     });
    return list;
}

} // namespace

VersionManifestService::VersionManifestService(QObject *parent)
    : QObject(parent)
    , m_network(new QNetworkAccessManager(this))
{
}

std::optional<RemoteVersionEntry> VersionManifestService::find(const QString &id) const
{
    const auto it = m_indexById.constFind(id);
    if (it == m_indexById.constEnd())
        return std::nullopt;
    return m_versions.at(it.value());
}

void VersionManifestService::applyVersions(QList<RemoteVersionEntry> list)
{
    m_versions = std::move(list);
    m_indexById.clear();
    m_indexById.reserve(m_versions.size());
    for (int i = 0; i < m_versions.size(); ++i)
        m_indexById.insert(m_versions.at(i).id, i);
}

void VersionManifestService::ensureLoaded(Callback callback, bool forceRefresh)
{
    if (!m_cacheRead) {
        m_cacheRead = true;
        if (readCache())
            emit versionsChanged();
    }

    const bool fresh = hasData() && m_fetchedAt.isValid()
                       && m_fetchedAt.secsTo(QDateTime::currentDateTimeUtc()) < kCacheTtlSeconds;
    if (!forceRefresh && fresh) {
        // Колбэк, который то синхронный, то нет, ломает вызывающий код —
        // на попадании в кэш отвечаем через событийный цикл.
        if (callback)
            QTimer::singleShot(0, this, [callback = std::move(callback)] { callback(true, {}); });
        return;
    }

    if (callback)
        m_pending << std::move(callback);
    if (m_refreshing)
        return;
    startRefresh();
}

void VersionManifestService::startRefresh()
{
    m_refreshing = true;
    emit refreshingChanged();

    QNetworkRequest request{QUrl(kManifestUrl)};
    request.setAttribute(QNetworkRequest::RedirectPolicyAttribute,
                         QNetworkRequest::NoLessSafeRedirectPolicy);
    request.setTransferTimeout(kTransferTimeoutMs);

    QNetworkReply *reply = m_network->get(request);
    connect(reply, &QNetworkReply::finished, this, [this, reply] {
        reply->deleteLater();

        if (reply->error() != QNetworkReply::NoError) {
            finishRefresh(false,
                          tr("Не удалось обновить список версий: %1").arg(reply->errorString()));
            return;
        }

        QList<RemoteVersionEntry> parsed =
            parseManifest(QJsonDocument::fromJson(reply->readAll()).object());
        // Пустой разбор — испорченный ответ; хороший кэш им не затираем.
        if (parsed.isEmpty()) {
            finishRefresh(false, tr("Манифест версий Mojang не удалось разобрать"));
            return;
        }

        applyVersions(std::move(parsed));
        m_fetchedAt = QDateTime::currentDateTimeUtc();
        writeCache();
        emit versionsChanged();
        finishRefresh(true, {});
    });
}

void VersionManifestService::finishRefresh(bool ok, const QString &warning)
{
    m_refreshing = false;
    emit refreshingChanged();

    // Колбэк может снова позвать ensureLoaded — список забираем себе заранее.
    const QList<Callback> pending = std::exchange(m_pending, {});
    for (const Callback &callback : pending) {
        // Сеть недоступна, но старые данные есть — работаем на них.
        if (!ok && hasData())
            callback(true, warning);
        else
            callback(ok, warning);
    }
}

bool VersionManifestService::readCache()
{
    QFile file(LauncherPaths::versionManifestFile());
    if (!file.open(QIODevice::ReadOnly))
        return false;

    const QJsonObject root = QJsonDocument::fromJson(file.readAll()).object();
    QList<RemoteVersionEntry> parsed = parseManifest(root);
    if (parsed.isEmpty())
        return false;

    applyVersions(std::move(parsed));
    m_fetchedAt = QDateTime::fromString(root.value(QStringLiteral("fetchedAt")).toString(),
                                        Qt::ISODate);
    return true;
}

void VersionManifestService::writeCache()
{
    // Папку кэша могли удалить уже после запуска лаунчера.
    if (!LauncherPaths::ensureRootExists())
        return;

    QJsonArray versions;
    for (const RemoteVersionEntry &entry : std::as_const(m_versions)) {
        versions << QJsonObject{{QStringLiteral("id"), entry.id},
                                {QStringLiteral("type"), entry.type},
                                {QStringLiteral("url"), entry.url.toString()},
                                {QStringLiteral("sha1"), entry.sha1},
                                {QStringLiteral("releaseTime"),
                                 entry.releaseTime.toString(Qt::ISODate)}};
    }

    // Своя обёртка: в манифесте Mojang нет отметки времени, по которой можно
    // судить о свежести кэша.
    const QJsonObject root{{QStringLiteral("fetchedAt"), m_fetchedAt.toString(Qt::ISODate)},
                           {QStringLiteral("versions"), versions}};

    // QSaveFile: оборванная запись не оставит обрезанный кэш, который потом
    // пришлось бы отличать от настоящего.
    QSaveFile file(LauncherPaths::versionManifestFile());
    if (!file.open(QIODevice::WriteOnly))
        return;
    const QByteArray payload = QJsonDocument(root).toJson(QJsonDocument::Compact);
    if (file.write(payload) != payload.size()) {
        file.cancelWriting();
        return;
    }
    file.commit();
}
