#include <QCoreApplication>
#include <QEventLoop>
#include <QFile>
#include <QFileInfo>
#include <QGuiApplication>
#include <QHash>
#include <QIcon>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QMetaObject>
#include <QSet>
#include <QThread>
#include <QTimer>
#include <QVariant>

#include <KNSCore/Author>
#include <KNSCore/CategoryMetadata>
#include <KNSCore/EngineBase>
#include <KNSCore/Entry>
#include <KNSCore/ResultsStream>
#include <KNSCore/SearchRequest>
#include <KNSCore/Transaction>

#include <cstdlib>
#include <cstring>
#include <functional>

namespace {

// Discover asks KNewStuff for larger chunks; small page sizes can come back
// short even when more server-side results exist.
constexpr int KNS_PAGE_SIZE = 100;

char *dupJson(const QJsonValue &value)
{
    const QByteArray data = QJsonDocument(value.toArray()).toJson(QJsonDocument::Compact);
    char *out = static_cast<char *>(std::malloc(static_cast<size_t>(data.size()) + 1));
    if (!out) {
        return nullptr;
    }
    std::memcpy(out, data.constData(), static_cast<size_t>(data.size()));
    out[data.size()] = '\0';
    return out;
}

char *dupJsonObject(const QJsonObject &value)
{
    const QByteArray data = QJsonDocument(value).toJson(QJsonDocument::Compact);
    char *out = static_cast<char *>(std::malloc(static_cast<size_t>(data.size()) + 1));
    if (!out) {
        return nullptr;
    }
    std::memcpy(out, data.constData(), static_cast<size_t>(data.size()));
    out[data.size()] = '\0';
    return out;
}

char *runOnAppThread(const std::function<char *()> &fn)
{
    QCoreApplication *app = QCoreApplication::instance();
    if (!app || QThread::currentThread() == app->thread()) {
        return fn();
    }

    char *result = nullptr;
    QMetaObject::invokeMethod(app, [&] {
        result = fn();
    }, Qt::BlockingQueuedConnection);
    return result;
}

QString safeString(const char *value)
{
    return value ? QString::fromUtf8(value) : QString();
}

QString resolveConfigPath(const QString &config);
bool initEngine(KNSCore::EngineBase &engine, const QString &config, QString *error);

QString categoryLabel(const QString &id)
{
    if (id.endsWith(QStringLiteral(".knsrc"))) return QFileInfo(id).baseName();
    const int split = id.indexOf(QStringLiteral("::"));
    if (split > 0) return id.mid(split + 2).section(QLatin1Char('/'), -1);
    if (id == "widgets") return QStringLiteral("Widgets");
    if (id == "themes") return QStringLiteral("Themes & Appearance");
    if (id == "window-manager") return QStringLiteral("Window Manager");
    if (id == "wallpapers") return QStringLiteral("Wallpapers");
    if (id == "service-menus") return QStringLiteral("Service Menus");
    if (id == "krunner") return QStringLiteral("KRunner");
    if (id == "system-monitor") return QStringLiteral("System Monitor");
    return QStringLiteral("Application Add-ons");
}

bool isDiscoverPlasmaConfig(const QString &file)
{
    static const QSet<QString> plasma = {
        QStringLiteral("aurorae.knsrc"),
        QStringLiteral("icons.knsrc"),
        QStringLiteral("kfontinst.knsrc"),
        QStringLiteral("lookandfeel.knsrc"),
        QStringLiteral("plasma-themes.knsrc"),
        QStringLiteral("plasmoids.knsrc"),
        QStringLiteral("wallpaper.knsrc"),
        QStringLiteral("wallpaper-mobile.knsrc"),
        QStringLiteral("xcursor.knsrc"),
        QStringLiteral("cgcgtk3.knsrc"),
        QStringLiteral("cgcicon.knsrc"),
        QStringLiteral("cgctheme.knsrc"),
        QStringLiteral("comic.knsrc"),
        QStringLiteral("colorschemes.knsrc"),
        QStringLiteral("emoticons.knsrc"),
        QStringLiteral("plymouth.knsrc"),
        QStringLiteral("sddmtheme.knsrc"),
        QStringLiteral("wallpaperplugin.knsrc"),
        QStringLiteral("ksplash.knsrc"),
        QStringLiteral("kwinswitcher.knsrc"),
        QStringLiteral("kwineffect.knsrc"),
        QStringLiteral("kwinscripts.knsrc"),
        QStringLiteral("window-decorations.knsrc"),
        QStringLiteral("kwinswitcher-x11.knsrc"),
        QStringLiteral("kwineffect-x11.knsrc"),
        QStringLiteral("kwinscripts-x11.knsrc"),
        QStringLiteral("window-decorations-x11.knsrc"),
    };
    return plasma.contains(file);
}

bool readKnsGroup(const QString &config, QString *displayName, QStringList *categories, bool *hasApplications)
{
    const QString path = resolveConfigPath(config);
    const QString fileName = QFileInfo(path).fileName();
    QFile file(path);
    if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
        return false;
    }

