#include "javaruntimeservice.h"

#include "launcherpaths.h"

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

#include <algorithm>
#include <utility>

namespace {

const QString kAdoptiumApi = QStringLiteral("https://api.adoptium.net/v3");

// Список компонентов рантайма Mojang. Ссылка зашита в официальный лаунчер и
// меняется вместе с ним; сами компоненты обновляются по этому же адресу.
const QString kMojangRuntimesUrl = QStringLiteral(
    "https://piston-meta.mojang.com/v1/products/java-runtime/"
    "2ec0cc96c44e5a76b9c8b7c39df7210883d12871/all.json");

// Сборки Java выходят раз в квартал, а не раз в неделю: суток кэша достаточно,
// чтобы новая появилась в тот же день, и хватает на десяток открытий окна.
constexpr qint64 kCacheTtlSeconds = 24 * 60 * 60;

constexpr int kTransferTimeoutMs = 20000;

// Сколько последних сборок каждой мажорной версии показывать. Больше трёх
// смысла нет: старые патчи отличаются только исправлениями безопасности.
constexpr int kReleasesPerMajor = 3;

QString adoptiumOs()
{
#if defined(Q_OS_WIN)
    return QStringLiteral("windows");
#elif defined(Q_OS_MACOS)
    return QStringLiteral("mac");
#else
    return QStringLiteral("linux");
#endif
}

QString adoptiumArch()
{
    const QString arch = QSysInfo::currentCpuArchitecture();
    if (arch == QLatin1String("arm64"))
        return QStringLiteral("aarch64");
    if (arch == QLatin1String("x86_64"))
        return QStringLiteral("x64");
    if (arch == QLatin1String("i386"))
        return QStringLiteral("x86");
    if (arch == QLatin1String("arm"))
        return QStringLiteral("arm");
    return arch;
}

// Ключ платформы в манифесте Mojang.
QString mojangPlatform()
{
    const QString arch = QSysInfo::currentCpuArchitecture();
#if defined(Q_OS_WIN)
    if (arch == QLatin1String("arm64"))
        return QStringLiteral("windows-arm64");
    return arch == QLatin1String("i386") ? QStringLiteral("windows-x86")
                                         : QStringLiteral("windows-x64");
#elif defined(Q_OS_MACOS)
    return arch == QLatin1String("arm64") ? QStringLiteral("mac-os-arm64")
                                          : QStringLiteral("mac-os");
#else
    return arch == QLatin1String("i386") ? QStringLiteral("linux-i386")
                                         : QStringLiteral("linux");
#endif
}

QNetworkRequest makeRequest(const QUrl &url)
{
    QNetworkRequest request{url};
    request.setAttribute(QNetworkRequest::RedirectPolicyAttribute,
                         QNetworkRequest::NoLessSafeRedirectPolicy);
    request.setTransferTimeout(kTransferTimeoutMs);
    return request;
}

// «1.8.0_504-b01» никто не узнаёт, а «8u504» — общепринятое имя той же сборки.
QString displayVersion(const QJsonObject &versionData)
{
    const int major = versionData.value(QStringLiteral("major")).toInt();
    if (major == 8) {
        return QStringLiteral("8u%1").arg(versionData.value(QStringLiteral("security")).toInt());
    }

    QString openjdk = versionData.value(QStringLiteral("openjdk_version")).toString();
    if (openjdk.endsWith(QLatin1String("-LTS")))
        openjdk.chop(4);
    if (!openjdk.isEmpty())
        return openjdk;
    return versionData.value(QStringLiteral("semver")).toString();
}

QString archiveKind(const QString &fileName)
{
    if (fileName.endsWith(QLatin1String(".zip"), Qt::CaseInsensitive))
        return QStringLiteral("zip");
    if (fileName.endsWith(QLatin1String(".tar.gz"), Qt::CaseInsensitive))
        return QStringLiteral("tar.gz");
    return {};
}

} // namespace

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

QList<JavaRuntimeEntry> JavaRuntimeService::entries(JavaRuntimeKind kind) const
{
    QList<JavaRuntimeEntry> result;
    for (const JavaRuntimeEntry &entry : m_entries) {
        if (entry.kind == kind)
            result << entry;
    }
    return result;
}

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

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

    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 (isRefreshing())
        return;
    startRefresh();
}

