/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
 * vi:set noexpandtab tabstop=8 shiftwidth=8:
 *
 * Copyright (C) 2017-2020 Kalev Lember <klember@redhat.com>
 *
 * SPDX-License-Identifier: GPL-2.0+
 */

#include <config.h>

#include <gnome-software.h>

#include <fcntl.h>
#include <gio/gio.h>
#include <gio/gunixfdlist.h>
#include <glib/gstdio.h>
#include <libdnf/libdnf.h>
#include <ostree.h>
#include <rpm/rpmdb.h>
#include <rpm/rpmlib.h>
#include <rpm/rpmts.h>
#include <rpmostree.h>

#include "gs-rpmostree-generated.h"

/* This shows up in the `rpm-ostree status` as the software that
 * initiated the update.
 */
#define GS_RPMOSTREE_CLIENT_ID PACKAGE_NAME

G_DEFINE_AUTO_CLEANUP_FREE_FUNC(Header, headerFree, NULL)
G_DEFINE_AUTO_CLEANUP_FREE_FUNC(rpmts, rpmtsFree, NULL);
G_DEFINE_AUTO_CLEANUP_FREE_FUNC(rpmdbMatchIterator, rpmdbFreeIterator, NULL);

struct GsPluginData {
	GMutex			 mutex;
	GsRPMOSTreeOS		*os_proxy;
	GsRPMOSTreeSysroot	*sysroot_proxy;
	OstreeRepo		*ot_repo;
	OstreeSysroot		*ot_sysroot;
	DnfContext		*dnf_context;
	gboolean		 update_triggered;
};

void
gs_plugin_initialize (GsPlugin *plugin)
{
	GsPluginData *priv = gs_plugin_alloc_data (plugin, sizeof(GsPluginData));

	/* only works on OSTree */
	if (!g_file_test ("/run/ostree-booted", G_FILE_TEST_EXISTS)) {
		gs_plugin_set_enabled (plugin, FALSE);
		return;
	}

	g_mutex_init (&priv->mutex);

	/* open transaction */
	rpmReadConfigFiles (NULL, NULL);

	/* rpm-ostree is already a daemon with a DBus API; hence it makes
	 * more sense to use a custom plugin instead of using PackageKit.
	 */
	gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_CONFLICTS, "packagekit");
	gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_CONFLICTS, "packagekit-history");
	gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_CONFLICTS, "packagekit-local");
	gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_CONFLICTS, "packagekit-offline");
	gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_CONFLICTS, "packagekit-proxy");
	gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_CONFLICTS, "packagekit-refine");
	gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_CONFLICTS, "packagekit-refine-repos");
	gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_CONFLICTS, "packagekit-refresh");
	gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_CONFLICTS, "packagekit-upgrade");
	gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_CONFLICTS, "packagekit-url-to-app");
	gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_CONFLICTS, "systemd-updates");

	/* need pkgname */
	gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_AFTER, "appstream");
}

void
gs_plugin_destroy (GsPlugin *plugin)
{
	GsPluginData *priv = gs_plugin_get_data (plugin);
	if (priv->os_proxy != NULL)
		g_object_unref (priv->os_proxy);
	if (priv->sysroot_proxy != NULL)
		g_object_unref (priv->sysroot_proxy);
	if (priv->ot_sysroot != NULL)
		g_object_unref (priv->ot_sysroot);
	if (priv->ot_repo != NULL)
		g_object_unref (priv->ot_repo);
	if (priv->dnf_context != NULL)
		g_object_unref (priv->dnf_context);
	g_mutex_clear (&priv->mutex);
}

static void
gs_rpmostree_error_convert (GError **perror)
{
	GError *error = perror != NULL ? *perror : NULL;

	/* not set */
	if (error == NULL)
		return;

	/* parse remote RPM_OSTREED_ERROR */
	if (g_dbus_error_is_remote_error (error)) {
		g_autofree gchar *remote_error = g_dbus_error_get_remote_error (error);

		g_dbus_error_strip_remote_error (error);

		if (g_strcmp0 (remote_error, "org.projectatomic.rpmostreed.Error.NotAuthorized") == 0) {
			error->code = GS_PLUGIN_ERROR_NO_SECURITY;
		} else if (g_str_has_prefix (remote_error, "org.projectatomic.rpmostreed.Error")) {
			error->code = GS_PLUGIN_ERROR_FAILED;
		} else {
			g_warning ("can't reliably fixup remote error %s", remote_error);
			error->code = GS_PLUGIN_ERROR_FAILED;
		}
		error->domain = GS_PLUGIN_ERROR;
		return;
	}

	/* this are allowed for low-level errors */
	if (gs_utils_error_convert_gio (perror))
		return;

	/* this are allowed for low-level errors */
	if (gs_utils_error_convert_gdbus (perror))
		return;
}

gboolean
gs_plugin_setup (GsPlugin *plugin, GCancellable *cancellable, GError **error)
{
	GsPluginData *priv = gs_plugin_get_data (plugin);
	g_autoptr(GVariantBuilder) options_builder = NULL;

	/* Create a proxy for sysroot */
	if (priv->sysroot_proxy == NULL) {
		priv->sysroot_proxy = gs_rpmostree_sysroot_proxy_new_for_bus_sync (G_BUS_TYPE_SYSTEM,
		                                                                   G_DBUS_PROXY_FLAGS_NONE,
		                                                                   "org.projectatomic.rpmostree1",
		                                                                   "/org/projectatomic/rpmostree1/Sysroot",
		                                                                   cancellable,
		                                                                   error);
		if (priv->sysroot_proxy == NULL) {
			gs_rpmostree_error_convert (error);
			return FALSE;
		}
	}

	/* Create a proxy for currently booted OS */
	if (priv->os_proxy == NULL) {
		g_autofree gchar *os_object_path = NULL;

		os_object_path = gs_rpmostree_sysroot_dup_booted (priv->sysroot_proxy);
		if (os_object_path == NULL &&
		    !gs_rpmostree_sysroot_call_get_os_sync (priv->sysroot_proxy,
		                                            "",
		                                            &os_object_path,
		                                            cancellable,
		                                            error)) {
			gs_rpmostree_error_convert (error);
			return FALSE;
		}

		priv->os_proxy = gs_rpmostree_os_proxy_new_for_bus_sync (G_BUS_TYPE_SYSTEM,
		                                                         G_DBUS_PROXY_FLAGS_NONE,
		                                                         "org.projectatomic.rpmostree1",
		                                                         os_object_path,
		                                                         cancellable,
		                                                         error);
		if (priv->os_proxy == NULL) {
			gs_rpmostree_error_convert (error);
			return FALSE;
		}
	}

	options_builder = g_variant_builder_new (G_VARIANT_TYPE ("a{sv}"));
	g_variant_builder_add (options_builder, "{sv}", "id",
			       g_variant_new_string (GS_RPMOSTREE_CLIENT_ID));
	/* Register as a client so that the rpm-ostree daemon doesn't exit */
	if (!gs_rpmostree_sysroot_call_register_client_sync (priv->sysroot_proxy,
	                                                     g_variant_builder_end (options_builder),
	                                                     cancellable,
	                                                     error)) {
		gs_rpmostree_error_convert (error);
		return FALSE;
	}

	/* Load ostree sysroot and repo */
	if (priv->ot_sysroot == NULL) {
		g_autofree gchar *sysroot_path = NULL;
		g_autoptr(GFile) sysroot_file = NULL;

		sysroot_path = gs_rpmostree_sysroot_dup_path (priv->sysroot_proxy);
		sysroot_file = g_file_new_for_path (sysroot_path);

		priv->ot_sysroot = ostree_sysroot_new (sysroot_file);
		if (!ostree_sysroot_load (priv->ot_sysroot, cancellable, error)) {
			gs_rpmostree_error_convert (error);
			return FALSE;
		}

		if (!ostree_sysroot_get_repo (priv->ot_sysroot, &priv->ot_repo, cancellable, error)) {
			gs_rpmostree_error_convert (error);
			return FALSE;
		}
	}

	return TRUE;
}

static void
app_set_rpm_ostree_packaging_format (GsApp *app)
{
	gs_app_set_metadata (app, "GnomeSoftware::PackagingFormat", "RPM");
}