    QHash<QString, QString> versioned;
    QHash<QString, QString> versionless;
    QHash<QString, QString> *current = nullptr;

    while (!file.atEnd()) {
        QString line = QString::fromUtf8(file.readLine()).trimmed();
        if (line.isEmpty() || line.startsWith(QLatin1Char('#')) || line.startsWith(QLatin1Char(';'))) {
            continue;
        }
        if (line.startsWith(QLatin1Char('[')) && line.endsWith(QLatin1Char(']'))) {
            const QString group = line.mid(1, line.length() - 2).trimmed();
            if (group == QStringLiteral("KNewStuff3")) current = &versioned;
            else if (group == QStringLiteral("KNewStuff")) current = &versionless;
            else current = nullptr;
            continue;
        }
        if (!current) {
            continue;
        }

        const int equals = line.indexOf(QLatin1Char('='));
        if (equals <= 0) {
            continue;
        }
        const QString key = line.left(equals).trimmed();
        const QString value = line.mid(equals + 1).trimmed();
        current->insert(key, value);
    }

    const QHash<QString, QString> &group = !versioned.isEmpty() ? versioned : versionless;
    if (group.isEmpty()) {
        return false;
    }

    if (displayName) {
        *displayName = group.value(QStringLiteral("Name"));
        if (displayName->isEmpty()) {
            *displayName = QFileInfo(fileName).baseName();
            if (!displayName->isEmpty()) {
                (*displayName)[0] = (*displayName)[0].toUpper();
            }
        }
    }
    if (categories) {
        *categories = group.value(QStringLiteral("Categories")).split(QLatin1Char(','), Qt::SkipEmptyParts);
        for (QString &category : *categories) {
            category = category.trimmed();
        }
    }
    if (hasApplications) {
        const QString value = group.value(QStringLiteral("X-Discover-HasApplications")).toLower();
        *hasApplications = value == QStringLiteral("true") || value == QStringLiteral("1") || value == QStringLiteral("yes");
    }
    return true;
}

