#include "modloaderinstaller.h"

#include "javalocator.h"
#include "launcherpaths.h"
#include "minecraftversion.h"
#include "modloaderversionservice.h"
#include "versioninstaller.h"

#include <QDir>
#include <QFileInfo>
#include <QFile>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QProcess>
#include <QSaveFile>
#include <QTimer>

namespace {

constexpr int kTransferTimeoutMs = 30000;
// Установщик Forge после распаковки ещё патчит клиентский jar: на медленной
// машине это несколько минут. Обрываем только совсем зависший процесс.
constexpr int kInstallerTimeoutMs = 15 * 60 * 1000;
constexpr int kLogTailLines = 12;

const QString kFabricProfile = QStringLiteral("https://meta.fabricmc.net/v2/versions/loader/");
const QString kQuiltProfile = QStringLiteral("https://meta.quiltmc.org/v3/versions/loader/");

// Тот же запрет, что у VersionInstaller: id версии становится именем каталога.
bool isSafeSegment(const QString &value)
{
    if (value.isEmpty() || value.contains(u'/') || value.contains(u'\\') || value.contains(u':'))
        return false;
    return value != QLatin1String(".") && value != QLatin1String("..");
}

QString versionJsonPath(const QString &gameDir, const QString &id)
{
    return gameDir + QStringLiteral("/versions/") + id + u'/' + id + QStringLiteral(".json");
}

} // namespace

ModLoaderInstaller::ModLoaderInstaller(ModLoaderVersionService *meta,
                                       VersionInstaller *versionInstaller,
                                       QObject *parent)
    : QObject(parent)
    , m_meta(meta)
    , m_versionInstaller(versionInstaller)
    , m_network(new QNetworkAccessManager(this))
{
    // Последний шаг обоих путей одинаков: VersionInstaller дотягивает всё, чего
    // не хватает получившемуся профилю.
    connect(m_versionInstaller, &VersionInstaller::finished, this, [this](const QString &id) {
        if (m_awaitingVersionId.isEmpty() || id != m_awaitingVersionId)
            return;
        const QString versionId = m_awaitingVersionId;
        const QString loader = loaderKey(m_loader);
        const QString game = m_gameVersion;
        const QString version = m_loaderVersion;
        reset();
        emit finished(loader, game, version, versionId);
    });
    connect(m_versionInstaller, &VersionInstaller::failed, this,
            [this](const QString &id, const QString &message) {
                if (m_awaitingVersionId.isEmpty() || id != m_awaitingVersionId)
                    return;
                fail(message);
            });
    connect(m_versionInstaller, &VersionInstaller::canceled, this, [this](const QString &id) {
        if (m_awaitingVersionId.isEmpty() || id != m_awaitingVersionId)
            return;
        const QString label = m_label;
        reset();
        emit canceled(label);
    });
}

ModLoaderInstaller::~ModLoaderInstaller()
{
    if (m_process && m_process->state() != QProcess::NotRunning) {
        m_process->kill();
        m_process->waitForFinished(2000);
    }
}

double ModLoaderInstaller::fraction() const
{
    if (m_bytesTotal <= 0)
        return -1.0;   // панель нарисует «…»
    return double(m_bytesDone) / double(m_bytesTotal);
}