void
gs_plugin_adopt_app (GsPlugin *plugin, GsApp *app)
{
	if (gs_app_get_bundle_kind (app) == AS_BUNDLE_KIND_PACKAGE &&
	    gs_app_get_scope (app) == AS_APP_SCOPE_SYSTEM) {
		gs_app_set_management_plugin (app, gs_plugin_get_name (plugin));
		gs_app_add_quirk (app, GS_APP_QUIRK_NEEDS_REBOOT);
		app_set_rpm_ostree_packaging_format (app);
	}

	if (gs_app_get_kind (app) == AS_APP_KIND_OS_UPGRADE) {
		gs_app_set_management_plugin (app, gs_plugin_get_name (plugin));
		gs_app_add_quirk (app, GS_APP_QUIRK_NEEDS_REBOOT);
	}
}

typedef struct {
	GsPlugin *plugin;
	GError *error;
	GMainLoop *loop;
	GsApp *app;
	gboolean complete;
} TransactionProgress;

static TransactionProgress *
transaction_progress_new (void)
{
	TransactionProgress *self;

	self = g_slice_new0 (TransactionProgress);
	self->loop = g_main_loop_new (NULL, FALSE);

	return self;
}

static void
transaction_progress_free (TransactionProgress *self)
{
	g_clear_object (&self->plugin);
	g_clear_error (&self->error);
	g_main_loop_unref (self->loop);
	g_clear_object (&self->app);
	g_slice_free (TransactionProgress, self);
}

G_DEFINE_AUTOPTR_CLEANUP_FUNC(TransactionProgress, transaction_progress_free);

static void
transaction_progress_end (TransactionProgress *self)
{
	g_main_loop_quit (self->loop);
}

static void
on_transaction_progress (GDBusProxy *proxy,
                         gchar *sender_name,
                         gchar *signal_name,
                         GVariant *parameters,
                         gpointer user_data)
{
	TransactionProgress *tp = user_data;

	if (g_strcmp0 (signal_name, "PercentProgress") == 0) {
		const gchar *message = NULL;
		guint32 percentage;

		g_variant_get_child (parameters, 0, "&s", &message);
		g_variant_get_child (parameters, 1, "u", &percentage);
		g_debug ("PercentProgress: %u, %s\n", percentage, message);

		if (tp->app != NULL)
			gs_app_set_progress (tp->app, (guint) percentage);

		if (tp->app != NULL && tp->plugin != NULL) {
			GsPluginStatus plugin_status;

			switch (gs_app_get_state (tp->app)) {
			case AS_APP_STATE_INSTALLING:
				plugin_status = GS_PLUGIN_STATUS_INSTALLING;
				break;
			case AS_APP_STATE_REMOVING:
				plugin_status = GS_PLUGIN_STATUS_REMOVING;
				break;
			default:
				plugin_status = GS_PLUGIN_STATUS_DOWNLOADING;
				break;
			}
			gs_plugin_status_update (tp->plugin, tp->app, plugin_status);
		}
	} else if (g_strcmp0 (signal_name, "Finished") == 0) {
		if (tp->error == NULL) {
			g_autofree gchar *error_message = NULL;
			gboolean success = FALSE;

			g_variant_get (parameters, "(bs)", &success, &error_message);

			if (!success) {
				tp->error = g_dbus_error_new_for_dbus_error ("org.projectatomic.rpmostreed.Error.Failed",
				                                             error_message);
			}
		}

		transaction_progress_end (tp);
	}
}

static void
cancelled_handler (GCancellable *cancellable,
                   gpointer user_data)
{
	GsRPMOSTreeTransaction *transaction = user_data;
	gs_rpmostree_transaction_call_cancel_sync (transaction, NULL, NULL);
}

static gboolean
gs_rpmostree_transaction_get_response_sync (GsRPMOSTreeSysroot *sysroot_proxy,
                                            const gchar *transaction_address,
                                            TransactionProgress *tp,
                                            GCancellable *cancellable,
                                            GError **error)
{
	GsRPMOSTreeTransaction *transaction = NULL;
	g_autoptr(GDBusConnection) peer_connection = NULL;
	gint cancel_handler;
	gulong signal_handler = 0;
	gboolean success = FALSE;
	gboolean just_started = FALSE;

	peer_connection = g_dbus_connection_new_for_address_sync (transaction_address,
	                                                          G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT,
	                                                          NULL,
	                                                          cancellable,
	                                                          error);

	if (peer_connection == NULL)
		goto out;

	transaction = gs_rpmostree_transaction_proxy_new_sync (peer_connection,
	                                                       G_DBUS_PROXY_FLAGS_NONE,
	                                                       NULL,
	                                                       "/",
	                                                       cancellable,
	                                                       error);
	if (transaction == NULL)
		goto out;

	/* setup cancel handler */
	cancel_handler = g_cancellable_connect (cancellable,
	                                        G_CALLBACK (cancelled_handler),
	                                        transaction, NULL);

	signal_handler = g_signal_connect (transaction, "g-signal",
	                                   G_CALLBACK (on_transaction_progress),
	                                   tp);

	/* Tell the server we're ready to receive signals. */
	if (!gs_rpmostree_transaction_call_start_sync (transaction,
	                                               &just_started,
	                                               cancellable,
	                                               error))
		goto out;

	g_main_loop_run (tp->loop);

	g_cancellable_disconnect (cancellable, cancel_handler);

	if (!g_cancellable_set_error_if_cancelled (cancellable, error)) {
		if (tp->error) {
			g_propagate_error (error, g_steal_pointer (&tp->error));
		} else {
			success = TRUE;
		}
	}

out:
	if (signal_handler)
		g_signal_handler_disconnect (transaction, signal_handler);
	if (transaction != NULL)
		g_object_unref (transaction);

	return success;
}

static GsApp *
app_from_modified_pkg_variant (GsPlugin *plugin, GVariant *variant)
{
	g_autoptr(GsApp) app = NULL;
	const char *name;
	const char *old_evr, *old_arch;
	const char *new_evr, *new_arch;
	g_autofree char *old_nevra = NULL;
	g_autofree char *new_nevra = NULL;

	g_variant_get (variant, "(us(ss)(ss))", NULL /* type*/, &name, &old_evr, &old_arch, &new_evr, &new_arch);
	old_nevra = g_strdup_printf ("%s-%s-%s", name, old_evr, old_arch);
	new_nevra = g_strdup_printf ("%s-%s-%s", name, new_evr, new_arch);

	app = gs_plugin_cache_lookup (plugin, old_nevra);
	if (app != NULL)
		return g_steal_pointer (&app);

	/* create new app */
	app = gs_app_new (NULL);
	gs_app_set_management_plugin (app, "rpm-ostree");
	gs_app_add_quirk (app, GS_APP_QUIRK_NEEDS_REBOOT);
	app_set_rpm_ostree_packaging_format (app);
	gs_app_set_size_download (app, 0);
	gs_app_set_kind (app, AS_APP_KIND_GENERIC);
	gs_app_set_bundle_kind (app, AS_BUNDLE_KIND_PACKAGE);
	gs_app_set_scope (app, AS_APP_SCOPE_SYSTEM);

	/* update or downgrade */
	gs_app_add_source (app, name);
	gs_app_set_version (app, old_evr);
	gs_app_set_update_version (app, new_evr);
	gs_app_set_state (app, AS_APP_STATE_UPDATABLE);

	g_debug ("!%s\n", old_nevra);
	g_debug ("=%s\n", new_nevra);

	gs_plugin_cache_add (plugin, old_nevra, app);
	return g_steal_pointer (&app);
}

static GsApp *
app_from_single_pkg_variant (GsPlugin *plugin, GVariant *variant, gboolean addition)
{
	g_autoptr(GsApp) app = NULL;
	const char *name;
	const char *evr;
	const char *arch;
	g_autofree char *nevra = NULL;

	g_variant_get (variant, "(usss)", NULL /* type*/, &name, &evr, &arch);
	nevra = g_strdup_printf ("%s-%s-%s", name, evr, arch);

	app = gs_plugin_cache_lookup (plugin, nevra);
	if (app != NULL)
		return g_steal_pointer (&app);

	/* create new app */
	app = gs_app_new (NULL);
	gs_app_set_management_plugin (app, "rpm-ostree");
	gs_app_add_quirk (app, GS_APP_QUIRK_NEEDS_REBOOT);
	app_set_rpm_ostree_packaging_format (app);
	gs_app_set_size_download (app, 0);
	gs_app_set_kind (app, AS_APP_KIND_GENERIC);
	gs_app_set_bundle_kind (app, AS_BUNDLE_KIND_PACKAGE);
	gs_app_set_scope (app, AS_APP_SCOPE_SYSTEM);

	if (addition) {
		/* addition */
		gs_app_add_source (app, name);
		gs_app_set_version (app, evr);
		gs_app_set_state (app, AS_APP_STATE_AVAILABLE);

		g_debug ("+%s\n", nevra);
	} else {
		/* removal */
		gs_app_add_source (app, name);
		gs_app_set_version (app, evr);
		gs_app_set_state (app, AS_APP_STATE_UNAVAILABLE);

		g_debug ("-%s\n", nevra);
	}

	gs_plugin_cache_add (plugin, nevra, app);
	return g_steal_pointer (&app);
}