void JavaRuntimeService::startRefresh()
{
    m_incoming.clear();
    m_warnings.clear();
    m_ltsVersions.clear();

    // Счётчик поднимаем на оба стартовых запроса разом: иначе ответ первого
    // успел бы обнулить его и завершить обновление раньше времени.
    m_outstanding = 2;
    emit refreshingChanged();

    requestAdoptiumReleases();
    requestMojangRuntimes();
}

void JavaRuntimeService::requestAdoptiumReleases()
{
    QNetworkReply *reply = m_network->get(
        makeRequest(QUrl(kAdoptiumApi + QStringLiteral("/info/available_releases"))));

    connect(reply, &QNetworkReply::finished, this, [this, reply] {
        reply->deleteLater();
        if (reply->error() != QNetworkReply::NoError) {
            requestFinished(tr("Не удалось получить список версий Java: %1")
                                .arg(reply->errorString()));
            return;
        }

        const QJsonObject root = QJsonDocument::fromJson(reply->readAll()).object();
        const QJsonArray lts = root.value(QStringLiteral("available_lts_releases")).toArray();
        for (const QJsonValue &value : lts)
            m_ltsVersions.insert(value.toInt());

        const QJsonArray available = root.value(QStringLiteral("available_releases")).toArray();
        QList<int> majors;
        for (const QJsonValue &value : available) {
            const int major = value.toInt();
            if (major > 0)
                majors << major;
        }
        if (majors.isEmpty()) {
            requestFinished(tr("Список версий Java не удалось разобрать"));
            return;
        }

        // Запросы добавляем до того, как отчитается этот: иначе счётчик уйдёт
        // в ноль между двумя строчками.
        m_outstanding += majors.size();
        const QString arch = adoptiumArch();
        for (const int major : std::as_const(majors))
            requestFeatureRelease(major, arch, arch == QLatin1String("aarch64"));

        requestFinished({});
    });
}

void JavaRuntimeService::requestFeatureRelease(int major,
                                               const QString &architecture,
                                               bool allowFallback)
{
    QUrl url(kAdoptiumApi + QStringLiteral("/assets/feature_releases/%1/ga").arg(major));
    QUrlQuery query;
    query.addQueryItem(QStringLiteral("architecture"), architecture);
    query.addQueryItem(QStringLiteral("heap_size"), QStringLiteral("normal"));
    query.addQueryItem(QStringLiteral("jvm_impl"), QStringLiteral("hotspot"));
    query.addQueryItem(QStringLiteral("os"), adoptiumOs());
    query.addQueryItem(QStringLiteral("page"), QStringLiteral("0"));
    query.addQueryItem(QStringLiteral("page_size"), QString::number(kReleasesPerMajor));
    query.addQueryItem(QStringLiteral("project"), QStringLiteral("jdk"));
    query.addQueryItem(QStringLiteral("sort_method"), QStringLiteral("DEFAULT"));
    query.addQueryItem(QStringLiteral("sort_order"), QStringLiteral("DESC"));
    query.addQueryItem(QStringLiteral("vendor"), QStringLiteral("eclipse"));
    url.setQuery(query);

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

        // 404 значит «под эту платформу такой сборки нет» — это не ошибка сети,
        // а нормальный ответ: Java 8 и 16 под Apple Silicon просто не собирали.
        const int status =
            reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
        QList<JavaRuntimeEntry> parsed;
        if (reply->error() == QNetworkReply::NoError)
            parsed = parseAdoptium(reply->readAll(), m_ltsVersions);

        if (parsed.isEmpty() && allowFallback && (status == 404 || status == 200)) {
            // На Apple Silicon старые сборки берём под x64: Rosetta их запускает.
            m_outstanding += 1;
            requestFeatureRelease(major, QStringLiteral("x64"), false);
            requestFinished({});
            return;
        }

        QString warning;
        if (reply->error() != QNetworkReply::NoError && status != 404) {
            warning = tr("Не удалось получить сборки Java %1: %2")
                          .arg(major)
                          .arg(reply->errorString());
        }
        m_incoming += parsed;
        requestFinished(warning);
    });
}

void JavaRuntimeService::requestMojangRuntimes()
{
    QNetworkReply *reply = m_network->get(makeRequest(QUrl(kMojangRuntimesUrl)));
    connect(reply, &QNetworkReply::finished, this, [this, reply] {
        reply->deleteLater();
        if (reply->error() != QNetworkReply::NoError) {
            requestFinished(tr("Не удалось получить рантаймы Mojang: %1")
                                .arg(reply->errorString()));
            return;
        }

        m_incoming += parseMojang(reply->readAll());
        requestFinished({});
    });
}