QStringList categoryConfigs(const QString &id)
{
    const int split = id.indexOf(QStringLiteral("::"));
    if (split > 0) return {id.left(split)};
    if (id.endsWith(QStringLiteral(".knsrc"))) return {id};

    if (id == "widgets") return {QStringLiteral("plasmoids.knsrc")};
    if (id == "themes") {
        return {
            QStringLiteral("lookandfeel.knsrc"),
            QStringLiteral("plasma-themes.knsrc"),
            QStringLiteral("colorschemes.knsrc"),
            QStringLiteral("icons.knsrc"),
            QStringLiteral("xcursor.knsrc"),
            QStringLiteral("gtk_themes.knsrc"),
            QStringLiteral("aurorae.knsrc"),
            QStringLiteral("ksplash.knsrc"),
            QStringLiteral("sddmtheme.knsrc"),
        };
    }
    if (id == "window-manager") {
        return {
            QStringLiteral("kwineffect.knsrc"),
            QStringLiteral("kwinscripts.knsrc"),
            QStringLiteral("kwinswitcher.knsrc"),
            QStringLiteral("window-decorations.knsrc"),
            QStringLiteral("kwineffect-x11.knsrc"),
            QStringLiteral("kwinscripts-x11.knsrc"),
            QStringLiteral("kwinswitcher-x11.knsrc"),
            QStringLiteral("window-decorations-x11.knsrc"),
        };
    }
    if (id == "wallpapers") {
        return {
            QStringLiteral("wallpaper.knsrc"),
            QStringLiteral("wallpaperplugin.knsrc"),
            QStringLiteral("wallpaper-mobile.knsrc"),
        };
    }
    if (id == "service-menus") return {QStringLiteral("servicemenu.knsrc")};
    if (id == "krunner") return {QStringLiteral("krunner.knsrc")};
    if (id == "system-monitor") {
        return {
            QStringLiteral("systemmonitor-faces.knsrc"),
            QStringLiteral("systemmonitor-presets.knsrc"),
            QStringLiteral("plasma-systemmonitor.knsrc"),
        };
    }

    QSet<QString> known;
    const QStringList knownGroups = {
        QStringLiteral("widgets"),
        QStringLiteral("themes"),
        QStringLiteral("window-manager"),
        QStringLiteral("wallpapers"),
        QStringLiteral("service-menus"),
        QStringLiteral("krunner"),
        QStringLiteral("system-monitor"),
    };
    for (const QString &group : knownGroups) {
        for (const QString &config : categoryConfigs(group)) {
            known.insert(config);
        }
    }

    QStringList configs;
    for (const QString &path : KNSCore::EngineBase::availableConfigFiles()) {
        const QString file = QFileInfo(path).fileName();
        if (!known.contains(file)) {
            configs.push_back(file);
        }
    }
    return configs;
}

QString categoryFilterName(const QString &id)
{
    const int split = id.indexOf(QStringLiteral("::"));
    if (split > 0) return id.mid(split + 2);
    return QString();
}

QString metadataLabel(const QHash<QString, QString> &labels, const QString &category)
{
    const QString label = labels.value(category);
    if (!label.isEmpty()) return label;
    return category.section(QLatin1Char('/'), -1);
}

QJsonArray buildCategoryNodes(const QString &config,
                              const QStringList &categories,
                              const QHash<QString, QString> &labels,
                              const QString &prefix = QString())
{
    QJsonArray out;
    for (const QString &category : categories) {
        bool direct = false;
        if (prefix.isEmpty()) {
            direct = !category.contains(QLatin1Char('/'));
        } else if (category.startsWith(prefix + QLatin1Char('/'))) {
            const QString rest = category.mid(prefix.length() + 1);
            direct = !rest.contains(QLatin1Char('/'));
        }
        if (!direct) continue;

        QJsonObject node;
        node["id"] = config + QStringLiteral("::") + category;
        node["label"] = metadataLabel(labels, category);
        node["subs"] = buildCategoryNodes(config, categories, labels, category);
        out.append(node);
    }
    return out;
}

QJsonArray discoverCategoryTree()
{
    QJsonArray plasmaAddons;
    QJsonArray applicationAddons;

    for (const QString &path : KNSCore::EngineBase::availableConfigFiles()) {
        const QString file = QFileInfo(path).fileName();
        QString displayName;
        QStringList categories;
        bool hasApplications = false;
        if (!readKnsGroup(path, &displayName, &categories, &hasApplications)) {
            continue;
        }

        QJsonObject node;
        node["id"] = file;
        node["label"] = displayName;
        QJsonArray children;
        if (categories.count() > 1) {
            children.append(QJsonObject{
                {"id", file},
                {"label", QStringLiteral("All")},
                {"subs", QJsonArray{}},
            });
            const QJsonArray categoryNodes = buildCategoryNodes(file, categories, {});
            for (const QJsonValue &categoryNode : categoryNodes) {
                children.append(categoryNode);
            }
        }
        node["subs"] = children;

        if (!hasApplications && isDiscoverPlasmaConfig(file)) {
            plasmaAddons.append(node);
        } else {
            applicationAddons.append(node);
        }
    }

    QJsonArray out;
    if (!plasmaAddons.isEmpty()) {
        out.append(QJsonObject{
            {"id", QStringLiteral("plasma-addons")},
            {"label", QStringLiteral("Plasma Addons")},
            {"subs", plasmaAddons},
        });
    }
    if (!applicationAddons.isEmpty()) {
        out.append(QJsonObject{
            {"id", QStringLiteral("application-addons")},
            {"label", QStringLiteral("Application Addons")},
            {"subs", applicationAddons},
        });
    }
    return out;
}