static GVariant *
make_rpmostree_options_variant (gboolean reboot,
                                gboolean allow_downgrade,
                                gboolean cache_only,
                                gboolean download_only,
                                gboolean skip_purge,
                                gboolean no_pull_base,
                                gboolean dry_run,
                                gboolean no_overrides)
{
	GVariantDict dict;
	g_variant_dict_init (&dict, NULL);
	g_variant_dict_insert (&dict, "reboot", "b", reboot);
	g_variant_dict_insert (&dict, "allow-downgrade", "b", allow_downgrade);
	g_variant_dict_insert (&dict, "cache-only", "b", cache_only);
	g_variant_dict_insert (&dict, "download-only", "b", download_only);
	g_variant_dict_insert (&dict, "skip-purge", "b", skip_purge);
	g_variant_dict_insert (&dict, "no-pull-base", "b", no_pull_base);
	g_variant_dict_insert (&dict, "dry-run", "b", dry_run);
	g_variant_dict_insert (&dict, "no-overrides", "b", no_overrides);
	return g_variant_ref_sink (g_variant_dict_end (&dict));
}

static GVariant *
make_refresh_md_options_variant (gboolean force)
{
	GVariantDict dict;
	g_variant_dict_init (&dict, NULL);
	g_variant_dict_insert (&dict, "force", "b", force);
	return g_variant_ref_sink (g_variant_dict_end (&dict));
}

static gboolean
make_rpmostree_modifiers_variant (const char *install_package,
                                  const char *uninstall_package,
                                  const char *install_local_package,
                                  GVariant **out_modifiers,
                                  GUnixFDList **out_fd_list,
                                  GError **error)
{
	GVariantDict dict;
	g_autoptr(GUnixFDList) fd_list = g_unix_fd_list_new ();

	g_variant_dict_init (&dict, NULL);

	if (install_package != NULL) {
		g_autoptr(GPtrArray) repo_pkgs = g_ptr_array_new ();

		g_ptr_array_add (repo_pkgs, install_package);

		g_variant_dict_insert_value (&dict, "install-packages",
		                             g_variant_new_strv ((const char *const*)repo_pkgs->pdata,
		                             repo_pkgs->len));

	}

	if (uninstall_package != NULL) {
		g_autoptr(GPtrArray) repo_pkgs = g_ptr_array_new ();

		g_ptr_array_add (repo_pkgs, uninstall_package);

		g_variant_dict_insert_value (&dict, "uninstall-packages",
		                             g_variant_new_strv ((const char *const*)repo_pkgs->pdata,
		                             repo_pkgs->len));

	}

	if (install_local_package != NULL) {
		g_auto(GVariantBuilder) builder;
		int fd;
		int idx;

		g_variant_builder_init (&builder, G_VARIANT_TYPE ("ah"));

		fd = openat (AT_FDCWD, install_local_package, O_RDONLY | O_CLOEXEC | O_NOCTTY);
		if (fd == -1) {
			g_set_error (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_FAILED,
			             "Failed to open %s", install_local_package);
			return FALSE;
		}

		idx = g_unix_fd_list_append (fd_list, fd, error);
		if (idx < 0) {
			close (fd);
			return FALSE;
		}

		g_variant_builder_add (&builder, "h", idx);
		g_variant_dict_insert_value (&dict, "install-local-packages",
		                             g_variant_new ("ah", &builder));
		close (fd);
	}

	*out_fd_list = g_steal_pointer (&fd_list);
	*out_modifiers = g_variant_ref_sink (g_variant_dict_end (&dict));
	return TRUE;
}

static gboolean
rpmostree_update_deployment (GsRPMOSTreeOS *os_proxy,
                             const char *install_package,
                             const char *uninstall_package,
                             const char *install_local_package,
                             GVariant *options,
                             char **out_transaction_address,
                             GCancellable *cancellable,
                             GError **error)
{
	g_autoptr(GUnixFDList) fd_list = NULL;
	g_autoptr(GVariant) modifiers = NULL;

	if (!make_rpmostree_modifiers_variant (install_package,
	                                       uninstall_package,
	                                       install_local_package,
	                                       &modifiers, &fd_list, error))
		return FALSE;

	return gs_rpmostree_os_call_update_deployment_sync (os_proxy,
	                                                    modifiers,
	                                                    options,
	                                                    fd_list,
	                                                    out_transaction_address,
	                                                    NULL,
	                                                    cancellable,
	                                                    error);
}

#define RPMOSTREE_CORE_CACHEDIR "/var/cache/rpm-ostree/"
#define RPMOSTREE_DIR_CACHE_REPOMD "repomd"
#define RPMOSTREE_DIR_CACHE_SOLV "solv"

static gboolean
ensure_rpmostree_dnf_context (GsPlugin *plugin, GCancellable *cancellable, GError **error)
{
	GsPluginData *priv = gs_plugin_get_data (plugin);
	g_autofree gchar *transaction_address = NULL;
	g_autoptr(GsApp) progress_app = gs_app_new (gs_plugin_get_name (plugin));
	g_autoptr(DnfContext) context = dnf_context_new ();
	g_autoptr(DnfState) state = dnf_state_new ();
	g_autoptr(GVariant) options = NULL;
	g_autoptr(TransactionProgress) tp = transaction_progress_new ();

	if (priv->dnf_context != NULL)
		return TRUE;

	tp->app = g_object_ref (progress_app);
	tp->plugin = g_object_ref (plugin);

	dnf_context_set_repo_dir (context, "/etc/yum.repos.d");
	dnf_context_set_cache_dir (context, RPMOSTREE_CORE_CACHEDIR RPMOSTREE_DIR_CACHE_REPOMD);
	dnf_context_set_solv_dir (context, RPMOSTREE_CORE_CACHEDIR RPMOSTREE_DIR_CACHE_SOLV);
	dnf_context_set_cache_age (context, G_MAXUINT);
	dnf_context_set_enable_filelists (context, FALSE);

	options = make_refresh_md_options_variant (FALSE /* force */);
	if (!gs_rpmostree_os_call_refresh_md_sync (priv->os_proxy,
	                                           options,
	                                           &transaction_address,
	                                           cancellable,
	                                           error)) {
		gs_rpmostree_error_convert (error);
		return FALSE;
	}

	if (!gs_rpmostree_transaction_get_response_sync (priv->sysroot_proxy,
	                                                 transaction_address,
	                                                 tp,
	                                                 cancellable,
	                                                 error)) {
		gs_rpmostree_error_convert (error);
		return FALSE;
	}

	if (!dnf_context_setup (context, cancellable, error)) {
		gs_rpmostree_error_convert (error);
		return FALSE;
	}

	if (!dnf_context_setup_sack_with_flags (context, state, DNF_CONTEXT_SETUP_SACK_FLAG_SKIP_RPMDB, error)) {
		gs_rpmostree_error_convert (error);
		return FALSE;
	}

	g_set_object (&priv->dnf_context, context);
	return TRUE;
}