void ModLoaderInstaller::install(const QString &gameDir,
                                 ModLoader loader,
                                 const QString &gameVersion,
                                 const QString &loaderVersion,
                                 const QString &javaPreference)
{
    if (m_running) {
        emit failed(loaderTitle(loader), tr("Установка модлоадера уже идёт"));
        return;
    }
    if (gameVersion.trimmed().isEmpty() || loaderVersion.trimmed().isEmpty()) {
        emit failed(loaderTitle(loader), tr("Не выбрана версия %1").arg(loaderTitle(loader)));
        return;
    }

    m_gameDir = gameDir;
    m_loader = loader;
    m_gameVersion = gameVersion.trimmed();
    m_loaderVersion = loaderVersion.trimmed();
    m_javaPreference = javaPreference;
    m_label = QStringLiteral("%1 %2").arg(loaderTitle(loader), m_loaderVersion);
    m_installerLog.clear();
    m_bytesDone = 0;
    m_bytesTotal = 0;
    m_currentFile.clear();
    m_canceling = false;
    m_running = true;

    emit started(m_label);
    setStage(tr("Подготовка %1").arg(loaderTitle(loader)));

    // Список версий нужен и для ссылки на installer.jar, и для точного id
    // профиля. После перезапуска лаунчера он ещё не прочитан с диска, поэтому
    // спрашиваем его до всего остального: попадание в кэш обойдётся без сети.
    m_meta->ensureLoaded(loader, m_gameVersion, [this, loader](bool ok, const QString &warning) {
        if (!m_running || m_canceling)
            return;
        if (!ok) {
            fail(warning.isEmpty() ? tr("Список версий %1 недоступен").arg(loaderTitle(loader))
                                   : warning);
            return;
        }
        if (loader == ModLoader::Fabric || loader == ModLoader::Quilt)
            beginFabricLike();
        else
            beginInstallerJar();
    });
}

void ModLoaderInstaller::cancel()
{
    if (!m_running)
        return;
    m_canceling = true;

    if (m_reply) {
        m_reply->abort();
        m_reply = nullptr;
    }
    if (m_process && m_process->state() != QProcess::NotRunning)
        m_process->kill();
    if (!m_awaitingVersionId.isEmpty()) {
        m_versionInstaller->cancel();
        return;   // финал придёт из VersionInstaller::canceled
    }

    const QString label = m_label;
    reset();
    emit canceled(label);
}

// ── Fabric и Quilt ─────────────────────────────────────────────────────────

void ModLoaderInstaller::beginFabricLike()
{
    const auto entry = m_meta->find(m_loader, m_gameVersion, m_loaderVersion);
    const QString expectedId =
        entry ? entry->versionId
              : (m_loader == ModLoader::Fabric ? QStringLiteral("fabric-loader-")
                                               : QStringLiteral("quilt-loader-"))
                    + m_loaderVersion + u'-' + m_gameVersion;

    if (!isSafeSegment(expectedId)) {
        fail(tr("Недопустимый идентификатор версии «%1»").arg(expectedId));
        return;
    }

    const QString base = m_loader == ModLoader::Fabric ? kFabricProfile : kQuiltProfile;
    const QUrl url(base + m_gameVersion + u'/' + m_loaderVersion + QStringLiteral("/profile/json"));

    setStage(tr("Описание версии %1").arg(loaderTitle(m_loader)));
    m_currentFile = expectedId + QStringLiteral(".json");
    emit progressChanged();

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

    m_reply = m_network->get(request);
    connect(m_reply, &QNetworkReply::finished, this, [this, expectedId] {
        QNetworkReply *reply = m_reply;
        m_reply = nullptr;
        reply->deleteLater();
        if (!m_running || m_canceling)
            return;

        if (reply->error() != QNetworkReply::NoError) {
            fail(tr("Не удалось получить описание %1: %2")
                     .arg(loaderTitle(m_loader), reply->errorString()));
            return;
        }

        const QByteArray payload = reply->readAll();
        const QJsonObject root = QJsonDocument::fromJson(payload).object();
        const QString id = root.value(QStringLiteral("id")).toString();
        const QString inherits = root.value(QStringLiteral("inheritsFrom")).toString();

        // Описание приходит из сети и превращается в путь на диске — проверяем
        // и id, и то, что версия действительно наследует выбранную ванильную.
        if (id.isEmpty() || !isSafeSegment(id)) {
            fail(tr("Описание %1 испорчено: недопустимый идентификатор версии")
                     .arg(loaderTitle(m_loader)));
            return;
        }
        if (inherits != m_gameVersion) {
            fail(tr("Описание %1 рассчитано на Minecraft %2, а не %3")
                     .arg(loaderTitle(m_loader), inherits, m_gameVersion));
            return;
        }
        if (id != expectedId)
            emit log(tr("Идентификатор версии: %1").arg(id));

        const QString directory = m_gameDir + QStringLiteral("/versions/") + id;
        if (!QDir().mkpath(directory)) {
            fail(tr("Не удалось создать %1").arg(QDir::toNativeSeparators(directory)));
            return;
        }

        QSaveFile file(versionJsonPath(m_gameDir, id));
        if (!file.open(QIODevice::WriteOnly) || file.write(payload) != payload.size()
            || !file.commit()) {
            file.cancelWriting();
            fail(tr("Не удалось сохранить описание версии «%1»").arg(id));
            return;
        }

        handOffToVersionInstaller(id);
    });
}