QString resolveConfigPath(const QString &config)
{
    for (const QString &path : KNSCore::EngineBase::availableConfigFiles()) {
        if (QFileInfo(path).fileName() == config || path == config) {
            return path;
        }
    }
    return config;
}

bool configExists(const QString &config)
{
    for (const QString &path : KNSCore::EngineBase::availableConfigFiles()) {
        if (QFileInfo(path).fileName() == config || path == config) {
            return true;
        }
    }
    return false;
}

QJsonObject entryToJson(const KNSCore::Entry &entry, const QString &config, const QString &category)
{
    const QString icon = !entry.previewUrl(KNSCore::Entry::PreviewSmall1).isEmpty()
        ? entry.previewUrl(KNSCore::Entry::PreviewSmall1)
        : entry.previewUrl(KNSCore::Entry::PreviewBig1);

    QJsonArray screenshots;
    const QList<QPair<KNSCore::Entry::PreviewType, KNSCore::Entry::PreviewType>> pairs = {
        {KNSCore::Entry::PreviewSmall1, KNSCore::Entry::PreviewBig1},
        {KNSCore::Entry::PreviewSmall2, KNSCore::Entry::PreviewBig2},
        {KNSCore::Entry::PreviewSmall3, KNSCore::Entry::PreviewBig3},
    };
    for (const auto &pair : pairs) {
        const QString big = entry.previewUrl(pair.second);
        const QString small = entry.previewUrl(pair.first);
        if (!big.isEmpty()) screenshots.append(big);
        else if (!small.isEmpty()) screenshots.append(small);
    }

    QString version = entry.version();
    if (version.isEmpty() && entry.releaseDate().isValid()) {
        version = entry.releaseDate().toString(Qt::ISODate);
    }

    QString availableVersion = entry.updateVersion();
    if (availableVersion.isEmpty() && entry.updateReleaseDate().isValid()) {
        availableVersion = entry.updateReleaseDate().toString(Qt::ISODate);
    }

    const bool installed = entry.status() == KNSCore::Entry::Installed
        || entry.status() == KNSCore::Entry::Updateable;

    QJsonObject out;
    out["id"] = QStringLiteral("kns://%1/%2").arg(config, entry.uniqueId());
    out["kns_id"] = entry.uniqueId();
    out["knsrc"] = config;
    out["category_id"] = category;
    out["category_label"] = categoryLabel(category);
    out["name"] = entry.name();
    out["summary"] = entry.shortSummary().isEmpty() ? entry.summary().left(160) : entry.shortSummary();
    out["description"] = KNSCore::replaceBBCode(entry.summary());
    out["developer"] = entry.author().name();
    out["version"] = version;
    out["available_version"] = availableVersion;
    out["license"] = entry.license();
    out["icon_url"] = icon;
    out["screenshots"] = screenshots;
    out["url_homepage"] = entry.homepage().toString();
    out["url_donation"] = entry.donationLink();
    out["package_name"] = entry.uniqueId();
    out["source"] = QStringLiteral("plasma-addon");
    out["label"] = QStringLiteral("KDE Store");
    out["installed"] = installed;
    out["updateable"] = entry.status() == KNSCore::Entry::Updateable;
    out["rating"] = entry.rating();
    out["comments"] = entry.numberOfComments();
    out["downloads"] = entry.downloadCount();
    return out;
}

bool initEngine(KNSCore::EngineBase &engine, const QString &config, QString *error)
{
    QEventLoop loop;
    QTimer timer;
    timer.setSingleShot(true);

    bool loaded = false;
    QObject::connect(&engine, &KNSCore::EngineBase::providerAdded, &loop, [&] {
        loaded = true;
        loop.quit();
    });
    QObject::connect(&engine, &KNSCore::EngineBase::signalProvidersLoaded, &loop, [&] {
        loaded = true;
        loop.quit();
    });
    QObject::connect(&engine, &KNSCore::EngineBase::signalErrorCode, &loop,
                     [&](KNSCore::ErrorCode::ErrorCode, const QString &message, const QVariant &) {
        if (error) *error = message;
        loop.quit();
    });
    QObject::connect(&timer, &QTimer::timeout, &loop, [&] {
        if (error && error->isEmpty()) *error = QStringLiteral("Timed out loading KNewStuff provider.");
        loop.quit();
    });

    if (!engine.init(resolveConfigPath(config))) {
        if (error) *error = QStringLiteral("Could not initialize %1.").arg(config);
        return false;
    }

    timer.start(10000);
    loop.exec();
    return loaded;
}