gboolean
gs_plugin_refresh (GsPlugin *plugin,
                   guint cache_age,
                   GCancellable *cancellable,
                   GError **error)
{
	GsPluginData *priv = gs_plugin_get_data (plugin);
	g_autoptr(GMutexLocker) locker = NULL;

	locker = g_mutex_locker_new (&priv->mutex);

	if (!ensure_rpmostree_dnf_context (plugin, cancellable, error))
		return FALSE;

	if (cache_age == G_MAXUINT)
		return TRUE;

	{
		g_autofree gchar *transaction_address = NULL;
		g_autoptr(GsApp) progress_app = gs_app_new (gs_plugin_get_name (plugin));
		g_autoptr(GVariant) options = NULL;
		g_autoptr(TransactionProgress) tp = transaction_progress_new ();

		tp->app = g_object_ref (progress_app);
		tp->plugin = g_object_ref (plugin);

		options = make_rpmostree_options_variant (FALSE,  /* reboot */
		                                          FALSE,  /* allow-downgrade */
		                                          FALSE,  /* cache-only */
		                                          TRUE,   /* download-only */
		                                          FALSE,  /* skip-purge */
		                                          FALSE,  /* no-pull-base */
		                                          FALSE,  /* dry-run */
		                                          FALSE); /* no-overrides */
		if (!gs_rpmostree_os_call_upgrade_sync (priv->os_proxy,
		                                        options,
		                                        NULL /* fd list */,
		                                        &transaction_address,
		                                        NULL /* fd list out */,
		                                        cancellable,
		                                        error)) {
			gs_rpmostree_error_convert (error);
			return FALSE;
		}

		if (!gs_rpmostree_transaction_get_response_sync (priv->sysroot_proxy,
		                                                 transaction_address,
		                                                 tp,
		                                                 cancellable,
		                                                 error)) {
			gs_rpmostree_error_convert (error);
			return FALSE;
		}
	}

	{
		g_autofree gchar *transaction_address = NULL;
		g_autoptr(GsApp) progress_app = gs_app_new (gs_plugin_get_name (plugin));
		g_autoptr(GVariant) options = NULL;
		GVariantDict dict;
		g_autoptr(TransactionProgress) tp = transaction_progress_new ();

		tp->app = g_object_ref (progress_app);
		tp->plugin = g_object_ref (plugin);

		g_variant_dict_init (&dict, NULL);
		g_variant_dict_insert (&dict, "mode", "s", "check");
		options = g_variant_ref_sink (g_variant_dict_end (&dict));

		if (!gs_rpmostree_os_call_automatic_update_trigger_sync (priv->os_proxy,
		                                                         options,
		                                                         NULL,
		                                                         &transaction_address,
		                                                         cancellable,
		                                                         error)) {
			gs_rpmostree_error_convert (error);
			return FALSE;
		}

		if (!gs_rpmostree_transaction_get_response_sync (priv->sysroot_proxy,
		                                                 transaction_address,
		                                                 tp,
		                                                 cancellable,
		                                                 error)) {
			gs_rpmostree_error_convert (error);
			return FALSE;
		}
	}

	/* update UI */
	gs_plugin_updates_changed (plugin);

	return TRUE;
}

gboolean
gs_plugin_add_updates (GsPlugin *plugin,
		       GsAppList *list,
		       GCancellable *cancellable,
		       GError **error)
{
	GsPluginData *priv = gs_plugin_get_data (plugin);
	g_autoptr(GVariant) cached_update = NULL;
	g_autoptr(GVariant) rpm_diff = NULL;
	const gchar *checksum = NULL;
	const gchar *version = NULL;
	g_auto(GVariantDict) cached_update_dict;

	/* ensure D-Bus properties are updated before reading them */
	if (!gs_rpmostree_sysroot_call_reload_sync (priv->sysroot_proxy, cancellable, error)) {
		gs_rpmostree_error_convert (error);
		return FALSE;
	}

	cached_update = gs_rpmostree_os_dup_cached_update (priv->os_proxy);
	g_variant_dict_init (&cached_update_dict, cached_update);

	if (!g_variant_dict_lookup (&cached_update_dict, "checksum", "&s", &checksum))
		return TRUE;
	if (!g_variant_dict_lookup (&cached_update_dict, "version", "&s", &version))
		return TRUE;

	g_debug ("got CachedUpdate version '%s', checksum '%s'", version, checksum);

	rpm_diff = g_variant_dict_lookup_value (&cached_update_dict, "rpm-diff", G_VARIANT_TYPE ("a{sv}"));
	if (rpm_diff != NULL) {
		GVariantIter iter;
		GVariant *child;
		g_autoptr(GVariant) upgraded = NULL;
		g_autoptr(GVariant) downgraded = NULL;
		g_autoptr(GVariant) removed = NULL;
		g_autoptr(GVariant) added = NULL;
		g_auto(GVariantDict) rpm_diff_dict;
		g_variant_dict_init (&rpm_diff_dict, rpm_diff);

		upgraded = g_variant_dict_lookup_value (&rpm_diff_dict, "upgraded", G_VARIANT_TYPE ("a(us(ss)(ss))"));
		if (upgraded == NULL) {
			g_set_error_literal (error,
			                     GS_PLUGIN_ERROR,
			                     GS_PLUGIN_ERROR_INVALID_FORMAT,
			                     "no 'upgraded' in rpm-diff dict");
			return FALSE;
		}
		downgraded = g_variant_dict_lookup_value (&rpm_diff_dict, "downgraded", G_VARIANT_TYPE ("a(us(ss)(ss))"));
		if (downgraded == NULL) {
			g_set_error_literal (error,
			                     GS_PLUGIN_ERROR,
			                     GS_PLUGIN_ERROR_INVALID_FORMAT,
			                     "no 'downgraded' in rpm-diff dict");
			return FALSE;
		}
		removed = g_variant_dict_lookup_value (&rpm_diff_dict, "removed", G_VARIANT_TYPE ("a(usss)"));
		if (removed == NULL) {
			g_set_error_literal (error,
			                     GS_PLUGIN_ERROR,
			                     GS_PLUGIN_ERROR_INVALID_FORMAT,
			                     "no 'removed' in rpm-diff dict");
			return FALSE;
		}
		added = g_variant_dict_lookup_value (&rpm_diff_dict, "added", G_VARIANT_TYPE ("a(usss)"));
		if (added == NULL) {
			g_set_error_literal (error,
			                     GS_PLUGIN_ERROR,
			                     GS_PLUGIN_ERROR_INVALID_FORMAT,
			                     "no 'added' in rpm-diff dict");
			return FALSE;
		}

		/* iterate over all upgraded packages and add them */
		g_variant_iter_init (&iter, upgraded);
		while ((child = g_variant_iter_next_value (&iter)) != NULL) {
			g_autoptr(GsApp) app = app_from_modified_pkg_variant (plugin, child);
			if (app != NULL)
				gs_app_list_add (list, app);
			g_variant_unref (child);
		}

		/* iterate over all downgraded packages and add them */
		g_variant_iter_init (&iter, downgraded);
		while ((child = g_variant_iter_next_value (&iter)) != NULL) {
			g_autoptr(GsApp) app = app_from_modified_pkg_variant (plugin, child);
			if (app != NULL)
				gs_app_list_add (list, app);
			g_variant_unref (child);
		}

		/* iterate over all removed packages and add them */
		g_variant_iter_init (&iter, removed);
		while ((child = g_variant_iter_next_value (&iter)) != NULL) {
			g_autoptr(GsApp) app = app_from_single_pkg_variant (plugin, child, FALSE);
			if (app != NULL)
				gs_app_list_add (list, app);
			g_variant_unref (child);
		}

		/* iterate over all added packages and add them */
		g_variant_iter_init (&iter, added);
		while ((child = g_variant_iter_next_value (&iter)) != NULL) {
			g_autoptr(GsApp) app = app_from_single_pkg_variant (plugin, child, TRUE);
			if (app != NULL)
				gs_app_list_add (list, app);
			g_variant_unref (child);
		}
	}

	return TRUE;
}

static gboolean
trigger_rpmostree_update (GsPlugin *plugin,
                          GsApp *app,
                          GCancellable *cancellable,
                          GError **error)
{
	GsPluginData *priv = gs_plugin_get_data (plugin);
	g_autofree gchar *transaction_address = NULL;
	g_autoptr(GVariant) options = NULL;
	g_autoptr(TransactionProgress) tp = transaction_progress_new ();

	/* if we can process this online do not require a trigger */
	if (gs_app_get_state (app) != AS_APP_STATE_UPDATABLE)
		return TRUE;

	/* only process this app if was created by this plugin */
	if (g_strcmp0 (gs_app_get_management_plugin (app), gs_plugin_get_name (plugin)) != 0)
		return TRUE;

	/* already in correct state */
	if (priv->update_triggered)
		return TRUE;

	/* trigger the update */
	options = make_rpmostree_options_variant (FALSE,  /* reboot */
	                                          FALSE,  /* allow-downgrade */
	                                          TRUE,   /* cache-only */
	                                          FALSE,  /* download-only */
	                                          FALSE,  /* skip-purge */
	                                          FALSE,  /* no-pull-base */
	                                          FALSE,  /* dry-run */
	                                          FALSE); /* no-overrides */
	if (!gs_rpmostree_os_call_upgrade_sync (priv->os_proxy,
	                                        options,
	                                        NULL /* fd list */,
	                                        &transaction_address,
	                                        NULL /* fd list out */,
	                                        cancellable,
	                                        error)) {
		gs_rpmostree_error_convert (error);
		return FALSE;
	}

	if (!gs_rpmostree_transaction_get_response_sync (priv->sysroot_proxy,
	                                                 transaction_address,
	                                                 tp,
	                                                 cancellable,
	                                                 error)) {
		gs_rpmostree_error_convert (error);
		return FALSE;
	}

	priv->update_triggered = TRUE;

	/* success */
	return TRUE;
}