void JavaRuntimeService::requestFinished(const QString &warning)
{
    if (!warning.isEmpty())
        m_warnings << warning;
    if (--m_outstanding > 0)
        return;
    finishRefresh();
}

void JavaRuntimeService::finishRefresh()
{
    m_outstanding = 0;
    const QString warning = m_warnings.join(QStringLiteral("; "));
    // Пустой разбор — испорченные ответы; хороший кэш ими не затираем.
    const bool ok = !m_incoming.isEmpty();
    if (ok) {
        applyEntries(std::exchange(m_incoming, {}));
        m_fetchedAt = QDateTime::currentDateTimeUtc();
        writeCache();
        emit catalogChanged();
    }
    m_incoming.clear();
    m_warnings.clear();
    emit refreshingChanged();

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

QList<JavaRuntimeEntry> JavaRuntimeService::parseAdoptium(const QByteArray &payload,
                                                          const QSet<int> &ltsVersions)
{
    QList<JavaRuntimeEntry> result;
    const QJsonArray releases = QJsonDocument::fromJson(payload).array();

    for (const QJsonValue &releaseValue : releases) {
        const QJsonObject release = releaseValue.toObject();
        const QJsonObject versionData = release.value(QStringLiteral("version_data")).toObject();
        const int major = versionData.value(QStringLiteral("major")).toInt();
        const QString releaseName = release.value(QStringLiteral("release_name")).toString();
        if (major <= 0 || releaseName.isEmpty())
            continue;

        const QString version = displayVersion(versionData);
        const QDateTime released = QDateTime::fromString(
            release.value(QStringLiteral("timestamp")).toString(), Qt::ISODate);

        const QJsonArray binaries = release.value(QStringLiteral("binaries")).toArray();
        for (const QJsonValue &binaryValue : binaries) {
            const QJsonObject binary = binaryValue.toObject();
            const auto kind = javaKindFromKey(binary.value(QStringLiteral("image_type")).toString());
            if (!kind || *kind == JavaRuntimeKind::Mojang)
                continue;   // testimage, debugimage, sources и прочее нам не нужно

            const QJsonObject package = binary.value(QStringLiteral("package")).toObject();
            const QUrl url(package.value(QStringLiteral("link")).toString());
            const QString archive = archiveKind(package.value(QStringLiteral("name")).toString());
            // Распаковать умеем только zip и tar.gz; https — потому что ссылку
            // мы получили из сети и открываем её же.
            if (archive.isEmpty() || url.scheme() != QLatin1String("https"))
                continue;

            JavaRuntimeEntry entry;
            entry.kind = *kind;
            entry.major = major;
            entry.version = version;
            entry.released = released;
            entry.url = url;
            entry.checksum = package.value(QStringLiteral("checksum")).toString();
            entry.size = static_cast<qint64>(package.value(QStringLiteral("size")).toDouble());
            entry.archive = archive;
            entry.architecture = binary.value(QStringLiteral("architecture")).toString();
            entry.lts = ltsVersions.contains(major);
            entry.id = JavaRuntimeStore::sanitizeId(QStringLiteral("temurin-%1-%2-%3")
                                                        .arg(javaKindKey(*kind),
                                                             version,
                                                             entry.architecture));
            result << entry;
        }
    }
    return result;
}

QList<JavaRuntimeEntry> JavaRuntimeService::parseMojang(const QByteArray &payload)
{
    QList<JavaRuntimeEntry> result;
    const QJsonObject root = QJsonDocument::fromJson(payload).object();
    const QJsonObject platform = root.value(mojangPlatform()).toObject();

    for (auto it = platform.constBegin(); it != platform.constEnd(); ++it) {
        // minecraft-java-exe — это лаунчер игры, а не Java.
        if (it.key() == QLatin1String("minecraft-java-exe"))
            continue;

        const QJsonArray variants = it.value().toArray();
        if (variants.isEmpty())
            continue;   // компонента под эту платформу нет

        const QJsonObject variant = variants.first().toObject();
        const QJsonObject manifest = variant.value(QStringLiteral("manifest")).toObject();
        const QUrl url(manifest.value(QStringLiteral("url")).toString());
        if (url.scheme() != QLatin1String("https"))
            continue;

        const QJsonObject versionObject = variant.value(QStringLiteral("version")).toObject();
        const QString version = versionObject.value(QStringLiteral("name")).toString();
        if (version.isEmpty())
            continue;

        JavaRuntimeEntry entry;
        entry.kind = JavaRuntimeKind::Mojang;
        entry.component = it.key();
        entry.version = version;
        // «8u51-cacert462b08» у jre-legacy, «21.0.7» у остальных.
        entry.major = version.startsWith(QLatin1String("8u"))
                          ? 8
                          : version.section(u'.', 0, 0).toInt();
        entry.released = QDateTime::fromString(
            versionObject.value(QStringLiteral("released")).toString(), Qt::ISODate);
        entry.url = url;
        entry.checksum = manifest.value(QStringLiteral("sha1")).toString();
        entry.size = 0;   // манифест знает только свой размер, не размер рантайма
        entry.archive = QStringLiteral("mojang");
        entry.architecture = QSysInfo::currentCpuArchitecture();
        entry.lts = false;
        entry.id = JavaRuntimeStore::sanitizeId(QStringLiteral("mojang-") + it.key());

        if (entry.major > 0)
            result << entry;
    }
    return result;
}

void JavaRuntimeService::applyEntries(QList<JavaRuntimeEntry> list)
{
    std::stable_sort(list.begin(), list.end(),
                     [](const JavaRuntimeEntry &a, const JavaRuntimeEntry &b) {
                         if (a.major != b.major)
                             return a.major > b.major;
                         if (a.released != b.released)
                             return a.released > b.released;
                         return a.version > b.version;
                     });

    m_entries = std::move(list);
    m_indexById.clear();
    m_indexById.reserve(m_entries.size());
    for (int i = 0; i < m_entries.size(); ++i)
        m_indexById.insert(m_entries.at(i).id, i);
}

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

    const QJsonObject root = QJsonDocument::fromJson(file.readAll()).object();
    // Кэш платформозависим: папку лаунчера могли принести с другой машины.
    if (root.value(QStringLiteral("platform")).toString() != mojangPlatform())
        return false;

    QList<JavaRuntimeEntry> parsed;
    const QJsonArray entries = root.value(QStringLiteral("entries")).toArray();
    for (const QJsonValue &value : entries) {
        const QJsonObject object = value.toObject();
        const auto kind = javaKindFromKey(object.value(QStringLiteral("kind")).toString());
        if (!kind)
            continue;

        JavaRuntimeEntry entry;
        entry.id = object.value(QStringLiteral("id")).toString();
        entry.kind = *kind;
        entry.major = object.value(QStringLiteral("major")).toInt();
        entry.version = object.value(QStringLiteral("version")).toString();
        entry.component = object.value(QStringLiteral("component")).toString();
        entry.released = QDateTime::fromString(
            object.value(QStringLiteral("released")).toString(), Qt::ISODate);
        entry.url = QUrl(object.value(QStringLiteral("url")).toString());
        entry.checksum = object.value(QStringLiteral("checksum")).toString();
        entry.size = static_cast<qint64>(object.value(QStringLiteral("size")).toDouble());
        entry.archive = object.value(QStringLiteral("archive")).toString();
        entry.architecture = object.value(QStringLiteral("architecture")).toString();
        entry.lts = object.value(QStringLiteral("lts")).toBool();

        if (entry.isValid() && entry.url.scheme() == QLatin1String("https"))
            parsed << entry;
    }
    if (parsed.isEmpty())
        return false;

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

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

    QJsonArray entries;
    for (const JavaRuntimeEntry &entry : std::as_const(m_entries)) {
        entries << QJsonObject{{QStringLiteral("id"), entry.id},
                               {QStringLiteral("kind"), javaKindKey(entry.kind)},
                               {QStringLiteral("major"), entry.major},
                               {QStringLiteral("version"), entry.version},
                               {QStringLiteral("component"), entry.component},
                               {QStringLiteral("released"), entry.released.toString(Qt::ISODate)},
                               {QStringLiteral("url"), entry.url.toString()},
                               {QStringLiteral("checksum"), entry.checksum},
                               {QStringLiteral("size"), static_cast<double>(entry.size)},
                               {QStringLiteral("archive"), entry.archive},
                               {QStringLiteral("architecture"), entry.architecture},
                               {QStringLiteral("lts"), entry.lts}};
    }

    const QJsonObject root{{QStringLiteral("fetchedAt"), m_fetchedAt.toString(Qt::ISODate)},
                           {QStringLiteral("platform"), mojangPlatform()},
                           {QStringLiteral("entries"), entries}};

    QSaveFile file(LauncherPaths::javaCatalogFile());
    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();
}