QList<KNSCore::Entry> searchEntries(KNSCore::EngineBase &engine,
                                    KNSCore::Filter filter,
                                    const QString &query,
                                    const QStringList &categories,
                                    int page,
                                    int pageSize,
                                    QString *error)
{
    QList<KNSCore::Entry> entries;
    QEventLoop loop;
    QTimer timer;
    timer.setSingleShot(true);

    KNSCore::SearchRequest request(KNSCore::SortMode::Newest, filter, query, categories, page, pageSize);
    KNSCore::ResultsStream *stream = engine.search(request);
    QObject::connect(stream, &KNSCore::ResultsStream::entriesFound, &loop, [&](const KNSCore::Entry::List &found) {
        entries.append(found);
    });
    QObject::connect(stream, &KNSCore::ResultsStream::finished, &loop, [&] {
        loop.quit();
    });
    QObject::connect(&engine, &KNSCore::EngineBase::signalErrorCode, &loop,
                     [&](KNSCore::ErrorCode::ErrorCode, const QString &message, const QVariant &) {
        if (error) *error = message;
        loop.quit();
    });
    QObject::connect(&timer, &QTimer::timeout, &loop, [&] {
        if (error && error->isEmpty()) *error = QStringLiteral("Timed out loading KNewStuff entries.");
        loop.quit();
    });

    timer.start(15000);
    stream->fetch();
    loop.exec();
    return entries;
}

QJsonArray queryCategory(const QString &category, const QString &query, KNSCore::Filter filter, int page, int pageSize)
{
    QJsonArray out;
    QSet<QString> seen;
    const QString serverCategory = categoryFilterName(category);
    const QStringList requestCategories = serverCategory.isEmpty()
        ? QStringList{}
        : QStringList{serverCategory};
    for (const QString &config : categoryConfigs(category)) {
        if (!configExists(config)) {
            continue;
        }

        QString error;
        KNSCore::EngineBase engine;
        if (!initEngine(engine, config, &error)) {
            continue;
        }

        const QList<KNSCore::Entry> entries = searchEntries(engine, filter, query, requestCategories, page, pageSize, &error);
        for (const KNSCore::Entry &entry : entries) {
            const QString key = config + QStringLiteral(":") + entry.uniqueId();
            if (seen.contains(key)) continue;
            seen.insert(key);
            out.append(entryToJson(entry, config, category));
        }
    }
    return out;
}

bool parseKnsId(const QString &id, QString *config, QString *entryId)
{
    if (!id.startsWith(QStringLiteral("kns://"))) {
        return false;
    }
    const QString rest = id.mid(6);
    const int slash = rest.indexOf('/');
    if (slash <= 0) {
        return false;
    }
    *config = rest.left(slash);
    *entryId = rest.mid(slash + 1);
    return !config->isEmpty() && !entryId->isEmpty();
}