gboolean
gs_plugin_update_app (GsPlugin *plugin,
                      GsApp *app,
                      GCancellable *cancellable,
                      GError **error)
{
	GsAppList *related = gs_app_get_related (app);

	/* we don't currently don't put all updates in the OsUpdate proxy app */
	if (!gs_app_has_quirk (app, GS_APP_QUIRK_IS_PROXY))
		return trigger_rpmostree_update (plugin, app, cancellable, error);

	/* try to trigger each related app */
	for (guint i = 0; i < gs_app_list_length (related); i++) {
		GsApp *app_tmp = gs_app_list_index (related, i);
		if (!trigger_rpmostree_update (plugin, app_tmp, cancellable, error))
			return FALSE;
	}

	/* success */
	return TRUE;
}

gboolean
gs_plugin_app_upgrade_trigger (GsPlugin *plugin,
                               GsApp *app,
                               GCancellable *cancellable,
                               GError **error)
{
	GsPluginData *priv = gs_plugin_get_data (plugin);
	const char *packages[] = { NULL };
	g_autofree gchar *new_refspec = NULL;
	g_autofree gchar *transaction_address = NULL;
	g_autoptr(GVariant) options = NULL;
	g_autoptr(TransactionProgress) tp = transaction_progress_new ();

	/* only process this app if was created by this plugin */
	if (g_strcmp0 (gs_app_get_management_plugin (app), gs_plugin_get_name (plugin)) != 0)
		return TRUE;

	/* check is distro-upgrade */
	if (gs_app_get_kind (app) != AS_APP_KIND_OS_UPGRADE)
		return TRUE;

	/* construct new refspec based on the distro version we're upgrading to */
	new_refspec = g_strdup_printf ("ostree://fedora/%s/x86_64/silverblue",
	                               gs_app_get_version (app));

	/* trigger the upgrade */
	options = make_rpmostree_options_variant (FALSE,  /* reboot */
	                                          TRUE,   /* allow-downgrade */
	                                          TRUE,   /* cache-only */
	                                          FALSE,  /* download-only */
	                                          FALSE,  /* skip-purge */
	                                          FALSE,  /* no-pull-base */
	                                          FALSE,  /* dry-run */
	                                          FALSE); /* no-overrides */

	if (!gs_rpmostree_os_call_rebase_sync (priv->os_proxy,
	                                       options,
	                                       new_refspec,
	                                       packages,
	                                       NULL /* fd list */,
	                                       &transaction_address,
	                                       NULL /* fd list out */,
	                                       cancellable,
	                                       error)) {
		gs_rpmostree_error_convert (error);
		return FALSE;
	}

	if (!gs_rpmostree_transaction_get_response_sync (priv->sysroot_proxy,
	                                                 transaction_address,
	                                                 tp,
	                                                 cancellable,
	                                                 error)) {
		gs_rpmostree_error_convert (error);

		if (g_strrstr ((*error)->message, "Old and new refs are equal")) {
			/* don't error out if the correct tree is already deployed */
			g_debug ("ignoring rpm-ostree error: %s", (*error)->message);
			g_clear_error (error);
		} else {
			return FALSE;
		}
	}

	/* success */
	return TRUE;
}

static gboolean
gs_plugin_repo_enable (GsPlugin *plugin,
                       GsApp *app,
                       gboolean enable,
                       GCancellable *cancellable,
                       GError **error)
{
	GsPluginData *priv = gs_plugin_get_data (plugin);
	g_autofree gchar *transaction_address = NULL;
	g_autoptr(GVariantBuilder) options_builder = NULL;
	g_autoptr(TransactionProgress) tp = NULL;

	if (enable)
		gs_app_set_state (app, AS_APP_STATE_INSTALLING);
	else
		gs_app_set_state (app, AS_APP_STATE_REMOVING);

	options_builder = g_variant_builder_new (G_VARIANT_TYPE ("a{ss}"));
	g_variant_builder_add (options_builder, "{ss}", "enabled", enable ? "1" : "0");
	if (!gs_rpmostree_os_call_modify_yum_repo_sync (priv->os_proxy,
	                                                gs_app_get_id (app),
	                                                g_variant_builder_end (options_builder),
	                                                &transaction_address,
	                                                cancellable,
	                                                error)) {
		gs_rpmostree_error_convert (error);
		gs_app_set_state_recover (app);
		gs_utils_error_add_origin_id (error, app);
		return FALSE;
	}

	tp = transaction_progress_new ();
	tp->app = g_object_ref (app);
	if (!gs_rpmostree_transaction_get_response_sync (priv->sysroot_proxy,
	                                                 transaction_address,
	                                                 tp,
	                                                 cancellable,
	                                                 error)) {
		gs_rpmostree_error_convert (error);
		gs_app_set_state_recover (app);
		gs_utils_error_add_origin_id (error, app);
		return FALSE;
	}


	/* state is known */
	if (enable)
		gs_app_set_state (app, AS_APP_STATE_INSTALLED);
	else
		gs_app_set_state (app, AS_APP_STATE_AVAILABLE);

	return TRUE;
}

gboolean
gs_plugin_app_install (GsPlugin *plugin,
                       GsApp *app,
                       GCancellable *cancellable,
                       GError **error)
{
	GsPluginData *priv = gs_plugin_get_data (plugin);
	const gchar *install_package = NULL;
	g_autofree gchar *local_filename = NULL;
	g_autofree gchar *transaction_address = NULL;
	g_autoptr(GVariant) options = NULL;
	g_autoptr(TransactionProgress) tp = transaction_progress_new ();

	/* only process this app if was created by this plugin */
	if (g_strcmp0 (gs_app_get_management_plugin (app), gs_plugin_get_name (plugin)) != 0)
		return TRUE;

	/* enable repo */
	if (gs_app_get_kind (app) == AS_APP_KIND_SOURCE)
		return gs_plugin_repo_enable (plugin, app, TRUE, cancellable, error);

	switch (gs_app_get_state (app)) {
	case AS_APP_STATE_AVAILABLE:
		if (gs_app_get_source_default (app) == NULL) {
			g_set_error_literal (error,
			                     GS_PLUGIN_ERROR,
			                     GS_PLUGIN_ERROR_NOT_SUPPORTED,
			                     "no source set");
			return FALSE;
		}

		install_package = gs_app_get_source_default (app);
		break;
	case AS_APP_STATE_AVAILABLE_LOCAL:
		if (gs_app_get_local_file (app) == NULL) {
			g_set_error_literal (error,
			                     GS_PLUGIN_ERROR,
			                     GS_PLUGIN_ERROR_NOT_SUPPORTED,
			                     "local package, but no filename");
			return FALSE;
		}

		local_filename = g_file_get_path (gs_app_get_local_file (app));
		break;
	default:
		g_set_error (error,
		             GS_PLUGIN_ERROR,
		             GS_PLUGIN_ERROR_NOT_SUPPORTED,
		             "do not know how to install app in state %s",
		             as_app_state_to_string (gs_app_get_state (app)));
		return FALSE;
	}

	gs_app_set_state (app, AS_APP_STATE_INSTALLING);
	tp->app = g_object_ref (app);

	options = make_rpmostree_options_variant (FALSE,  /* reboot */
	                                          FALSE,  /* allow-downgrade */
	                                          FALSE,   /* cache-only */
	                                          FALSE,  /* download-only */
	                                          FALSE,  /* skip-purge */
	                                          TRUE,  /* no-pull-base */
	                                          FALSE,  /* dry-run */
	                                          FALSE); /* no-overrides */

	if (!rpmostree_update_deployment (priv->os_proxy,
	                                  install_package,
	                                  NULL /* remove package */,
	                                  local_filename,
	                                  options,
	                                  &transaction_address,
	                                  cancellable,
	                                  error)) {
		gs_rpmostree_error_convert (error);
		gs_app_set_state_recover (app);
		return FALSE;
	}

	if (!gs_rpmostree_transaction_get_response_sync (priv->sysroot_proxy,
	                                                 transaction_address,
	                                                 tp,
	                                                 cancellable,
	                                                 error)) {
		gs_rpmostree_error_convert (error);
		gs_app_set_state_recover (app);
		return FALSE;
	}

	/* state is known */
	gs_app_set_state (app, AS_APP_STATE_INSTALLED);

	/* get the new icon from the package */
	gs_app_set_local_file (app, NULL);
	gs_app_add_icon (app, NULL);
	gs_app_set_pixbuf (app, NULL);

	/* no longer valid */
	gs_app_clear_source_ids (app);

	return TRUE;
}

