// SPDX-License-Identifier: GPL-3.0-only
/*
 *  Prism Launcher - Minecraft Launcher
 *  Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
 *  Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
 *
 *  This program 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, version 3.
 *
 *  This program 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 this program.  If not, see <https://www.gnu.org/licenses/>.
 */

#include "Packwiz.h"

#include <QDebug>
#include <QDir>
#include <QObject>
#include <algorithm>
#include <compare>
#include <sstream>
#include <string>
#include <utility>

#include "FileSystem.h"
#include "StringUtils.h"

#include "Version.h"
#include "modplatform/ModIndex.h"

#include <toml++/toml.h>

namespace Packwiz {

namespace {
auto getRealIndexName(const QDir& indexDir, const QString& normalizedFname, bool shouldFindMatch = false) -> QString
{
    const QFile indexFile(indexDir.absoluteFilePath(normalizedFname));

    QString realFname = normalizedFname;
    if (!indexFile.exists()) {
        // Tries to get similar entries
        for (auto& fileName : indexDir.entryList(QDir::Filter::Files)) {
            if (QString::compare(normalizedFname, fileName, Qt::CaseInsensitive) == 0) {
                realFname = fileName;
                break;
            }
        }

        if (shouldFindMatch && (QString::compare(normalizedFname, realFname, Qt::CaseSensitive) == 0)) {
            qCritical() << "Could not find a match for a valid metadata file!";
            qCritical() << "File:" << normalizedFname;
            return {};
        }
    }

    return realFname;
}

// Helpers
auto indexFileName(const QString& modSlug) -> QString
{
    if (modSlug.endsWith(".pw.toml")) {
        return modSlug;
    }
    return QString("%1.pw.toml").arg(modSlug);
}

// Helper functions for extracting data from the TOML file
auto stringEntry(toml::table table, const QString& entryName) -> QString
{
    auto* node = table.get(StringUtils::toStdString(entryName));
    if (!node) {
        qDebug() << "Failed to read str property '" + entryName + "' in mod metadata.";
        return {};
    }

    return node->value_or("");
}

auto intEntry(toml::table table, const QString& entryName) -> int
{
    auto* node = table.get(StringUtils::toStdString(entryName));
    if (!node) {
        qDebug() << "Failed to read int property '" + entryName + "' in mod metadata.";
        return {};
    }

    return node->value_or(0);
}

bool sortMCVersions(const QString& a, const QString& b)
{
    auto cmp = Version(a) <=> Version(b);
    if (cmp == std::strong_ordering::equal) {
        return a < b;
    }
    return cmp == std::strong_ordering::less;
}

}  // namespace
auto V1::createModFormat([[maybe_unused]] const QDir& index_dir,
                         ModPlatform::IndexedPack& mod_pack,
                         ModPlatform::IndexedVersion& mod_version) -> Mod
{
    Mod mod;

    mod.slug = mod_pack.slug;
    mod.name = mod_pack.name;
    mod.filename = mod_version.fileName;

    if (mod_pack.provider == ModPlatform::ResourceProvider::FLAME) {
        mod.mode = "metadata:curseforge";
    } else {
        mod.mode = "url";
        mod.url = mod_version.downloadUrl;
    }

    mod.hash_format = mod_version.hash_type;
    mod.hash = mod_version.hash;

    mod.provider = mod_pack.provider;
    mod.file_id = mod_version.fileId;
    mod.project_id = mod_pack.addonId;
    mod.side = mod_version.side == ModPlatform::Side::NoSide ? mod_pack.side : mod_version.side;
    mod.loaders = mod_version.loaders;
    mod.mcVersions = mod_version.mcVersion;
    mod.mcVersions.removeDuplicates();
    std::ranges::sort(mod.mcVersions, sortMCVersions);
    mod.releaseType = mod_version.version_type;

    mod.version_number = mod_version.version_number;
    if (mod.version_number.isNull())  // on CurseForge, there is only a version name - not a version number
        mod.version_number = mod_version.version;

    mod.dependencies = mod_version.dependencies;
    return mod;
}

void V1::updateModIndex(const QDir& index_dir, Mod& mod)
{
    if (!mod.isValid()) {
        qCritical() << QString("Tried to update metadata of an invalid mod!");
        return;
    }

    // Ensure the corresponding mod's info exists, and create it if not

    auto normalized_fname = indexFileName(mod.slug);
    auto real_fname = getRealIndexName(index_dir, normalized_fname);

    QFile index_file(index_dir.absoluteFilePath(real_fname));

    if (real_fname != normalized_fname)
        index_file.rename(normalized_fname);

    // There's already data on there!
    // TODO: We should do more stuff here, as the user is likely trying to
    // override a file. In this case, check versions and ask the user what
    // they want to do!
    if (index_file.exists()) {
        index_file.remove();
    } else {
        FS::ensureFilePathExists(index_file.fileName());
    }

    toml::table update;
    switch (mod.provider) {
        case (ModPlatform::ResourceProvider::FLAME):
            if (mod.file_id.toInt() == 0 || mod.project_id.toInt() == 0) {
                qCritical() << QString("Did not write file %1 because missing information!").arg(normalized_fname);
                return;
            }
            update = toml::table{
                { "file-id", mod.file_id.toInt() },
                { "project-id", mod.project_id.toInt() },
            };
            break;
        case (ModPlatform::ResourceProvider::MODRINTH):
            if (mod.mod_id().toString().isEmpty() || mod.version().toString().isEmpty()) {
                qCritical() << QString("Did not write file %1 because missing information!").arg(normalized_fname);
                return;
            }
            update = toml::table{
                { "mod-id", mod.mod_id().toString().toStdString() },
                { "version", mod.version().toString().toStdString() },
            };
            break;
    }

    toml::array loaders;
    for (auto loader : ModPlatform::modLoaderTypesToList(mod.loaders)) {
        loaders.push_back(getModLoaderAsString(loader).toStdString());
    }
    toml::array mcVersions;
    for (auto version : mod.mcVersions) {
        mcVersions.push_back(version.toStdString());
    }

    if (!index_file.open(QIODevice::ReadWrite)) {
        qCritical() << "Could not open file" << normalized_fname << "error:" << index_file.errorString();
        return;
    }

    toml::array deps;
    for (auto dep : mod.dependencies) {
        auto tbl = toml::table{ { "addonId", dep.addonId.toString().toStdString() },
                                { "type", ModPlatform::DependencyTypeUtils::toString(dep.type).toStdString() } };
        if (!dep.version.isEmpty()) {
            tbl.emplace("version", dep.version.toStdString());
        }
        deps.push_back(tbl);
    }

    // Put TOML data into the file
    QTextStream in_stream(&index_file);
    {
        auto tbl = toml::table{ { "name", mod.name.toStdString() },
                                { "filename", mod.filename.toStdString() },
                                { "side", ModPlatform::SideUtils::toString(mod.side).toStdString() },
                                { "x-prismlauncher-loaders", loaders },
                                { "x-prismlauncher-mc-versions", mcVersions },
                                { "x-prismlauncher-release-type", mod.releaseType.toString().toStdString() },
                                { "x-prismlauncher-version-number", mod.version_number.toStdString() },
                                { "x-prismlauncher-dependencies", deps },
                                { "download",
                                  toml::table{
                                      { "mode", mod.mode.toStdString() },
                                      { "url", mod.url.toString().toStdString() },
                                      { "hash-format", mod.hash_format.toStdString() },
                                      { "hash", mod.hash.toStdString() },
                                  } },
                                { "update", toml::table{ { ModPlatform::ProviderCapabilities::name(mod.provider), update } } } };
        std::stringstream ss;
        ss << tbl;
        in_stream << QString::fromStdString(ss.str());
    }

    index_file.flush();
    index_file.close();
}

void V1::deleteModIndex(const QDir& index_dir, QString& mod_slug)
{
    auto normalized_fname = indexFileName(mod_slug);
    auto real_fname = getRealIndexName(index_dir, normalized_fname);
    if (real_fname.isEmpty())
        return;

    QFile index_file(index_dir.absoluteFilePath(real_fname));

    if (!index_file.exists()) {
        qWarning() << QString("Tried to delete non-existent mod metadata for %1!").arg(mod_slug);
        return;
    }

    if (!index_file.remove()) {
        qWarning() << QString("Failed to remove metadata for mod %1!").arg(mod_slug);
    }
}

auto V1::getIndexForMod(const QDir& index_dir, QString slug) -> Mod
{
    Mod mod;

    auto normalized_fname = indexFileName(slug);
    auto real_fname = getRealIndexName(index_dir, normalized_fname, true);
    if (real_fname.isEmpty())
        return {};

    toml::table table;
#if TOML_EXCEPTIONS
    try {
        table = toml::parse_file(StringUtils::toStdString(index_dir.absoluteFilePath(real_fname)));
    } catch (const toml::parse_error& err) {
        qWarning() << QString("Could not open file %1!").arg(normalized_fname);
        qWarning() << "Reason:" << QString(err.what());
        return {};
    }
#else
    toml::parse_result result = toml::parse_file(StringUtils::toStdString(index_dir.absoluteFilePath(real_fname)));
    if (!result) {
        qWarning() << QString("Could not open file %1!").arg(normalized_fname);
        qWarning() << "Reason:" << result.error().description();
        return {};
    }
    table = result.table();
#endif

    // index_file.close();

    mod.slug = slug;

    {  // Basic info
        mod.name = stringEntry(table, "name");
        mod.filename = stringEntry(table, "filename");
        mod.side = ModPlatform::SideUtils::fromString(stringEntry(table, "side"));
        mod.releaseType = ModPlatform::IndexedVersionType::fromString(table["x-prismlauncher-release-type"].value_or(""));
        if (auto loaders = table["x-prismlauncher-loaders"]; loaders && loaders.is_array()) {
            for (auto&& loader : *loaders.as_array()) {
                if (loader.is_string()) {
                    mod.loaders |= ModPlatform::getModLoaderFromString(QString::fromStdString(loader.as_string()->value_or("")));
                }
            }
        }
        if (auto versions = table["x-prismlauncher-mc-versions"]; versions && versions.is_array()) {
            for (auto&& version : *versions.as_array()) {
                if (version.is_string()) {
                    auto ver = QString::fromStdString(version.as_string()->value_or(""));
                    if (!ver.isEmpty()) {
                        mod.mcVersions << ver;
                    }
                }
            }
            mod.mcVersions.removeDuplicates();
            std::ranges::sort(mod.mcVersions, sortMCVersions);
        }
    }
    mod.version_number = table["x-prismlauncher-version-number"].value_or("");

    {  // [download] info
        auto download_table = table["download"].as_table();
        if (!download_table) {
            qCritical() << QString("No [download] section found on mod metadata!");
            return {};
        }

        mod.mode = stringEntry(*download_table, "mode");
        mod.url = stringEntry(*download_table, "url");
        mod.hash_format = stringEntry(*download_table, "hash-format");
        mod.hash = stringEntry(*download_table, "hash");
    }

    {  // [update] info
        using Provider = ModPlatform::ResourceProvider;

        auto update_table = table["update"];
        if (!update_table || !update_table.is_table()) {
            qCritical() << QString("No [update] section found on mod metadata!");
            return {};
        }

        toml::table* mod_provider_table = nullptr;
        if ((mod_provider_table = update_table[ModPlatform::ProviderCapabilities::name(Provider::FLAME)].as_table())) {
            mod.provider = Provider::FLAME;
            mod.file_id = intEntry(*mod_provider_table, "file-id");
            mod.project_id = intEntry(*mod_provider_table, "project-id");
        } else if ((mod_provider_table = update_table[ModPlatform::ProviderCapabilities::name(Provider::MODRINTH)].as_table())) {
            mod.provider = Provider::MODRINTH;
            mod.mod_id() = stringEntry(*mod_provider_table, "mod-id");
            mod.version() = stringEntry(*mod_provider_table, "version");
        } else {
            qCritical() << QString("No mod provider on mod metadata!");
            return {};
        }
    }
    {  // dependencies
        auto deps = table["x-prismlauncher-dependencies"].as_array();
        if (deps) {
            for (auto&& depNode : *deps) {
                auto dep = depNode.as_table();
                if (dep) {
                    ModPlatform::Dependency d;
                    d.addonId = stringEntry(*dep, "addonId");
                    if (dep->contains("version")) {
                        d.version = stringEntry(*dep, "version");
                    }
                    d.type = ModPlatform::DependencyTypeUtils::fromString(stringEntry(*dep, "type"));
                    mod.dependencies << d;
                }
            }
        }
    }

    return mod;
}

auto V1::getIndexForMod(const QDir& index_dir, QVariant& mod_id) -> Mod
{
    for (auto& file_name : index_dir.entryList(QDir::Filter::Files)) {
        auto mod = getIndexForMod(index_dir, file_name);

        if (mod.mod_id() == mod_id)
            return mod;
    }

    return {};
}

}  // namespace Packwiz