// ── Forge и NeoForge ───────────────────────────────────────────────────────

void ModLoaderInstaller::beginInstallerJar()
{
    const auto entry = m_meta->find(m_loader, m_gameVersion, m_loaderVersion);
    if (!entry || !entry->installerUrl.isValid()) {
        fail(tr("%1 %2 не подходит к Minecraft %3 — выберите версию из списка")
                 .arg(loaderTitle(m_loader), m_loaderVersion, m_gameVersion));
        return;
    }
    if (entry->installerUrl.scheme() != QLatin1String("https")) {
        fail(tr("Установщик %1 доступен только по недопустимой ссылке").arg(loaderTitle(m_loader)));
        return;
    }

    const QUrl installerUrl = entry->installerUrl;
    // Установщик патчит versions/<mc>/<mc>.jar — без ванильной версии он не отработает.
    ensureBaseVersion([this, installerUrl] {
        downloadInstaller(installerUrl, [this](const QString &path) { runInstaller(path); });
    });
}

void ModLoaderInstaller::ensureBaseVersion(std::function<void()> next)
{
    if (QFile::exists(versionJsonPath(m_gameDir, m_gameVersion))
        && VersionLoader::installedVersions(m_gameDir).contains(m_gameVersion)) {
        next();
        return;
    }

    setStage(tr("Установка Minecraft %1").arg(m_gameVersion));
    m_currentFile = m_gameVersion;
    emit progressChanged();

    const QString base = m_gameVersion;
    auto guard = std::make_shared<QMetaObject::Connection>();
    auto failGuard = std::make_shared<QMetaObject::Connection>();

    *guard = connect(m_versionInstaller, &VersionInstaller::finished, this,
                     [this, base, guard, failGuard, next](const QString &id) {
                         if (id != base)
                             return;
                         disconnect(*guard);
                         disconnect(*failGuard);
                         if (!m_running || m_canceling)
                             return;
                         next();
                     });
    *failGuard = connect(m_versionInstaller, &VersionInstaller::failed, this,
                         [this, base, guard, failGuard](const QString &id, const QString &message) {
                             if (id != base)
                                 return;
                             disconnect(*guard);
                             disconnect(*failGuard);
                             fail(message);
                         });

    m_versionInstaller->install(m_gameDir, base);
}

void ModLoaderInstaller::downloadInstaller(const QUrl &url, std::function<void(const QString &)> next)
{
    if (!LauncherPaths::ensureRootExists()) {
        fail(tr("Нет доступа к папке лаунчера"));
        return;
    }

    const QString target = LauncherPaths::loaderDownloadDir() + u'/' + loaderKey(m_loader) + u'-'
                           + QString(m_loaderVersion).replace(u'/', u'_')
                           + QStringLiteral("-installer.jar");

    setStage(tr("Загрузка установщика %1").arg(loaderTitle(m_loader)));
    m_currentFile = QFileInfo(target).fileName();
    emit progressChanged();

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

    m_reply = m_network->get(request);
    connect(m_reply, &QNetworkReply::downloadProgress, this,
            [this](qint64 received, qint64 total) {
                m_bytesDone = received;
                m_bytesTotal = total;
                emit progressChanged();
            });
    connect(m_reply, &QNetworkReply::finished, this, [this, target, next] {
        QNetworkReply *reply = m_reply;
        m_reply = nullptr;
        reply->deleteLater();
        if (!m_running || m_canceling)
            return;

        if (reply->error() != QNetworkReply::NoError) {
            fail(tr("Не удалось скачать установщик %1: %2")
                     .arg(loaderTitle(m_loader), reply->errorString()));
            return;
        }

        const QByteArray payload = reply->readAll();
        QSaveFile file(target);
        if (!file.open(QIODevice::WriteOnly) || file.write(payload) != payload.size()
            || !file.commit()) {
            file.cancelWriting();
            fail(tr("Не удалось сохранить установщик %1").arg(loaderTitle(m_loader)));
            return;
        }
        next(target);
    });
}