gboolean
gs_plugin_app_remove (GsPlugin *plugin,
                      GsApp *app,
                      GCancellable *cancellable,
                      GError **error)
{
	GsPluginData *priv = gs_plugin_get_data (plugin);
	g_autofree gchar *transaction_address = NULL;
	g_autoptr(GVariant) options = NULL;
	g_autoptr(TransactionProgress) tp = transaction_progress_new ();

	/* only process this app if was created by this plugin */
	if (g_strcmp0 (gs_app_get_management_plugin (app), gs_plugin_get_name (plugin)) != 0)
		return TRUE;

	/* disable repo */
	if (gs_app_get_kind (app) == AS_APP_KIND_SOURCE)
		return gs_plugin_repo_enable (plugin, app, FALSE, cancellable, error);

	gs_app_set_state (app, AS_APP_STATE_REMOVING);
	tp->app = g_object_ref (app);

	options = make_rpmostree_options_variant (FALSE,  /* reboot */
	                                          FALSE,  /* allow-downgrade */
	                                          TRUE,   /* cache-only */
	                                          FALSE,  /* download-only */
	                                          FALSE,  /* skip-purge */
	                                          TRUE,  /* no-pull-base */
	                                          FALSE,  /* dry-run */
	                                          FALSE); /* no-overrides */

	if (!rpmostree_update_deployment (priv->os_proxy,
	                                  NULL /* install package */,
	                                  gs_app_get_source_default (app),
	                                  NULL /* install local package */,
	                                  options,
	                                  &transaction_address,
	                                  cancellable,
	                                  error)) {
		gs_rpmostree_error_convert (error);
		gs_app_set_state_recover (app);
		return FALSE;
	}

	if (!gs_rpmostree_transaction_get_response_sync (priv->sysroot_proxy,
	                                                 transaction_address,
	                                                 tp,
	                                                 cancellable,
	                                                 error)) {
		gs_rpmostree_error_convert (error);
		gs_app_set_state_recover (app);
		return FALSE;
	}

	/* state is not known: we don't know if we can re-install this app */
	gs_app_set_state (app, AS_APP_STATE_UNKNOWN);

	return TRUE;
}

static DnfPackage *
find_package_by_name (DnfSack     *sack,
                      const char  *pkgname)
{
	g_autoptr(GPtrArray) pkgs = NULL;
	hy_autoquery HyQuery query = hy_query_create (sack);

	hy_query_filter (query, HY_PKG_NAME, HY_EQ, pkgname);
	hy_query_filter_latest_per_arch (query, TRUE);

	pkgs = hy_query_run (query);
	if (pkgs->len == 0)
		return NULL;

	return g_object_ref (pkgs->pdata[pkgs->len-1]);
}

static GPtrArray *
find_packages_by_provides (DnfSack *sack,
                           gchar **search)
{
	g_autoptr(GPtrArray) pkgs = NULL;
	hy_autoquery HyQuery query = hy_query_create (sack);

	hy_query_filter_provides_in (query, search);
	hy_query_filter_latest_per_arch (query, TRUE);

	pkgs = hy_query_run (query);

	return g_steal_pointer (&pkgs);
}

static gboolean
resolve_installed_packages_app (GsPlugin *plugin,
                                GPtrArray *pkglist,
                                gchar **layered_packages,
                                gchar **layered_local_packages,
                                GsApp *app)
{
	for (guint i = 0; i < pkglist->len; i++) {
		RpmOstreePackage *pkg = g_ptr_array_index (pkglist, i);
		if (g_strcmp0 (rpm_ostree_package_get_name (pkg), gs_app_get_source_default (app)) == 0) {
			gs_app_set_version (app, rpm_ostree_package_get_evr (pkg));
			if (gs_app_get_state (app) == AS_APP_STATE_UNKNOWN)
				gs_app_set_state (app, AS_APP_STATE_INSTALLED);
			if (g_strv_contains ((const gchar * const *) layered_packages,
			                     rpm_ostree_package_get_name (pkg)) ||
			    g_strv_contains ((const gchar * const *) layered_local_packages,
			                     rpm_ostree_package_get_nevra (pkg))) {
				/* layered packages can always be removed */
				gs_app_remove_quirk (app, GS_APP_QUIRK_COMPULSORY);
			} else {
				/* can't remove packages that are part of the base system */
				gs_app_add_quirk (app, GS_APP_QUIRK_COMPULSORY);
			}
			if (gs_app_get_origin (app) == NULL)
				gs_app_set_origin (app, "rpm-ostree");
			return TRUE /* found */;
		}
	}

	return FALSE /* not found */;
}

static gboolean
resolve_available_packages_app (GsPlugin *plugin,
                                DnfSack *sack,
                                GsApp *app)
{
	g_autoptr(DnfPackage) pkg = NULL;

	pkg = find_package_by_name (sack, gs_app_get_source_default (app));
	if (pkg != NULL) {
		gs_app_set_version (app, dnf_package_get_evr (pkg));
		if (gs_app_get_state (app) == AS_APP_STATE_UNKNOWN)
			gs_app_set_state (app, AS_APP_STATE_AVAILABLE);

		/* anything not part of the base system can be removed */
		gs_app_remove_quirk (app, GS_APP_QUIRK_COMPULSORY);

		/* set origin */
		if (gs_app_get_origin (app) == NULL) {
			const gchar *reponame = dnf_package_get_reponame (pkg);
			gs_app_set_origin (app, reponame);
		}

		/* set more metadata for packages that don't have appstream data */
		gs_app_set_name (app, GS_APP_QUALITY_LOWEST, dnf_package_get_name (pkg));
		gs_app_set_summary (app, GS_APP_QUALITY_LOWEST, dnf_package_get_summary (pkg));

		/* set hide-from-search quirk for available apps we don't want to show */
		if (!gs_app_is_installed (app)) {
			switch (gs_app_get_kind (app)) {
			case AS_APP_KIND_DESKTOP:
			case AS_APP_KIND_WEB_APP:
			case AS_APP_KIND_CONSOLE:
				gs_app_add_quirk (app, GS_APP_QUIRK_HIDE_FROM_SEARCH);
				break;
			default:
				break;
			}
		}

		return TRUE /* found */;
	}

	return FALSE /* not found */;
}

static gboolean
resolve_appstream_source_file_to_package_name (GsPlugin *plugin,
                                               GsApp *app,
                                               GsPluginRefineFlags flags,
                                               GCancellable *cancellable,
                                               GError **error)
{
	Header h;
	const gchar *fn;
	gint rc;
	g_auto(rpmdbMatchIterator) mi = NULL;
	g_auto(rpmts) ts = NULL;

	/* open db readonly */
	ts = rpmtsCreate();
	rpmtsSetRootDir (ts, NULL);
	rc = rpmtsOpenDB (ts, O_RDONLY);
	if (rc != 0) {
		g_set_error (error,
		             GS_PLUGIN_ERROR,
		             GS_PLUGIN_ERROR_NOT_SUPPORTED,
		             "Failed to open rpmdb: %i", rc);
		return FALSE;
	}

	/* look for a specific file */
	fn = gs_app_get_metadata_item (app, "appstream::source-file");
	if (fn == NULL)
		return TRUE;

	mi = rpmtsInitIterator (ts, RPMDBI_INSTFILENAMES, fn, 0);
	if (mi == NULL) {
		g_debug ("rpm: no search results for %s", fn);
		return TRUE;
	}

	/* process any results */
	g_debug ("rpm: querying for %s with %s", gs_app_get_id (app), fn);
	while ((h = rpmdbNextIterator (mi)) != NULL) {
		const gchar *name;

		/* add default source */
		name = headerGetString (h, RPMTAG_NAME);
		if (gs_app_get_source_default (app) == NULL) {
			g_debug ("rpm: setting source to %s", name);
			gs_app_add_source (app, name);
			gs_app_set_management_plugin (app, gs_plugin_get_name (plugin));
			gs_app_add_quirk (app, GS_APP_QUIRK_NEEDS_REBOOT);
			app_set_rpm_ostree_packaging_format (app);
			gs_app_set_bundle_kind (app, AS_BUNDLE_KIND_PACKAGE);
		}
	}

	return TRUE;
}