QJsonObject runTransaction(const QString &id, bool remove)
{
    QString config;
    QString entryId;
    if (!parseKnsId(id, &config, &entryId)) {
        return {{"ok", false}, {"msg", QStringLiteral("Invalid KNewStuff id.")}};
    }

    QString error;
    KNSCore::EngineBase engine;
    if (!initEngine(engine, config, &error)) {
        return {{"ok", false}, {"msg", error}};
    }

    const QList<KNSCore::Entry> entries = searchEntries(
        engine, KNSCore::Filter::ExactEntryId, entryId, {}, 0, 1, &error);
    if (entries.isEmpty()) {
        return {{"ok", false}, {"msg", QStringLiteral("Could not find KDE Store entry.")}};
    }

    QEventLoop loop;
    QTimer timer;
    timer.setSingleShot(true);
    QString message;
    bool ok = true;

    KNSCore::Transaction *tx = remove
        ? KNSCore::Transaction::uninstall(&engine, entries.first())
        : KNSCore::Transaction::installLatest(&engine, entries.first());

    QObject::connect(tx, &KNSCore::Transaction::signalMessage, &loop, [&](const QString &msg) {
        message = msg;
    });
    QObject::connect(tx, &KNSCore::Transaction::signalErrorCode, &loop,
                     [&](KNSCore::ErrorCode::ErrorCode, const QString &msg, const QVariant &) {
        ok = false;
        message = msg;
        loop.quit();
    });
    QObject::connect(tx, &KNSCore::Transaction::finished, &loop, [&] {
        loop.quit();
    });
    QObject::connect(&timer, &QTimer::timeout, &loop, [&] {
        ok = false;
        message = QStringLiteral("Timed out waiting for KDE Store transaction.");
        loop.quit();
    });

    timer.start(60000);
    loop.exec();
    return {{"ok", ok}, {"msg", message.isEmpty() ? QStringLiteral("Done.") : message}};
}

} // namespace

extern "C" void set_qt_app_properties() {
    // Ensure Qt can find image format plugins (WebP, AVIF, etc.)
    QCoreApplication::addLibraryPath("/usr/lib64/qt6/plugins");
    QCoreApplication::addLibraryPath("/usr/lib/qt6/plugins");

    QGuiApplication::setDesktopFileName("org.rakuos.Software");
    QGuiApplication::setWindowIcon(QIcon("/usr/share/pixmaps/rakuos-logo.png"));
}

extern "C" void free_qt_json(char *value) {
    std::free(value);
}

extern "C" char *plasma_addon_categories_json() {
    return runOnAppThread([]() -> char * {
        return dupJson(discoverCategoryTree());
    });
}

extern "C" char *plasma_addons_browse_json(const char *category, const char *query, int page) {
    const QString cat = safeString(category);
    const QString q = safeString(query);
    return runOnAppThread([cat, q, page]() -> char * {
    return dupJson(queryCategory(cat, q, KNSCore::Filter::None, page, KNS_PAGE_SIZE));
    });
}

extern "C" char *plasma_addons_installed_json() {
    return runOnAppThread([]() -> char * {
    QJsonArray all;
    for (const QString &category : {
             QStringLiteral("widgets"),
             QStringLiteral("themes"),
             QStringLiteral("window-manager"),
             QStringLiteral("wallpapers"),
             QStringLiteral("service-menus"),
             QStringLiteral("krunner"),
             QStringLiteral("system-monitor"),
             QStringLiteral("application-addons"),
         }) {
        const QJsonArray entries = queryCategory(category, QString(), KNSCore::Filter::Installed, 0, KNS_PAGE_SIZE);
        for (const QJsonValue &entry : entries) {
            all.append(entry);
        }
    }
    return dupJson(all);
    });
}

extern "C" char *plasma_addons_updates_json() {
    return runOnAppThread([]() -> char * {
    QJsonArray all;
    for (const QString &category : {
             QStringLiteral("widgets"),
             QStringLiteral("themes"),
             QStringLiteral("window-manager"),
             QStringLiteral("wallpapers"),
             QStringLiteral("service-menus"),
             QStringLiteral("krunner"),
             QStringLiteral("system-monitor"),
             QStringLiteral("application-addons"),
         }) {
        const QJsonArray entries = queryCategory(category, QString(), KNSCore::Filter::Updates, 0, KNS_PAGE_SIZE);
        for (const QJsonValue &entry : entries) {
            all.append(entry);
        }
    }
    return dupJson(all);
    });
}

extern "C" char *plasma_addon_install_json(const char *id) {
    const QString addonId = safeString(id);
    return runOnAppThread([addonId]() -> char * {
        return dupJsonObject(runTransaction(addonId, false));
    });
}

extern "C" char *plasma_addon_remove_json(const char *id) {
    const QString addonId = safeString(id);
    return runOnAppThread([addonId]() -> char * {
        return dupJsonObject(runTransaction(addonId, true));
    });
}