// Без launcher_profiles.json установщик Forge и NeoForge завершается с
// сообщением про испорченный профиль лаунчера. Свои профили мы там не держим,
// поэтому достаточно заглушки.
bool ModLoaderInstaller::writeLauncherProfilesStub()
{
    const QString path = m_gameDir + QStringLiteral("/launcher_profiles.json");
    if (QFile::exists(path))
        return true;
    if (!QDir().mkpath(m_gameDir))
        return false;

    const QJsonObject stub{
        {QStringLiteral("profiles"), QJsonObject{}},
        {QStringLiteral("selectedProfile"), QString()},
        {QStringLiteral("clientToken"), QString()},
        {QStringLiteral("authenticationDatabase"), QJsonObject{}},
        {QStringLiteral("launcherVersion"),
         QJsonObject{{QStringLiteral("name"), QString()},
                     {QStringLiteral("format"), 21},
                     {QStringLiteral("profilesFormat"), 2}}}};

    QSaveFile file(path);
    if (!file.open(QIODevice::WriteOnly))
        return false;
    const QByteArray payload = QJsonDocument(stub).toJson(QJsonDocument::Indented);
    if (file.write(payload) != payload.size()) {
        file.cancelWriting();
        return false;
    }
    return file.commit();
}

void ModLoaderInstaller::runInstaller(const QString &installerPath)
{
    if (!writeLauncherProfilesStub()) {
        fail(tr("Не удалось создать launcher_profiles.json — без него установщик %1 не работает")
                 .arg(loaderTitle(m_loader)));
        return;
    }

    // Постобработка установщика гоняет те же классы, что и сама игра, поэтому
    // требования к Java берём у ванильной версии.
    QString loadError;
    const MinecraftVersion base = VersionLoader::load(m_gameDir, m_gameVersion, {}, &loadError);
    const int requiredMajor = base.isValid() ? base.javaMajor : 8;

    QString javaError;
    const QString java = JavaLocator::select(m_gameDir, requiredMajor, m_javaPreference, &javaError);
    if (java.isEmpty()) {
        fail(javaError.isEmpty() ? tr("Java не найдена — установщик %1 запустить нечем")
                                       .arg(loaderTitle(m_loader))
                                 : javaError);
        return;
    }

    m_versionsBefore = VersionLoader::installedVersions(m_gameDir);
    setStage(tr("Установка %1").arg(loaderTitle(m_loader)));
    m_currentFile = tr("работает установщик, это может занять несколько минут");
    // Прогресс установщика неизвестен: панель покажет «…».
    m_bytesDone = 0;
    m_bytesTotal = 0;
    emit progressChanged();

    m_process = new QProcess(this);
    m_process->setProgram(java);
    m_process->setArguments({QStringLiteral("-jar"), installerPath,
                             QStringLiteral("--installClient"), QDir::toNativeSeparators(m_gameDir)});
    m_process->setWorkingDirectory(LauncherPaths::loaderDownloadDir());
    m_process->setProcessChannelMode(QProcess::MergedChannels);

    connect(m_process, &QProcess::readyReadStandardOutput, this, [this] {
        const QStringList lines = QString::fromLocal8Bit(m_process->readAllStandardOutput())
                                      .split(u'\n', Qt::SkipEmptyParts);
        for (const QString &line : lines) {
            m_installerLog << line.trimmed();
            emit log(line.trimmed());
        }
        // Хвоста хватает, чтобы показать причину отказа; весь лог не нужен.
        while (m_installerLog.size() > kLogTailLines)
            m_installerLog.removeFirst();
    });
    connect(m_process, &QProcess::finished, this,
            [this](int exitCode, QProcess::ExitStatus status) {
                handleInstallerFinished(exitCode, status == QProcess::CrashExit);
            });
    connect(m_process, &QProcess::errorOccurred, this, [this](QProcess::ProcessError error) {
        if (m_canceling || error == QProcess::Crashed)
            return;   // Crashed придёт ещё и в finished
        fail(tr("Не удалось запустить установщик %1: %2")
                 .arg(loaderTitle(m_loader), m_process->errorString()));
    });

    m_process->start();
    if (!m_process->waitForStarted(kTransferTimeoutMs)) {
        fail(tr("Установщик %1 не запустился: %2")
                 .arg(loaderTitle(m_loader), m_process->errorString()));
        return;
    }

    // Совсем зависший установщик не должен блокировать лаунчер навсегда.
    QTimer::singleShot(kInstallerTimeoutMs, this, [this] {
        if (m_process && m_process->state() != QProcess::NotRunning) {
            m_process->kill();
            fail(tr("Установщик %1 не завершился за отведённое время").arg(loaderTitle(m_loader)));
        }
    });
}