gboolean
gs_plugin_refine (GsPlugin *plugin,
                  GsAppList *list,
                  GsPluginRefineFlags flags,
                  GCancellable *cancellable,
                  GError **error)
{
	GsPluginData *priv = gs_plugin_get_data (plugin);
	g_autoptr(GMutexLocker) locker = NULL;
	g_autoptr(GPtrArray) pkglist = NULL;
	g_autoptr(GVariant) default_deployment = NULL;
	g_auto(GStrv) layered_packages = NULL;
	g_auto(GStrv) layered_local_packages = NULL;
	g_autofree gchar *checksum = NULL;

	locker = g_mutex_locker_new (&priv->mutex);

	/* ensure D-Bus properties are updated before reading them */
	if (!gs_rpmostree_sysroot_call_reload_sync (priv->sysroot_proxy, cancellable, error)) {
		gs_rpmostree_error_convert (error);
		return FALSE;
	}

	default_deployment = gs_rpmostree_os_dup_default_deployment (priv->os_proxy);
	g_assert (g_variant_lookup (default_deployment,
	                            "packages", "^as",
	                            &layered_packages));
	g_assert (g_variant_lookup (default_deployment,
	                            "requested-local-packages", "^as",
	                            &layered_local_packages));
	g_assert (g_variant_lookup (default_deployment,
	                            "checksum", "s",
	                            &checksum));

	pkglist = rpm_ostree_db_query_all (priv->ot_repo, checksum, cancellable, error);
	if (pkglist == NULL) {
		gs_rpmostree_error_convert (error);
		return FALSE;
	}

	for (guint i = 0; i < gs_app_list_length (list); i++) {
		GsApp *app = gs_app_list_index (list, i);
		gboolean found;

		if (gs_app_has_quirk (app, GS_APP_QUIRK_IS_WILDCARD))
			continue;
		/* set management plugin for apps where appstream just added the source package name in refine() */
		if (gs_app_get_management_plugin (app) == NULL &&
		    gs_app_get_bundle_kind (app) == AS_BUNDLE_KIND_PACKAGE &&
		    gs_app_get_scope (app) == AS_APP_SCOPE_SYSTEM &&
		    gs_app_get_source_default (app) != NULL) {
			gs_app_set_management_plugin (app, gs_plugin_get_name (plugin));
			gs_app_add_quirk (app, GS_APP_QUIRK_NEEDS_REBOOT);
			app_set_rpm_ostree_packaging_format (app);
		}
		/* resolve the source package name based on installed appdata/desktop file name */
		if (gs_app_get_management_plugin (app) == NULL &&
		    gs_app_get_bundle_kind (app) == AS_BUNDLE_KIND_UNKNOWN &&
		    gs_app_get_scope (app) == AS_APP_SCOPE_SYSTEM &&
		    gs_app_get_source_default (app) == NULL) {
			if (!resolve_appstream_source_file_to_package_name (plugin, app, flags, cancellable, error))
				return FALSE;
		}
		if (g_strcmp0 (gs_app_get_management_plugin (app), gs_plugin_get_name (plugin)) != 0)
			continue;
		if (gs_app_get_source_default (app) == NULL)
			continue;

		/* first try to resolve from installed packages */
		found = resolve_installed_packages_app (plugin, pkglist, layered_packages, layered_local_packages, app);

		/* if we didn't find anything, try resolving from available packages */
		if (!found && priv->dnf_context != NULL)
			found = resolve_available_packages_app (plugin, dnf_context_get_sack (priv->dnf_context), app);

		/* if we still didn't find anything then it's likely a package
		 * that is still in appstream data, but removed from the repos */
		if (!found)
			g_debug ("failed to resolve %s", gs_app_get_unique_id (app));
	}

	return TRUE;
}

gboolean
gs_plugin_app_upgrade_download (GsPlugin *plugin,
                                GsApp *app,
                                GCancellable *cancellable,
                                GError **error)
{
	GsPluginData *priv = gs_plugin_get_data (plugin);
	const char *packages[] = { NULL };
	g_autofree gchar *new_refspec = NULL;
	g_autofree gchar *transaction_address = NULL;
	g_autoptr(GVariant) options = NULL;
	g_autoptr(TransactionProgress) tp = transaction_progress_new ();

	/* only process this app if was created by this plugin */
	if (g_strcmp0 (gs_app_get_management_plugin (app), gs_plugin_get_name (plugin)) != 0)
		return TRUE;

	/* check is distro-upgrade */
	if (gs_app_get_kind (app) != AS_APP_KIND_OS_UPGRADE)
		return TRUE;

	/* construct new refspec based on the distro version we're upgrading to */
	new_refspec = g_strdup_printf ("ostree://fedora/%s/x86_64/silverblue",
	                               gs_app_get_version (app));

	options = make_rpmostree_options_variant (FALSE,  /* reboot */
	                                          TRUE,   /* allow-downgrade */
	                                          FALSE,  /* cache-only */
	                                          TRUE,   /* download-only */
	                                          FALSE,  /* skip-purge */
	                                          FALSE,  /* no-pull-base */
	                                          FALSE,  /* dry-run */
	                                          FALSE); /* no-overrides */

	gs_app_set_state (app, AS_APP_STATE_INSTALLING);
	tp->app = g_object_ref (app);

	if (!gs_rpmostree_os_call_rebase_sync (priv->os_proxy,
	                                       options,
	                                       new_refspec,
	                                       packages,
	                                       NULL /* fd list */,
	                                       &transaction_address,
	                                       NULL /* fd list out */,
	                                       cancellable,
	                                       error)) {
		gs_rpmostree_error_convert (error);
		gs_app_set_state_recover (app);
		return FALSE;
	}

	if (!gs_rpmostree_transaction_get_response_sync (priv->sysroot_proxy,
	                                                 transaction_address,
	                                                 tp,
	                                                 cancellable,
	                                                 error)) {
		gs_rpmostree_error_convert (error);

		if (g_strrstr ((*error)->message, "Old and new refs are equal")) {
			/* don't error out if the correct tree is already deployed */
			g_debug ("ignoring rpm-ostree error: %s", (*error)->message);
			g_clear_error (error);
		} else {
			gs_app_set_state_recover (app);
			return FALSE;
		}
	}

	/* state is known */
	gs_app_set_state (app, AS_APP_STATE_UPDATABLE);
	return TRUE;
}

gboolean
gs_plugin_launch (GsPlugin *plugin,
                  GsApp *app,
                  GCancellable *cancellable,
                  GError **error)
{
	/* only process this app if was created by this plugin */
	if (g_strcmp0 (gs_app_get_management_plugin (app),
	               gs_plugin_get_name (plugin)) != 0)
		return TRUE;

	/* these are handled by the shell extensions plugin */
	if (gs_app_get_kind (app) == AS_APP_KIND_SHELL_EXTENSION)
		return TRUE;

	return gs_plugin_app_launch (plugin, app, error);
}

static void
add_quirks_from_package_name (GsApp *app, const gchar *package_name)
{
	/* these packages don't have a .repo file in their file lists, but
	 * instead install one through rpm scripts / cron job */
	const gchar *packages_with_repos[] = {
		"google-chrome-stable",
		"google-earth-pro-stable",
		"google-talkplugin",
		NULL };

	if (g_strv_contains (packages_with_repos, package_name))
		gs_app_add_quirk (app, GS_APP_QUIRK_HAS_SOURCE);
}

