/*
 * Copyright (C) 2016-2026 Matthias Klumpp <matthias@tenstral.net>
 *
 * Licensed under the GNU Lesser General Public License Version 3
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the license, or
 * (at your option) any later version.
 *
 * This software 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this software.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "extractor.h"

#include <format>
#include <string_view>
#include <algorithm>
#include <sstream>
#include <appstream.h>
#include <appstream-compose.h>
#include <glib.h>

#include "config.h"
#include "logging.h"
#include "hintregistry.h"
#include "result.h"
#include "backends/interfaces.h"
#include "datastore.h"
#include "iconhandler.h"
#include "utils.h"
#include "cptmodifiers.h"

namespace ASGenerator
{

DataExtractor::DataExtractor(
    std::shared_ptr<DataStore> db,
    std::shared_ptr<IconHandler> iconHandler,
    AsgLocaleUnit *localeUnit,
    AscImageFormat imageFormat,
    std::shared_ptr<InjectedModifications> modInjInfo,
    const std::string &prefix)
    : m_log(getLogger("extractor")),
      m_compose(nullptr),
      m_media(nullptr),
      m_dstore(std::move(db)),
      m_iconh(std::move(iconHandler)),
      m_modInj(std::move(modInjInfo)),
      m_l10nUnit(nullptr)
{
    m_conf = &Config::get();
    m_dtype = m_conf->metadataType;
    if (localeUnit != nullptr)
        m_l10nUnit = g_object_ref(localeUnit);

    m_compose = asc_compose_new();

    // media processing is done by a helper process which we want to reuse for as long as possible,
    // so we own one instance here and hand it to AscCompose as well as to the icon handler.
    // A DataExtractor is only ever used by one thread at a time, which is exactly what AscMedia requires.
    m_media = asc_media_new();
    asc_compose_set_media(m_compose, m_media);

    // format that any media rendered by the compose process is stored in
    asc_compose_set_image_format(m_compose, imageFormat);

    asc_compose_set_media_baseurl(m_compose, "");

    // set custom prefix for processing, if one was given
    if (!prefix.empty()) {
        const auto normPrefix = Utils::normalizePath(prefix);
        asc_compose_set_prefix(m_compose, normPrefix.c_str());
    }

    // Set callback for intermediate metadata checking
    asc_compose_set_check_metadata_early_func(m_compose, &checkMetadataIntermediate, this, nullptr);

    AscComposeFlags flags = static_cast<AscComposeFlags>(
        ASC_COMPOSE_FLAG_IGNORE_ICONS |             // we do custom icon processing
        ASC_COMPOSE_FLAG_PROCESS_UNPAIRED_DESKTOP | // handle desktop-entry files without metainfo data
        ASC_COMPOSE_FLAG_NO_FINAL_CHECK);           // we trigger the final check manually
    asc_compose_add_flags(m_compose, flags);

    // we handle all threading, so the compose process doesn't also have to be threaded
    // (this is also a prerequisite for sharing our AscMedia instance with it)
    asc_compose_remove_flags(m_compose, ASC_COMPOSE_FLAG_USE_THREADS);

    // set CAInfo for any download operations performed by this AscCompose
    if (!m_conf->caInfo.empty())
        asc_compose_set_cainfo(m_compose, m_conf->caInfo.c_str());

    // set dummy locale unit for advanced locale processing
    if (m_l10nUnit)
        asc_compose_set_locale_unit(m_compose, ASC_UNIT(m_l10nUnit));

    // set max screenshot size in bytes, if size is limited
    if (m_conf->maxScrFileSize != 0) {
        auto maxSize = static_cast<gssize>(m_conf->maxScrFileSize * 1024 * 1024);
        asc_compose_set_max_screenshot_size(m_compose, maxSize);
    }

    // enable or disable user-defined features
    if (m_conf->feature.validate)
        asc_compose_add_flags(m_compose, ASC_COMPOSE_FLAG_VALIDATE);
    else
        asc_compose_remove_flags(m_compose, ASC_COMPOSE_FLAG_VALIDATE);

    if (m_conf->feature.noDownloads)
        asc_compose_remove_flags(m_compose, ASC_COMPOSE_FLAG_ALLOW_NET);
    else
        asc_compose_add_flags(m_compose, ASC_COMPOSE_FLAG_ALLOW_NET);

    if (m_conf->feature.processLocale)
        asc_compose_add_flags(m_compose, ASC_COMPOSE_FLAG_PROCESS_TRANSLATIONS);
    else
        asc_compose_remove_flags(m_compose, ASC_COMPOSE_FLAG_PROCESS_TRANSLATIONS);

    if (m_conf->feature.processFonts)
        asc_compose_add_flags(m_compose, ASC_COMPOSE_FLAG_PROCESS_FONTS);
    else
        asc_compose_remove_flags(m_compose, ASC_COMPOSE_FLAG_PROCESS_FONTS);

    if (m_conf->feature.storeScreenshots)
        asc_compose_add_flags(m_compose, ASC_COMPOSE_FLAG_STORE_SCREENSHOTS);
    else
        asc_compose_remove_flags(m_compose, ASC_COMPOSE_FLAG_STORE_SCREENSHOTS);

    if (m_conf->feature.screenshotVideos)
        asc_compose_add_flags(m_compose, ASC_COMPOSE_FLAG_ALLOW_VIDEOS);
    else
        asc_compose_remove_flags(m_compose, ASC_COMPOSE_FLAG_ALLOW_VIDEOS);

    if (m_conf->feature.propagateMetaInfoArtifacts)
        asc_compose_add_flags(m_compose, ASC_COMPOSE_FLAG_PROPAGATE_ARTIFACTS);
    else
        asc_compose_remove_flags(m_compose, ASC_COMPOSE_FLAG_PROPAGATE_ARTIFACTS);

    // override icon policy with our own, possible user-modified one
    asc_compose_set_icon_policy(m_compose, m_conf->iconPolicy());

    // register allowed custom keys with the composer
    if (!m_conf->allowedCustomKeys.empty()) {
        asc_compose_add_flags(m_compose, ASC_COMPOSE_FLAG_PROPAGATE_CUSTOM);
        for (const auto &[key, _] : m_conf->allowedCustomKeys)
            asc_compose_add_custom_allowed(m_compose, key.c_str());
    } else {
        asc_compose_remove_flags(m_compose, ASC_COMPOSE_FLAG_PROPAGATE_CUSTOM);
    }
}

DataExtractor::~DataExtractor()
{
    g_object_unref(m_compose);
    g_object_unref(m_media);
    if (m_l10nUnit)
        g_object_unref(m_l10nUnit);
}

void DataExtractor::checkMetadataIntermediate(AscResult *cres, AscUnit *cunit, void *userData)
{
    auto self = static_cast<DataExtractor *>(userData);

    g_autoptr(GPtrArray) cptsPtrArray = asc_result_fetch_components(cres);
    for (guint i = 0; i < cptsPtrArray->len; i++) {
        auto cpt = AS_COMPONENT(g_ptr_array_index(cptsPtrArray, i));
        auto gcid = asc_result_gcid_for_component(cres, cpt);

        if (gcid == nullptr) {
            LOG_WARNING(
                self->m_log,
                "Component {} has no global ID, skipping intermediate metadata check.",
                as_component_get_id(cpt));
            continue;
        }

        const auto bundleId = asc_result_get_bundle_id(cres);
        if (bundleId && std::string(bundleId) == EXTRA_METAINFO_FAKE_PKGNAME) {
            // the "package" was injected and therefore has likely already been unlinked
            // and we will want to reprocess it unconditionally. Therefore, we just skip
            // all following checks on duplicate IDs and just continue processing the
            // metadata without modifications.
            continue;
        }

        // Find out which package this component belongs to. Nobody having claimed it means
        // that we are the first to produce it, so there is nothing to weigh ourselves against.
        const auto ownerPkid = self->m_dstore->getGcidOwner(gcid);
        if (ownerPkid.empty())
            continue;

        // We compare package names rather than IDs here, as a package that merely changed
        // its version is still the same software and keeps its component.
        const auto [ownerName, ownerVersion] = Utils::pkidSplitNameVersion(ownerPkid);
        if (bundleId && ownerName == bundleId) {
            // The component is ours already. If the owner is this very package built for a
            // different architecture, it has produced this exact component before - the global
            // ID is a hash of the metadata, and the media of an architecture variant is the
            // same as well - so we recycle what is in the pool instead of rendering it again
            // for every architecture we process.
            // Any other version of ourselves is processed again: the metadata can not have
            // changed, but this way its screenshots and icons are refreshed instead of being
            // left at whatever state an earlier run of ours produced.
            const auto ourVersion = Utils::pkidSplitNameVersion(self->m_currentPkid).second;
            if (ownerPkid == self->m_currentPkid || ownerVersion != ourVersion
                || !self->m_dstore->metadataExists(self->m_dtype, gcid))
                continue;

            // keep the global ID: we still provide this component, we just do not have to
            // produce it a second time
            asc_result_remove_component_full(cres, cpt, FALSE);
            continue;
        }

        // Another package provides the exact same component. Ownership is settled when the
        // results are committed, but if we would lose that contest there is no point in
        // rendering icons and screenshots for data we are going to throw away, so we bail
        // out here already. If we would win, we just keep going and take the component over
        // once we are done.
        if (DataStore::componentOwnerWins(self->m_currentPkid, ownerPkid))
            continue;

        if (as_component_get_kind(cpt) != AS_COMPONENT_KIND_WEB_APP) {
            // The exact same metadata exists in a different package already, we emit an error hint.
            // ATTENTION: This does not cover the case where *different* metadata (as in, different summary etc.)
            // but with the *same ID* exists.
            // We only catch that kind of problem later.
            // We can only get here if another package owns the component, so its name is known.
            asc_result_add_hint(
                cres,
                cpt,
                "metainfo-duplicate-id",
                "cid",
                as_component_get_id(cpt),
                "pkgname",
                ownerName.c_str(),
                nullptr);
        }

        // Drop the component along with its global ID: it belongs to another package, and a
        // component is only ever exported for the package owning it. This has to match what
        // DataStore::addGeneratorResult() does for a package that only loses at commit time,
        // or our output would depend on the order in which the two were processed.
        asc_result_remove_component_full(cres, cpt, TRUE);
    }
}

GPtrArray *DataExtractor::translateDesktopTextCallback(GKeyFile *dePtr, const char *text, void *userData)
{
    const auto &pkg = *static_cast<std::shared_ptr<Package> *>(userData);
    auto res = g_ptr_array_new_with_free_func(g_free);

    auto translations = pkg->getDesktopFileTranslations(dePtr, std::string(text));
    for (const auto &[key, value] : translations) {
        g_ptr_array_add(res, g_strdup(key.c_str()));
        g_ptr_array_add(res, g_strdup(value.c_str()));
    }

    return res;
}

GeneratorResult DataExtractor::processPackage(std::shared_ptr<Package> pkg)
{
    // reset compose instance to clear data from any previous invocation
    asc_compose_reset(m_compose);

    // Every package renders its media into a staging area of its own, which we hand to the
    // result we produce below. Until then it is ours to clean up.
    const auto stagingDir = m_dstore->createMediaStagingDir();
    asc_compose_set_media_result_dir(m_compose, stagingDir.string().c_str());

    // remember who we are processing, so the duplicate-component check can weigh us
    // against the package that currently owns a component
    m_currentPkid = pkg->id();

    // set external desktop-entry translation function, if needed.
    // AscCompose owns the user data, so we hand it a package reference that is guaranteed to
    // stay alive for as long as the callback may be invoked.
    if (pkg->hasDesktopFileTranslations()) {
        asc_compose_set_desktop_entry_l10n_func(
            m_compose, &translateDesktopTextCallback, new std::shared_ptr<Package>(pkg), [](gpointer data) {
                delete static_cast<std::shared_ptr<Package> *>(data);
            });
    } else {
        asc_compose_set_desktop_entry_l10n_func(m_compose, nullptr, nullptr, nullptr);
    }

    // wrap package into unit, so AppStream Compose can work with it
    auto unit = asg_package_unit_new(pkg);
    asc_compose_add_unit(m_compose, ASC_UNIT(unit));

    // process all data
    g_autoptr(GError) error = nullptr;
    if (!asc_compose_run(m_compose, nullptr, &error))
        throw std::runtime_error(
            std::format("Failed to run compose process: {}", error ? error->message : "Unknown error"));

    auto resultsArray = asc_compose_get_results(m_compose);

    // we processed one unit, so should always generate one result
    if (resultsArray->len != 1)
        throw std::runtime_error(
            std::format("Expected 1 result for data extraction, but retrieved {}.", resultsArray->len));

    // create result wrapper, handing the staging area over to it
    GeneratorResult gres(ASC_RESULT(g_ptr_array_index(resultsArray, 0)), pkg, stagingDir);

    // process icons and perform additional refinements
    g_autoptr(GPtrArray) cptsPtrArray = gres.fetchComponents();
    for (guint i = 0; i < cptsPtrArray->len; i++) {
        auto cpt = AS_COMPONENT(g_ptr_array_index(cptsPtrArray, i));
        const auto ckind = as_component_get_kind(cpt);

        auto context = as_component_get_context(cpt);
        if (!context) {
            context = as_context_new();
            as_component_set_context(cpt, context);
        }

        // find & store icons
        m_iconh->process(gres, cpt, m_media);
        if (gres.isIgnored(cpt))
            continue;

        // add fallback long descriptions only for desktop apps, console apps and web apps
        if (as_component_get_merge_kind(cpt) != AS_MERGE_KIND_NONE)
            continue;
        if (ckind != AS_COMPONENT_KIND_DESKTOP_APP && ckind != AS_COMPONENT_KIND_CONSOLE_APP
            && ckind != AS_COMPONENT_KIND_WEB_APP)
            continue;

        // inject package descriptions, if needed
        auto valueFlags = as_context_get_value_flags(context);
        valueFlags = static_cast<AsValueFlags>(valueFlags | AS_VALUE_FLAG_NO_TRANSLATION_FALLBACK);
        as_context_set_value_flags(context, valueFlags);
        as_context_set_locale(context, "C");
        const auto cptDesc = as_component_get_description(cpt);
        if (cptDesc != nullptr && cptDesc[0] != '\0')
            continue;

        // component doesn't have a long description, add one from the packaging.
        bool desc_added = false;
        for (const auto &[lang, desc] : pkg->description()) {
            as_component_set_description(cpt, desc.c_str(), lang.c_str());
            desc_added = true;
        }

        if (desc_added) {
            // we only add the "description-from-package" tag if we haven't already
            // emitted a "no-metainfo" tag, to avoid two hints explaining the same thing
            if (!gres.hasHint(cpt, "no-metainfo")) {
                if (!gres.addHint(cpt, "description-from-package"))
                    continue;
            }
        } else {
            std::string kindStr = as_component_kind_to_string(ckind);
            if (!gres.addHint(
                    cpt,
                    "description-missing",
                    {
                        {"kind", kindStr}
            }))
                continue;
        }
    }

    // handle GStreamer integration (usually for Ubuntu)
    if (m_conf->feature.processGStreamer && pkg->gst().has_value() && pkg->gst()->isNotEmpty()) {
        std::ostringstream data;
        data.str().reserve(200);

        g_autoptr(AsComponent) cpt = as_component_new();
        as_component_set_id(cpt, pkg->name().c_str());
        as_component_set_kind(cpt, AS_COMPONENT_KIND_CODEC);
        as_component_set_name(cpt, "GStreamer Multimedia Codecs", "C");

        for (const auto &[lang, desc] : pkg->summary()) {
            as_component_set_summary(cpt, desc.c_str(), lang.c_str());
            data << desc;
        }

        gres.addComponentWithString(cpt, data.str());
    }

    // perform final checks
    asc_compose_finalize_results(m_compose);

    // do our own final validation
    g_ptr_array_unref(g_steal_pointer(&cptsPtrArray));
    cptsPtrArray = gres.fetchComponents();
    for (guint i = 0; i < cptsPtrArray->len; i++) {
        auto cpt = AS_COMPONENT(g_ptr_array_index(cptsPtrArray, i));
        const auto ckind = as_component_get_kind(cpt);
        const auto cid = as_component_get_id(cpt);

        if (m_modInj) {
            // drop component that the repository owner wants to remove
            if (m_modInj->isComponentRemoved(cid)) {
                gres.removeComponent(cpt);
                continue;
            }

            // inject custom fields from the repository owner, if we have any
            auto injectedCustom = m_modInj->injectedCustomData(cid);
            if (injectedCustom.has_value()) {
                for (const auto &[key, value] : injectedCustom.value())
                    as_component_insert_custom_value(cpt, key.c_str(), value.c_str());
            }
        }

        if (as_component_get_merge_kind(cpt) != AS_MERGE_KIND_NONE)
            continue;

        auto pkgnames = as_component_get_pkgnames(cpt);
        if (!pkgnames || !pkgnames[0]) {
            // no packages are associated with this component

            if (ckind != AS_COMPONENT_KIND_WEB_APP && ckind != AS_COMPONENT_KIND_OPERATING_SYSTEM
                && ckind != AS_COMPONENT_KIND_REPOSITORY) {
                // this component is not allowed to have no installation candidate
                if (!as_component_has_bundle(cpt)) {
                    if (!gres.addHint(cpt, "no-install-candidate"))
                        continue;
                }
            }
        } else {
            // packages are associated with this component

            if (pkg->kind() == PackageKind::Fake) {
                // drop any association with the dummy package
                std::vector<std::string> filteredPkgnames;
                for (int j = 0; pkgnames[j] != nullptr; j++) {
                    if (std::string(pkgnames[j]) != EXTRA_METAINFO_FAKE_PKGNAME)
                        filteredPkgnames.push_back(pkgnames[j]);
                }

                // We only keep the C++ vector for memory management purposes,
                // as calling as_component_set_pkgnames() will free the originally
                // referenced data.
                g_autoptr(GStrvBuilder) builder = g_strv_builder_new();
                for (const auto &pkgname : filteredPkgnames)
                    g_strv_builder_add(builder, pkgname.c_str());
                g_auto(GStrv) filteredArray = g_strv_builder_end(builder);
                as_component_set_pkgnames(cpt, filteredArray);
            }
        }
    }

    // Without a component there can be no media, so the staging area we handed to the
    // result has nothing in it worth keeping around while the result is waiting to be
    // committed. Note that components may well have been dropped after their media were
    // created, in which case this discards those.
    if (gres.componentsCount() == 0)
        gres.clearMediaStaging();

    // clean up and return result
    pkg->finish();
    return gres;
}

} // namespace ASGenerator