void ModLoaderInstaller::handleInstallerFinished(int exitCode, bool crashed)
{
    if (m_process) {
        m_process->deleteLater();
        m_process = nullptr;
    }
    if (!m_running || m_canceling)
        return;

    if (crashed || exitCode != 0) {
        fail(tr("Установщик %1 завершился с ошибкой (код %2).\n%3")
                 .arg(loaderTitle(m_loader))
                 .arg(exitCode)
                 .arg(m_installerLog.join(u'\n')));
        return;
    }

    // Какой профиль появился — выясняем разницей: id у Forge разные по эпохам
    // (1.20.1-forge-47.4.0, 1.12.2-forge-14.23.5.2859, neoforge-21.1.66).
    const QStringList after = VersionLoader::installedVersions(m_gameDir);
    QStringList added;
    for (const QString &id : after) {
        if (!m_versionsBefore.contains(id))
            added << id;
    }

    if (added.isEmpty()) {
        fail(tr("Установщик %1 отработал, но новая версия в %2 не появилась.\n%3")
                 .arg(loaderTitle(m_loader),
                      QDir::toNativeSeparators(m_gameDir + QStringLiteral("/versions")),
                      m_installerLog.join(u'\n')));
        return;
    }
    if (added.size() > 1) {
        // Берём тот, где встречается версия лоадера: остальные — попутно
        // доустановленные зависимости.
        const auto match = std::find_if(added.cbegin(), added.cend(), [this](const QString &id) {
            return id.contains(m_loaderVersion);
        });
        added = {match != added.cend() ? *match : added.first()};
    }

    handOffToVersionInstaller(added.first());
}

// ── Общий хвост ────────────────────────────────────────────────────────────

void ModLoaderInstaller::handOffToVersionInstaller(const QString &versionId)
{
    m_awaitingVersionId = versionId;
    setStage(tr("Загрузка файлов версии"));
    m_currentFile = versionId;
    emit progressChanged();
    m_versionInstaller->install(m_gameDir, versionId);
}

void ModLoaderInstaller::setStage(const QString &stage)
{
    m_stage = stage;
    emit progressChanged();
}

void ModLoaderInstaller::reset()
{
    m_running = false;
    m_canceling = false;
    m_awaitingVersionId.clear();
    m_stage.clear();
    m_currentFile.clear();
    m_bytesDone = 0;
    m_bytesTotal = 0;
    m_versionsBefore.clear();
    if (m_process) {
        m_process->deleteLater();
        m_process = nullptr;
    }
    emit progressChanged();
}

void ModLoaderInstaller::fail(const QString &message)
{
    const QString label = m_label;
    reset();
    emit failed(label, message);
}