gboolean
gs_plugin_file_to_app (GsPlugin *plugin,
		       GsAppList *list,
		       GFile *file,
		       GCancellable *cancellable,
		       GError **error)
{
	gboolean ret = FALSE;
	FD_t rpmfd = NULL;
	int r;
	guint64 epoch;
	guint64 size;
	const gchar *name;
	const gchar *version;
	const gchar *release;
	const gchar *license;
	g_auto(Header) h = NULL;
	g_auto(rpmts) ts = NULL;
	g_autofree gchar *evr = NULL;
	g_autofree gchar *filename = NULL;
	g_autoptr(GsApp) app = NULL;

	filename = g_file_get_path (file);
	if (!g_str_has_suffix (filename, ".rpm")) {
		ret = TRUE;
		goto out;
	}

	ts = rpmtsCreate ();
	rpmtsSetVSFlags (ts, _RPMVSF_NOSIGNATURES);

	/* librpm needs Fopenfd */
	rpmfd = Fopen (filename, "r.fdio");
	if (rpmfd == NULL) {
		g_set_error (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_FAILED,
		             "Opening %s failed", filename);
		goto out;
	}
	if (Ferror (rpmfd)) {
		g_set_error (error,
		             GS_PLUGIN_ERROR,
		             GS_PLUGIN_ERROR_FAILED,
		             "Opening %s failed: %s",
		             filename,
		             Fstrerror (rpmfd));
		goto out;
	}

	if ((r = rpmReadPackageFile (ts, rpmfd, filename, &h)) != RPMRC_OK) {
		g_set_error (error,
		             GS_PLUGIN_ERROR,
		             GS_PLUGIN_ERROR_FAILED,
		             "Verification of %s failed",
		             filename);
		goto out;
	}

	app = gs_app_new (NULL);
	gs_app_set_metadata (app, "GnomeSoftware::Creator", gs_plugin_get_name (plugin));
	gs_app_set_management_plugin (app, gs_plugin_get_name (plugin));
	gs_app_add_quirk (app, GS_APP_QUIRK_NEEDS_REBOOT);
	app_set_rpm_ostree_packaging_format (app);
	gs_app_set_kind (app, AS_APP_KIND_GENERIC);
	gs_app_set_bundle_kind (app, AS_BUNDLE_KIND_PACKAGE);
	gs_app_set_scope (app, AS_APP_SCOPE_SYSTEM);
	gs_app_set_state (app, AS_APP_STATE_AVAILABLE_LOCAL);

	/* add default source */
	name = headerGetString (h, RPMTAG_NAME);
	g_debug ("rpm: setting source to %s", name);
	gs_app_add_source (app, name);

	/* add version */
	epoch = headerGetNumber (h, RPMTAG_EPOCH);
	version = headerGetString (h, RPMTAG_VERSION);
	release = headerGetString (h, RPMTAG_RELEASE);
	if (epoch > 0) {
		evr = g_strdup_printf ("%" G_GUINT64_FORMAT ":%s-%s",
		                       epoch, version, release);
	} else {
		evr = g_strdup_printf ("%s-%s",
		                       version, release);
	}
	g_debug ("rpm: setting version to %s", evr);
	gs_app_set_version (app, evr);

	/* set size */
	size = headerGetNumber (h, RPMTAG_SIZE);
	gs_app_set_size_installed (app, size);

	/* set license */
	license = headerGetString (h, RPMTAG_LICENSE);
	if (license != NULL) {
		g_autofree gchar *license_spdx = NULL;
		license_spdx = as_utils_license_to_spdx (license);
		gs_app_set_license (app, GS_APP_QUALITY_NORMAL, license_spdx);
		g_debug ("rpm: setting license to %s", license_spdx);
	}

	add_quirks_from_package_name (app, name);

	gs_app_list_add (list, app);
	ret = TRUE;

out:
	if (rpmfd != NULL)
		(void) Fclose (rpmfd);
	return ret;
}

static gchar **
what_provides_decompose (gchar **values)
{
	GPtrArray *array = g_ptr_array_new ();

	/* iter on each provide string, and wrap it with the Fedora prefix */
	for (guint i = 0; values[i] != NULL; i++) {
		g_ptr_array_add (array, g_strdup (values[i]));
		g_ptr_array_add (array, g_strdup_printf ("gstreamer0.10(%s)", values[i]));
		g_ptr_array_add (array, g_strdup_printf ("gstreamer1(%s)", values[i]));
		g_ptr_array_add (array, g_strdup_printf ("font(%s)", values[i]));
		g_ptr_array_add (array, g_strdup_printf ("mimehandler(%s)", values[i]));
		g_ptr_array_add (array, g_strdup_printf ("postscriptdriver(%s)", values[i]));
		g_ptr_array_add (array, g_strdup_printf ("plasma4(%s)", values[i]));
		g_ptr_array_add (array, g_strdup_printf ("plasma5(%s)", values[i]));
	}
	g_ptr_array_add (array, NULL);
	return (gchar **) g_ptr_array_free (array, FALSE);
}

gboolean
gs_plugin_add_search_what_provides (GsPlugin *plugin,
                                    gchar **search,
                                    GsAppList *list,
                                    GCancellable *cancellable,
                                    GError **error)
{
	GsPluginData *priv = gs_plugin_get_data (plugin);
	g_autoptr(GMutexLocker) locker = NULL;
	g_autoptr(GPtrArray) pkglist = NULL;
	g_auto(GStrv) provides = NULL;

	locker = g_mutex_locker_new (&priv->mutex);

	if (priv->dnf_context == NULL)
		return TRUE;

	provides = what_provides_decompose (search);
	pkglist = find_packages_by_provides (dnf_context_get_sack (priv->dnf_context), provides);
	for (guint i = 0; i < pkglist->len; i++) {
		DnfPackage *pkg = g_ptr_array_index (pkglist, i);
		g_autoptr(GsApp) app = NULL;

		app = gs_plugin_cache_lookup (plugin, dnf_package_get_nevra (pkg));
		if (app != NULL) {
			gs_app_list_add (list, app);
			continue;
		}

		/* create new app */
		app = gs_app_new (NULL);
		gs_app_set_metadata (app, "GnomeSoftware::Creator", gs_plugin_get_name (plugin));
		gs_app_set_management_plugin (app, gs_plugin_get_name (plugin));
		gs_app_add_quirk (app, GS_APP_QUIRK_NEEDS_REBOOT);
		app_set_rpm_ostree_packaging_format (app);
		gs_app_set_kind (app, AS_APP_KIND_GENERIC);
		gs_app_set_bundle_kind (app, AS_BUNDLE_KIND_PACKAGE);
		gs_app_set_scope (app, AS_APP_SCOPE_SYSTEM);
		gs_app_add_source (app, dnf_package_get_name (pkg));

		gs_plugin_cache_add (plugin, dnf_package_get_nevra (pkg), app);
		gs_app_list_add (list, app);
	}

	return TRUE;
}

gboolean
gs_plugin_add_sources (GsPlugin *plugin,
		       GsAppList *list,
		       GCancellable *cancellable,
		       GError **error)
{
	GsPluginData *priv = gs_plugin_get_data (plugin);
	g_autoptr(GMutexLocker) locker = NULL;
	GPtrArray *repos;

	locker = g_mutex_locker_new (&priv->mutex);

	if (priv->dnf_context == NULL)
		return TRUE;

	repos = dnf_context_get_repos (priv->dnf_context);
	if (repos == NULL)
		return TRUE;

	for (guint i = 0; i < repos->len; i++) {
		DnfRepo *repo = g_ptr_array_index (repos, i);
		g_autofree gchar *description = NULL;
		g_autoptr(GsApp) app = NULL;
		gboolean enabled;

		/* hide these from the user */
		if (dnf_repo_is_devel (repo) || dnf_repo_is_source (repo))
			continue;

		app = gs_app_new (dnf_repo_get_id (repo));
		gs_app_set_management_plugin (app, gs_plugin_get_name (plugin));
		gs_app_set_kind (app, AS_APP_KIND_SOURCE);
		gs_app_set_bundle_kind (app, AS_BUNDLE_KIND_PACKAGE);
		gs_app_add_quirk (app, GS_APP_QUIRK_NOT_LAUNCHABLE);

		enabled = (dnf_repo_get_enabled (repo) & DNF_REPO_ENABLED_PACKAGES) > 0;
		gs_app_set_state (app, enabled ? AS_APP_STATE_INSTALLED : AS_APP_STATE_AVAILABLE);

		description = dnf_repo_get_description (repo);
		gs_app_set_name (app, GS_APP_QUALITY_LOWEST, description);
		gs_app_set_summary (app, GS_APP_QUALITY_LOWEST, description);

		gs_app_list_add (list, app);
	}

	return TRUE;
}
