/*
 * Copyright 2015 Richard Hughes <richard@hughsie.com>
 *
 * SPDX-License-Identifier: LGPL-2.1-or-later
 */

#define G_LOG_DOMAIN "FuMain"

#include "config.h"

#include <fwupdplugin.h>

#include <fcntl.h>
#include <glib/gi18n.h>
#include <glib/gstdio.h>
#ifdef HAVE_GIO_UNIX
#include <gio/gunixfdlist.h>
#include <glib-unix.h>
#endif
#include <locale.h>
#include <stdlib.h>
#include <unistd.h>

#include "fu-console.h"
#include "fu-polkit-agent.h"
#include "fu-util-bios-setting.h"
#include "fu-util-common.h"

#ifdef HAVE_SYSTEMD
#include "fu-systemd.h"
#endif

typedef enum {
	FU_UTIL_OPERATION_UNKNOWN,
	FU_UTIL_OPERATION_UPDATE,
	FU_UTIL_OPERATION_DOWNGRADE,
	FU_UTIL_OPERATION_INSTALL,
	FU_UTIL_OPERATION_LAST
} FuUtilOperation;

struct FuUtil {
	GCancellable *cancellable;
	GMainContext *main_ctx;
	GMainLoop *loop;
	GOptionContext *context;
	FwupdInstallFlags flags;
	FwupdClientDownloadFlags download_flags;
	FwupdClient *client;
	FuConsole *console;
	gboolean no_remote_check;
	gboolean no_metadata_check;
	gboolean no_reboot_check;
	gboolean no_unreported_check;
	gboolean no_safety_check;
	gboolean no_device_prompt;
	gboolean no_emulation_check;
	gboolean no_security_fix;
	gboolean assume_yes;
	gboolean sign;
	gboolean show_all;
	gboolean disable_ssl_strict;
	gboolean as_json;
	/* only valid in update and downgrade */
	FuUtilOperation current_operation;
	FwupdDevice *current_device;
	GPtrArray *post_requests;
	FwupdDeviceFlags completion_flags;
	FwupdDeviceFlags filter_device_include;
	FwupdDeviceFlags filter_device_exclude;
	FwupdReleaseFlags filter_release_include;
	FwupdReleaseFlags filter_release_exclude;
};

static gboolean
fu_util_report_history(FuUtil *self, gchar **values, GError **error);
static FwupdDevice *
fu_util_get_device_by_id(FuUtil *self, const gchar *id, GError **error);

static void
fu_util_client_notify_cb(GObject *object, GParamSpec *pspec, FuUtil *self)
{
	if (self->as_json)
		return;
	fu_console_set_progress(self->console,
				fwupd_client_get_status(self->client),
				fwupd_client_get_percentage_full(self->client));
}

static void
fu_util_update_device_request_cb(FwupdClient *client, FwupdRequest *request, FuUtil *self)
{
	/* nothing sensible to show */
	if (fwupd_request_get_message(request) == NULL)
		return;

	/* show this now */
	if (fwupd_request_get_kind(request) == FWUPD_REQUEST_KIND_IMMEDIATE) {
		g_autofree gchar *fmt = NULL;
		g_autofree gchar *tmp = NULL;

		/* TRANSLATORS: the user needs to do something, e.g. remove the device */
		fmt = fu_console_color_format(_("Action Required:"), FU_CONSOLE_COLOR_RED);
		tmp = g_strdup_printf("%s %s", fmt, fwupd_request_get_message(request));
		fu_console_set_progress_title(self->console, tmp);
		fu_console_beep(self->console, 5);
	}

	/* save for later */
	if (fwupd_request_get_kind(request) == FWUPD_REQUEST_KIND_POST)
		g_ptr_array_add(self->post_requests, g_object_ref(request));
}

static void
fu_util_update_device_changed_cb(FwupdClient *client, FwupdDevice *device, FuUtil *self)
{
	g_autofree gchar *str = NULL;

	/* action has not been assigned yet */
	if (self->current_operation == FU_UTIL_OPERATION_UNKNOWN)
		return;

	/* allowed to set whenever the device has changed */
	if (fwupd_device_has_flag(device, FWUPD_DEVICE_FLAG_NEEDS_SHUTDOWN))
		self->completion_flags |= FWUPD_DEVICE_FLAG_NEEDS_SHUTDOWN;
	if (fwupd_device_has_flag(device, FWUPD_DEVICE_FLAG_NEEDS_REBOOT))
		self->completion_flags |= FWUPD_DEVICE_FLAG_NEEDS_REBOOT;

	/* same as last time, so ignore */
	if (self->current_device == NULL ||
	    g_strcmp0(fwupd_device_get_composite_id(self->current_device),
		      fwupd_device_get_composite_id(device)) == 0) {
		g_set_object(&self->current_device, device);
		return;
	}

	/* ignore indirect devices that might have changed */
	if (fwupd_device_get_status(device) == FWUPD_STATUS_IDLE ||
	    fwupd_device_get_status(device) == FWUPD_STATUS_UNKNOWN) {
		g_debug("ignoring %s with status %s",
			fwupd_device_get_name(device),
			fwupd_status_to_string(fwupd_device_get_status(device)));
		return;
	}

	/* show message in console */
	if (self->current_operation == FU_UTIL_OPERATION_UPDATE) {
		/* TRANSLATORS: %1 is a device name */
		str = g_strdup_printf(_("Updating %s…"), fwupd_device_get_name(device));
		fu_console_set_progress_title(self->console, str);
	} else if (self->current_operation == FU_UTIL_OPERATION_DOWNGRADE) {
		/* TRANSLATORS: %1 is a device name */
		str = g_strdup_printf(_("Downgrading %s…"), fwupd_device_get_name(device));
		fu_console_set_progress_title(self->console, str);
	} else if (self->current_operation == FU_UTIL_OPERATION_INSTALL) {
		/* TRANSLATORS: %1 is a device name  */
		str = g_strdup_printf(_("Installing on %s…"), fwupd_device_get_name(device));
		fu_console_set_progress_title(self->console, str);
	} else {
		g_warning("no FuUtilOperation set");
	}
	g_set_object(&self->current_device, device);
}

static FwupdDevice *
fu_util_prompt_for_device(FuUtil *self, GPtrArray *devices, GError **error)
{
	FwupdDevice *dev;
	guint idx;
	g_autoptr(GPtrArray) devices_filtered = NULL;

	/* filter results */
	devices_filtered = fwupd_device_array_filter_flags(devices,
							   self->filter_device_include,
							   self->filter_device_exclude,
							   error);
	if (devices_filtered == NULL)
		return NULL;

	/* exactly one */
	if (devices_filtered->len == 1) {
		dev = g_ptr_array_index(devices_filtered, 0);
		if (!self->as_json) {
			fu_console_print(
			    self->console,
			    "%s: %s",
			    /* TRANSLATORS: device has been chosen by the daemon for the user */
			    _("Selected device"),
			    fwupd_device_get_name(dev));
		}
		return g_object_ref(dev);
	}

	/* no questions */
	if (self->no_device_prompt) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOT_FOUND,
				    "can't prompt for devices");
		return NULL;
	}

	/* TRANSLATORS: this is to abort the interactive prompt */
	fu_console_print(self->console, "0.\t%s", _("Cancel"));
	for (guint i = 0; i < devices_filtered->len; i++) {
		dev = g_ptr_array_index(devices_filtered, i);
		fu_console_print(self->console,
				 "%u.\t%s (%s)",
				 i + 1,
				 fwupd_device_get_id(dev),
				 fwupd_device_get_name(dev));
	}
	/* TRANSLATORS: get interactive prompt */
	idx = fu_console_input_uint(self->console, devices_filtered->len, "%s", _("Choose device"));
	if (idx == 0) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOTHING_TO_DO,
				    "Request canceled");
		return NULL;
	}
	dev = g_ptr_array_index(devices_filtered, idx - 1);
	return g_object_ref(dev);
}

static gboolean
fu_util_perhaps_show_unreported(FuUtil *self, GError **error)
{
	g_autoptr(GError) error_local = NULL;
	g_autoptr(GPtrArray) devices = NULL;
	g_autoptr(GPtrArray) devices_failed = g_ptr_array_new();
	g_autoptr(GPtrArray) devices_success = g_ptr_array_new();
	g_autoptr(GPtrArray) remotes = NULL;
	g_autoptr(GHashTable) remote_id_uri_map = NULL;
	gboolean all_automatic = FALSE;

	/* we don't want to ask anything */
	if (self->no_unreported_check || self->as_json) {
		g_debug("skipping unreported check");
		return TRUE;
	}

	/* get all devices from the history database */
	devices = fwupd_client_get_history(self->client, self->cancellable, &error_local);
	if (devices == NULL) {
		if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO))
			return TRUE;
		g_propagate_error(error, g_steal_pointer(&error_local));
		return FALSE;
	}

	/* create a map of RemoteID to RemoteURI */
	remotes = fwupd_client_get_remotes(self->client, self->cancellable, error);
	if (remotes == NULL)
		return FALSE;
	remote_id_uri_map = g_hash_table_new(g_str_hash, g_str_equal);
	for (guint i = 0; i < remotes->len; i++) {
		FwupdRemote *remote = g_ptr_array_index(remotes, i);
		gboolean remote_automatic;
		if (fwupd_remote_get_id(remote) == NULL)
			continue;
		if (fwupd_remote_get_report_uri(remote) == NULL)
			continue;
		g_debug("adding %s for %s",
			fwupd_remote_get_report_uri(remote),
			fwupd_remote_get_id(remote));
		g_hash_table_insert(remote_id_uri_map,
				    (gpointer)fwupd_remote_get_id(remote),
				    (gpointer)fwupd_remote_get_report_uri(remote));
		remote_automatic =
		    fwupd_remote_has_flag(remote, FWUPD_REMOTE_FLAG_AUTOMATIC_REPORTS);
		g_debug("%s is %d", fwupd_remote_get_title(remote), remote_automatic);
		if (remote_automatic && !all_automatic)
			all_automatic = TRUE;
		if (!remote_automatic && all_automatic) {
			all_automatic = FALSE;
			break;
		}
	}

	/* check that they can be reported */
	for (guint i = 0; i < devices->len; i++) {
		FwupdDevice *dev = g_ptr_array_index(devices, i);
		FwupdRelease *rel = fwupd_device_get_release_default(dev);
		const gchar *remote_id;
		const gchar *remote_uri;

		if (!fwupd_device_match_flags(dev,
					      self->filter_device_include,
					      self->filter_device_exclude))
			continue;
		if (fwupd_device_has_flag(dev, FWUPD_DEVICE_FLAG_REPORTED))
			continue;
		if (!fwupd_device_has_flag(dev, FWUPD_DEVICE_FLAG_SUPPORTED))
			continue;

		/* find the RemoteURI to use for the device */
		remote_id = fwupd_release_get_remote_id(rel);
		if (remote_id == NULL) {
			g_debug("%s has no RemoteID", fwupd_device_get_id(dev));
			continue;
		}
		remote_uri = g_hash_table_lookup(remote_id_uri_map, remote_id);
		if (remote_uri == NULL) {
			g_debug("%s has no RemoteURI", remote_id);
			continue;
		}

		/* only send success and failure */
		if (fwupd_device_get_update_state(dev) == FWUPD_UPDATE_STATE_FAILED) {
			g_ptr_array_add(devices_failed, dev);
		} else if (fwupd_device_get_update_state(dev) == FWUPD_UPDATE_STATE_SUCCESS) {
			g_ptr_array_add(devices_success, dev);
		} else {
			g_debug("ignoring %s with UpdateState %s",
				fwupd_device_get_id(dev),
				fwupd_update_state_to_string(fwupd_device_get_update_state(dev)));
		}
	}

	/* nothing to do */
	if (devices_failed->len == 0 && devices_success->len == 0) {
		g_debug("no unreported devices");
		return TRUE;
	}

	g_debug("all automatic: %d", all_automatic);
	/* show the success and failures */
	if (!self->assume_yes && !all_automatic) {
		/* delimit */
		fu_console_line(self->console, 48);

		/* failures */
		if (devices_failed->len > 0) {
			fu_console_print_literal(self->console,
						 /* TRANSLATORS: a list of failed updates */
						 _("Devices that were not updated correctly:"));
			for (guint i = 0; i < devices_failed->len; i++) {
				FwupdDevice *dev = g_ptr_array_index(devices_failed, i);
				FwupdRelease *rel = fwupd_device_get_release_default(dev);
				fu_console_print(self->console,
						 " • %s (%s → %s)",
						 fwupd_device_get_name(dev),
						 fwupd_device_get_version(dev),
						 fwupd_release_get_version(rel));
			}
		}

		/* success */
		if (devices_success->len > 0) {
			fu_console_print_literal(self->console,
						 /* TRANSLATORS: a list of successful updates */
						 _("Devices that have been updated successfully:"));
			for (guint i = 0; i < devices_success->len; i++) {
				FwupdDevice *dev = g_ptr_array_index(devices_success, i);
				FwupdRelease *rel = fwupd_device_get_release_default(dev);
				fu_console_print(self->console,
						 " • %s (%s → %s)",
						 fwupd_device_get_name(dev),
						 fwupd_device_get_version(dev),
						 fwupd_release_get_version(rel));
			}
		}

		/* ask for permission */
		fu_console_print_literal(self->console,
					 /* TRANSLATORS: explain why we want to upload */
					 _("Uploading firmware reports helps hardware vendors "
					   "to quickly identify failing and successful updates "
					   "on real devices."));
		if (!fu_console_input_bool(self->console,
					   TRUE,
					   "%s (%s)",
					   /* TRANSLATORS: ask the user to upload */
					   _("Review and upload report now?"),
					   /* TRANSLATORS: metadata is downloaded */
					   _("Requires internet connection"))) {
			if (fu_console_input_bool(self->console,
						  FALSE,
						  "%s",
						  /* TRANSLATORS: offer to disable this nag */
						  _("Do you want to disable this feature "
						    "for future updates?"))) {
				for (guint i = 0; i < remotes->len; i++) {
					FwupdRemote *remote = g_ptr_array_index(remotes, i);
					const gchar *remote_id = fwupd_remote_get_id(remote);
					if (fwupd_remote_get_report_uri(remote) == NULL)
						continue;
					if (!fwupd_client_modify_remote(self->client,
									remote_id,
									"ReportURI",
									"",
									self->cancellable,
									error))
						return FALSE;
				}
			}
			g_set_error_literal(error,
					    FWUPD_ERROR,
					    FWUPD_ERROR_NOTHING_TO_DO,
					    "Declined upload");
			return FALSE;
		}
	}

	/* upload */
	if (!fu_util_report_history(self, NULL, error))
		return FALSE;

	/* offer to make automatic */
	if (!self->assume_yes && !all_automatic) {
		if (fu_console_input_bool(self->console,
					  FALSE,
					  "%s",
					  /* TRANSLATORS: offer to stop asking the question */
					  _("Do you want to upload reports automatically for "
					    "future updates?"))) {
			for (guint i = 0; i < remotes->len; i++) {
				FwupdRemote *remote = g_ptr_array_index(remotes, i);
				const gchar *remote_id = fwupd_remote_get_id(remote);
				if (fwupd_remote_get_report_uri(remote) == NULL)
					continue;
				if (fwupd_remote_has_flag(remote,
							  FWUPD_REMOTE_FLAG_AUTOMATIC_REPORTS))
					continue;
				if (!fwupd_client_modify_remote(self->client,
								remote_id,
								"AutomaticReports",
								"true",
								self->cancellable,
								error))
					return FALSE;
			}
		}
	}

	/* success */
	return TRUE;
}

static void
fu_util_build_device_tree(FuUtil *self, FuUtilNode *root, GPtrArray *devs, FwupdDevice *dev)
{
	for (guint i = 0; i < devs->len; i++) {
		FwupdDevice *dev_tmp = g_ptr_array_index(devs, i);
		if (!fwupd_device_match_flags(dev_tmp,
					      self->filter_device_include,
					      self->filter_device_exclude))
			continue;
		if (!self->show_all && !fu_util_is_interesting_device(devs, dev_tmp))
			continue;
		if (fwupd_device_get_parent(dev_tmp) == dev) {
			FuUtilNode *child = g_node_append_data(root, g_object_ref(dev_tmp));
			fu_util_build_device_tree(self, child, devs, dev_tmp);
		}
	}
}

static void
fu_util_get_releases_as_json(FuUtil *self, GPtrArray *rels)
{
	g_autoptr(FwupdJsonObject) json_obj = fwupd_json_object_new();
	g_autoptr(FwupdJsonArray) json_arr = fwupd_json_array_new();

	for (guint i = 0; i < rels->len; i++) {
		FwupdRelease *rel = g_ptr_array_index(rels, i);
		g_autoptr(FwupdJsonObject) json_obj_tmp = fwupd_json_object_new();
		if (!fwupd_release_match_flags(rel,
					       self->filter_release_include,
					       self->filter_release_exclude))
			continue;
		fwupd_codec_to_json(FWUPD_CODEC(rel), json_obj_tmp, FWUPD_CODEC_FLAG_NONE);
		fwupd_json_array_add_object(json_arr, json_obj_tmp);
	}
	fwupd_json_object_add_array(json_obj, "Releases", json_arr);
	fu_util_print_json_object(self->console, json_obj);
}

static void
fu_util_get_devices_as_json(FuUtil *self, GPtrArray *devs)
{
	g_autoptr(FwupdJsonObject) json_obj = fwupd_json_object_new();
	g_autoptr(FwupdJsonArray) json_arr = fwupd_json_array_new();

	for (guint i = 0; i < devs->len; i++) {
		FwupdDevice *dev = g_ptr_array_index(devs, i);
		g_autoptr(GPtrArray) rels = NULL;
		g_autoptr(GError) error_local = NULL;
		g_autoptr(FwupdJsonObject) json_obj_tmp = fwupd_json_object_new();

		/* filter */
		if (!fwupd_device_match_flags(dev,
					      self->filter_device_include,
					      self->filter_device_exclude))
			continue;

		/* add all releases that could be applied */
		rels = fwupd_client_get_releases(self->client,
						 fwupd_device_get_id(dev),
						 self->cancellable,
						 &error_local);
		if (rels == NULL) {
			g_debug("not adding releases to device: %s", error_local->message);
		} else {
			for (guint j = 0; j < rels->len; j++) {
				FwupdRelease *rel = g_ptr_array_index(rels, j);
				if (!fwupd_release_match_flags(rel,
							       self->filter_release_include,
							       self->filter_release_exclude))
					continue;
				fwupd_device_add_release(dev, rel);
			}
		}

		/* add to builder */
		fwupd_codec_to_json(FWUPD_CODEC(dev), json_obj_tmp, FWUPD_CODEC_FLAG_TRUSTED);
		fwupd_json_array_add_object(json_arr, json_obj_tmp);
	}
	fwupd_json_object_add_array(json_obj, "Devices", json_arr);
	fu_util_print_json_object(self->console, json_obj);
}

static gboolean
fu_util_check_reboot_needed(FuUtil *self, gchar **values, GError **error)
{
	/* handle both forms */
	if (g_strv_length(values) == 0) {
		g_autoptr(GPtrArray) devices =
		    fwupd_client_get_devices(self->client, self->cancellable, error);
		if (devices == NULL)
			return FALSE;
		for (guint i = 0; i < devices->len; i++) {
			FwupdDevice *device = g_ptr_array_index(devices, i);

			if (fwupd_device_get_update_state(device) ==
			    FWUPD_UPDATE_STATE_NEEDS_REBOOT)
				self->completion_flags |= FWUPD_DEVICE_FLAG_NEEDS_REBOOT;
		}
	} else {
		for (guint i = 0; values[i] != NULL; i++) {
			g_autoptr(FwupdDevice) device = NULL;
			device = fu_util_get_device_by_id(self, values[i], error);
			if (device == NULL)
				return FALSE;
			if (fwupd_device_get_update_state(device) ==
			    FWUPD_UPDATE_STATE_NEEDS_REBOOT)
				self->completion_flags |= FWUPD_DEVICE_FLAG_NEEDS_REBOOT;
		}
	}

	if (!(self->completion_flags & FWUPD_DEVICE_FLAG_NEEDS_REBOOT)) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOTHING_TO_DO,
				    /* TRANSLATORS: no rebooting needed */
				    _("No reboot is necessary"));
		return FALSE;
	}

	if (self->as_json)
		return TRUE;

	return fu_util_prompt_complete(self->console, self->completion_flags, TRUE, error);
}

static gboolean
fu_util_get_devices(FuUtil *self, gchar **values, GError **error)
{
	g_autoptr(FuUtilNode) root = g_node_new(NULL);
	g_autoptr(GPtrArray) devs = NULL;

	/* get results from daemon */
	if (g_strv_length(values) > 0) {
		devs = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref);
		for (guint i = 0; values[i] != NULL; i++) {
			FwupdDevice *device = fu_util_get_device_by_id(self, values[i], error);
			if (device == NULL)
				return FALSE;
			g_ptr_array_add(devs, device);
		}
	} else {
		devs = fwupd_client_get_devices(self->client, self->cancellable, error);
		if (devs == NULL)
			return FALSE;
	}

	/* not for human consumption */
	if (self->as_json) {
		fu_util_get_devices_as_json(self, devs);
		return TRUE;
	}

	/* print */
	if (devs->len > 0)
		fu_util_build_device_tree(self, root, devs, NULL);
	if (g_node_n_children(root) == 0) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOTHING_TO_DO,
				    /* TRANSLATORS: nothing attached that can be upgraded */
				    _("No hardware detected with firmware update capability"));
		return FALSE;
	}
	fu_util_print_node(self->console, self->client, root);

	/* nag? */
	if (!fu_util_perhaps_show_unreported(self, error))
		return FALSE;

	return TRUE;
}

static gboolean
fu_util_get_plugins(FuUtil *self, gchar **values, GError **error)
{
	g_autoptr(GPtrArray) plugins = NULL;

	/* get results from daemon */
	plugins = fwupd_client_get_plugins(self->client, self->cancellable, error);
	if (plugins == NULL)
		return FALSE;
	g_ptr_array_sort(plugins, (GCompareFunc)fu_util_plugin_name_sort_cb);
	if (self->as_json) {
		g_autoptr(FwupdJsonObject) json_obj = fwupd_json_object_new();
		fwupd_codec_array_to_json(plugins, "Plugins", json_obj, FWUPD_CODEC_FLAG_TRUSTED);
		fu_util_print_json_object(self->console, json_obj);
		return TRUE;
	}

	/* print */
	for (guint i = 0; i < plugins->len; i++) {
		FuPlugin *plugin = g_ptr_array_index(plugins, i);
		g_autofree gchar *str = fu_util_plugin_to_string(FWUPD_PLUGIN(plugin), 0);
		fu_console_print_literal(self->console, str);
	}

	/* success */
	return TRUE;
}

static gchar *
fu_util_download_if_required(FuUtil *self, const gchar *perhapsfn, GError **error)
{
	g_autofree gchar *filename = NULL;
	g_autoptr(GBytes) blob = NULL;

	/* a local file */
	if (g_file_test(perhapsfn, G_FILE_TEST_EXISTS))
		return g_strdup(perhapsfn);
	if (!fu_util_is_url(perhapsfn))
		return g_strdup(perhapsfn);

	/* download the firmware to a cachedir */
	filename = fu_util_get_user_cache_path(perhapsfn);
	if (g_file_test(filename, G_FILE_TEST_EXISTS))
		return g_steal_pointer(&filename);
	if (!fu_path_mkdir_parent(filename, error))
		return NULL;
	blob = fwupd_client_download_bytes(self->client,
					   perhapsfn,
					   self->download_flags,
					   self->cancellable,
					   error);
	if (blob == NULL)
		return NULL;

	/* save file to cache */
	if (!fu_bytes_set_contents(filename, blob, error))
		return NULL;
	return g_steal_pointer(&filename);
}

static void
fu_util_display_current_message(FuUtil *self)
{
	if (self->as_json)
		return;

	/* TRANSLATORS: success message */
	fu_console_print_literal(self->console, _("Successfully installed firmware"));

	/* print all POST requests */
	for (guint i = 0; i < self->post_requests->len; i++) {
		FwupdRequest *request = g_ptr_array_index(self->post_requests, i);
		fu_console_print_literal(self->console, fu_util_request_get_message(request));
	}
}

typedef struct {
	guint nr_success;
	guint nr_missing;
	guint nr_skipped;
	const gchar *name;
	gboolean use_emulation;
	GHashTable *report_metadata;
} FuUtilDeviceTestHelper;

static void
fu_util_device_test_helper_free(FuUtilDeviceTestHelper *helper)
{
	if (helper->report_metadata != NULL)
		g_hash_table_unref(helper->report_metadata);
	g_free(helper);
}

static FuUtilDeviceTestHelper *
fu_util_device_test_helper_new(void)
{
	FuUtilDeviceTestHelper *helper = g_new0(FuUtilDeviceTestHelper, 1);
	return helper;
}

G_DEFINE_AUTOPTR_CLEANUP_FUNC(FuUtilDeviceTestHelper, fu_util_device_test_helper_free)

static GPtrArray *
fu_util_filter_devices(FuUtil *self, GPtrArray *devices, GError **error)
{
	g_autoptr(GPtrArray) devices_filtered =
	    g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref);
	for (guint i = 0; i < devices->len; i++) {
		FwupdDevice *dev = g_ptr_array_index(devices, i);
		if (!fwupd_device_match_flags(dev,
					      self->filter_device_include,
					      self->filter_device_exclude))
			continue;
		g_ptr_array_add(devices_filtered, g_object_ref(dev));
	}
	if (devices_filtered->len == 0) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOT_FOUND,
				    "failed to find any devices");
		return NULL;
	}
	return g_steal_pointer(&devices_filtered);
}

static gboolean
fu_util_device_test_component(FuUtil *self,
			      FuUtilDeviceTestHelper *helper,
			      FwupdJsonObject *json_obj,
			      FwupdJsonObject *json_object_result,
			      GError **error)
{
	const gchar *name;
	const gchar *protocol;
	const gchar *version;
	g_autoptr(FwupdDevice) device = NULL;
	g_autoptr(FwupdJsonArray) json_arr = NULL;

	/* some elements are optional */
	name = fwupd_json_object_get_string_with_default(json_obj, "name", "component", error);
	if (name == NULL)
		return FALSE;
	if (g_strcmp0(name, "component") != 0)
		fwupd_json_object_add_string(json_object_result, "name", name);
	protocol = fwupd_json_object_get_string(json_obj, "protocol", NULL);
	if (protocol != NULL)
		fwupd_json_object_add_string(json_object_result, "protocol", protocol);

	/* find the device with any of the matching GUIDs */
	json_arr = fwupd_json_object_get_array(json_obj, "guids", error);
	if (json_arr == NULL)
		return FALSE;
	fwupd_json_object_add_array(json_object_result, "guids", json_arr);
	for (guint i = 0; i < fwupd_json_array_get_size(json_arr); i++) {
		FwupdDevice *device_tmp;
		const gchar *guid;
		g_autoptr(GPtrArray) devices = NULL;
		g_autoptr(GPtrArray) devices_filtered = NULL;

		guid = fwupd_json_array_get_string(json_arr, i, error);
		if (guid == NULL)
			return FALSE;

		g_debug("looking for guid %s", guid);
		devices =
		    fwupd_client_get_devices_by_guid(self->client, guid, self->cancellable, NULL);
		if (devices == NULL)
			continue;
		devices_filtered = fu_util_filter_devices(self, devices, NULL);
		if (devices_filtered == NULL)
			continue;
		if (devices_filtered->len > 1) {
			g_set_error(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOT_SUPPORTED,
				    "multiple devices with GUID %s",
				    guid);
			return FALSE;
		}
		device_tmp = g_ptr_array_index(devices_filtered, 0);
		if (protocol != NULL && !fwupd_device_has_protocol(device_tmp, protocol))
			continue;
		device = g_object_ref(device_tmp);
		break;
	}
	if (device == NULL) {
		if (!self->as_json) {
			g_autofree gchar *msg = NULL;
			msg = fu_console_color_format(
			    /* TRANSLATORS: this is for the device tests */
			    _("Did not find any devices with matching GUIDs"),
			    FU_CONSOLE_COLOR_RED);
			fu_console_print(self->console, "%s: %s", name, msg);
		}
		g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no devices found");
		return FALSE;
	}

	/* verify the version matches what we expected */
	version = fwupd_json_object_get_string(json_obj, "version", NULL);
	if (version != NULL) {
		if (g_strcmp0(version, fwupd_device_get_version(device)) != 0) {
			g_set_error(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INTERNAL,
				    "version did not match: got %s, expected %s",
				    fwupd_device_get_version(device),
				    version);
			return FALSE;
		}
	}

	/* verify the bootloader version matches what we expected */
	version = fwupd_json_object_get_string(json_obj, "version-bootloader", NULL);
	if (version != NULL) {
		if (g_strcmp0(version, fwupd_device_get_version_bootloader(device)) != 0) {
			g_set_error(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INTERNAL,
				    "bootloader version did not match: got %s, expected %s",
				    fwupd_device_get_version_bootloader(device),
				    version);
			return FALSE;
		}
	}

	/* verify the branch matches what we expected */
	version = fwupd_json_object_get_string(json_obj, "branch", NULL);
	if (version != NULL) {
		if (g_strcmp0(version, fwupd_device_get_branch(device)) != 0) {
			g_set_error(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INTERNAL,
				    "branch did not match: got %s, expected %s",
				    fwupd_device_get_branch(device),
				    version);
			return FALSE;
		}
	}

	/* success */
	if (!self->as_json) {
		g_autofree gchar *msg = NULL;
		/* TRANSLATORS: this is for the device tests */
		msg = fu_console_color_format(_("OK!"), FU_CONSOLE_COLOR_GREEN);
		if (g_strcmp0(name, "component") != 0) {
			fu_console_print(self->console, "%s [%s]: %s", helper->name, name, msg);
		} else {
			fu_console_print(self->console, "%s: %s", helper->name, msg);
		}
	}
	helper->nr_success++;
	return TRUE;
}

static gboolean
fu_util_device_test_remove_emulated_devices(FuUtil *self, GError **error)
{
	g_autoptr(GPtrArray) devices = NULL;

	devices = fwupd_client_get_devices(self->client, self->cancellable, error);
	if (devices == NULL)
		return FALSE;
	for (guint i = 0; i < devices->len; i++) {
		FwupdDevice *device = g_ptr_array_index(devices, i);
		g_autoptr(GError) error_local = NULL;
		if (!fwupd_device_has_flag(device, FWUPD_DEVICE_FLAG_EMULATED))
			continue;
		if (!fwupd_client_modify_device(self->client,
						fwupd_device_get_id(device),
						"Flags",
						"~emulated",
						self->cancellable,
						&error_local)) {
			if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND)) {
				g_debug("ignoring: %s", error_local->message);
				continue;
			}
			g_propagate_prefixed_error(error,
						   g_steal_pointer(&error_local),
						   "failed to modify device: ");
			return FALSE;
		}
	}

	/* success */
	return TRUE;
}

static gchar *
fu_util_maybe_expand_basename(FuUtil *self, const gchar *maybe_basename, GError **error)
{
	g_autoptr(FwupdRemote) remote = NULL;

	if (g_str_has_prefix(maybe_basename, "https://"))
		return g_strdup(maybe_basename);
	if (g_str_has_prefix(maybe_basename, "/"))
		return g_strdup(maybe_basename);

	/* find LVFS remote */
	remote = fwupd_client_get_remote_by_id(self->client, "lvfs", self->cancellable, error);
	if (remote == NULL)
		return NULL;
	if (fwupd_remote_get_firmware_base_uri(remote) == NULL) {
		g_debug("no FirmwareBaseURI set in lvfs.conf, using default");
		return g_strdup_printf("https://fwupd.org/downloads/%s", maybe_basename);
	}
	return g_strdup_printf("%s/%s", fwupd_remote_get_firmware_base_uri(remote), maybe_basename);
}

static gboolean
fu_util_device_test_step(FuUtil *self,
			 FuUtilDeviceTestHelper *helper,
			 FwupdJsonObject *json_obj,
			 FwupdJsonObject *json_object_result,
			 GError **error)
{
	const gchar *url_tmp;
	g_autoptr(FwupdJsonArray) json_arr = NULL;

	/* send this data to the daemon */
	if (helper->use_emulation) {
		g_autofree gchar *emulation_filename = NULL;
		g_autofree gchar *emulation_url = NULL;
		g_autoptr(GError) error_local = NULL;

		/* ignore anything without emulation data */
		url_tmp = fwupd_json_object_get_string(json_obj, "emulation-url", NULL);
		if (url_tmp != NULL) {
			emulation_url = fu_util_maybe_expand_basename(self, url_tmp, error);
			if (emulation_url == NULL)
				return FALSE;
			emulation_filename =
			    fu_util_download_if_required(self, emulation_url, error);
			if (emulation_filename == NULL) {
				g_prefix_error(error, "failed to download %s: ", emulation_url);
				return FALSE;
			}
		} else {
			emulation_filename = g_strdup(
			    fwupd_json_object_get_string(json_obj, "emulation-file", NULL));
			if (emulation_filename == NULL)
				return TRUE;
		}

		/* log */
		if (emulation_url != NULL) {
			fwupd_json_object_add_string(json_object_result,
						     "emulation-url",
						     emulation_url);
		}
		fwupd_json_object_add_string(json_object_result,
					     "emulation-file",
					     emulation_filename);
		if (!fwupd_client_emulation_load(self->client,
						 emulation_filename,
						 self->cancellable,
						 &error_local)) {
			if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED)) {
				g_debug("ignoring: %s", error_local->message);
				helper->nr_skipped++;
				return TRUE;
			}
			g_propagate_prefixed_error(error,
						   g_steal_pointer(&error_local),
						   "failed to load %s: ",
						   emulation_filename);
			return FALSE;
		}
	}

	/* download file if required */
	url_tmp = fwupd_json_object_get_string(json_obj, "url", NULL);
	if (url_tmp != NULL) {
		g_autofree gchar *filename = NULL;
		g_autofree gchar *url = NULL;
		g_autoptr(GError) error_local = NULL;

		url = fu_util_maybe_expand_basename(self, url_tmp, error);
		if (url == NULL)
			return FALSE;
		filename = fu_util_download_if_required(self, url, error);
		if (filename == NULL) {
			g_prefix_error(error, "failed to download %s: ", url);
			return FALSE;
		}

		/* log */
		fwupd_json_object_add_string(json_object_result, "url", url);

		/* install file */
		self->flags |= FWUPD_INSTALL_FLAG_ALLOW_OLDER;
		self->flags |= FWUPD_INSTALL_FLAG_ALLOW_REINSTALL;
		if (!fwupd_client_install(self->client,
					  FWUPD_DEVICE_ID_ANY,
					  filename,
					  self->flags,
					  self->cancellable,
					  &error_local)) {
			if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND)) {
				if (self->as_json) {
					fwupd_json_object_add_string(json_object_result,
								     "info",
								     error_local->message);
				} else {
					g_autofree gchar *msg = NULL;
					msg = fu_console_color_format(error_local->message,
								      FU_CONSOLE_COLOR_YELLOW);
					fu_console_print(self->console,
							 "%s: %s",
							 helper->name,
							 msg);
				}
				helper->nr_missing++;
				return TRUE;
			}
			g_propagate_error(error, g_steal_pointer(&error_local));
			return FALSE;
		}
	}

	/* process each step */
	json_arr = fwupd_json_object_get_array(json_obj, "components", error);
	if (json_arr == NULL)
		return FALSE;
	for (guint i = 0; i < fwupd_json_array_get_size(json_arr); i++) {
		g_autoptr(FwupdJsonObject) json_obj_tmp = NULL;
		json_obj_tmp = fwupd_json_array_get_object(json_arr, i, error);
		if (json_obj_tmp == NULL)
			return FALSE;
		if (!fu_util_device_test_component(self,
						   helper,
						   json_obj_tmp,
						   json_object_result,
						   error))
			return FALSE;
	}

	/* remove emulated devices */
	if (helper->use_emulation) {
		if (!fu_util_device_test_remove_emulated_devices(self, error)) {
			g_prefix_error_literal(error, "failed to remove emulated devices: ");
			return FALSE;
		}
	}

	/* success */
	fwupd_json_object_add_boolean(json_object_result, "success", TRUE);
	return TRUE;
}

static gboolean
fu_util_device_test_filename(FuUtil *self,
			     FuUtilDeviceTestHelper *helper,
			     const gchar *filename,
			     FwupdJsonObject *json_object_result,
			     GError **error)
{
	gint64 repeat = 1;
	gboolean interactive = FALSE;
	g_autoptr(FwupdJsonArray) json_archs_cpu = NULL;
	g_autoptr(FwupdJsonArray) json_archs_plat = NULL;
	g_autoptr(FwupdJsonArray) json_steps = NULL;
	g_autoptr(FwupdJsonArray) json_steps_result = fwupd_json_array_new();
	g_autoptr(FwupdJsonNode) json_node = NULL;
	g_autoptr(FwupdJsonObject) json_obj = NULL;
	g_autoptr(FwupdJsonParser) json_parser = fwupd_json_parser_new();
	g_autoptr(GBytes) blob = NULL;

	/* set appropriate limits */
	fwupd_json_parser_set_max_depth(json_parser, 10);
	fwupd_json_parser_set_max_items(json_parser, 100);
	fwupd_json_parser_set_max_quoted(json_parser, 10000);

	/* log */
	fwupd_json_object_add_string(json_object_result, "filename", filename);

	/* parse JSON */
	blob = fu_bytes_get_contents(filename, error);
	if (blob == NULL)
		return FALSE;
	json_node =
	    fwupd_json_parser_load_from_bytes(json_parser, blob, FWUPD_JSON_LOAD_FLAG_NONE, error);
	if (json_node == NULL) {
		g_prefix_error_literal(error, "test not in JSON format: ");
		return FALSE;
	}
	json_obj = fwupd_json_node_get_object(json_node, error);
	if (json_obj == NULL)
		return FALSE;

	/* some elements are optional */
	helper->name = fwupd_json_object_get_string(json_obj, "name", NULL);
	if (helper->name != NULL)
		fwupd_json_object_add_string(json_object_result, "name", helper->name);
	if (!fwupd_json_object_get_boolean_with_default(json_obj,
							"interactive",
							&interactive,
							FALSE,
							error))
		return FALSE;
	fwupd_json_object_add_boolean(json_object_result, "interactive", interactive);

	json_archs_cpu = fwupd_json_object_get_array(json_obj, "cpu-architectures", NULL);
	if (json_archs_cpu != NULL) {
		gboolean matched = FALSE;
		const gchar *arch = g_hash_table_lookup(helper->report_metadata, "CpuArchitecture");
		for (guint i = 0; i < fwupd_json_array_get_size(json_archs_cpu); i++) {
			const gchar *arch_tmp;
			arch_tmp = fwupd_json_array_get_string(json_archs_cpu, i, error);
			if (arch_tmp == NULL)
				return FALSE;
			if (g_strcmp0(arch, arch_tmp) == 0) {
				matched = TRUE;
				break;
			}
		}
		if (!matched) {
			helper->nr_skipped++;
			return TRUE;
		}
	}

	json_archs_plat = fwupd_json_object_get_array(json_obj, "platform-architectures", NULL);
	if (json_archs_plat != NULL) {
		gboolean matched = FALSE;
		const gchar *arch =
		    g_hash_table_lookup(helper->report_metadata, "PlatformArchitecture");
		for (guint i = 0; i < fwupd_json_array_get_size(json_archs_plat); i++) {
			const gchar *arch_tmp;
			arch_tmp = fwupd_json_array_get_string(json_archs_plat, i, error);
			if (arch_tmp == NULL)
				return FALSE;
			if (g_strcmp0(arch, arch_tmp) == 0) {
				matched = TRUE;
				break;
			}
		}
		if (!matched) {
			helper->nr_skipped++;
			return TRUE;
		}
	}

	/* process each step */
	if (!fwupd_json_object_get_integer_with_default(json_obj, "repeat", &repeat, 1, error))
		return FALSE;

	json_steps = fwupd_json_object_get_array(json_obj, "steps", error);
	if (json_steps == NULL)
		return FALSE;
	fwupd_json_object_add_array(json_object_result, "steps", json_steps_result);
	for (guint j = 0; j < repeat; j++) {
		for (guint i = 0; i < fwupd_json_array_get_size(json_steps); i++) {
			g_autoptr(FwupdJsonObject) json_step = NULL;
			g_autoptr(FwupdJsonObject) json_step_result = fwupd_json_object_new();

			json_step = fwupd_json_array_get_object(json_steps, i, error);
			if (json_step == NULL)
				return FALSE;
			fwupd_json_array_add_object(json_steps_result, json_step_result);
			if (!fu_util_device_test_step(self,
						      helper,
						      json_step,
						      json_step_result,
						      error))
				return FALSE;
		}
	}

	/* success */
	return TRUE;
}

typedef struct {
	FuUtil *self;
	gchar *inhibit_id;
} FuUtilInhibitHelper;

static void
fu_util_inhibit_helper_free(FuUtilInhibitHelper *helper)
{
	g_free(helper->inhibit_id);
	g_free(helper);
}

G_DEFINE_AUTOPTR_CLEANUP_FUNC(FuUtilInhibitHelper, fu_util_inhibit_helper_free)

static gboolean
fu_util_inhibit_timeout_cb(FuUtilInhibitHelper *helper)
{
	FuUtil *self = helper->self;
	g_autoptr(GError) error_local = NULL;

	if (!fwupd_client_uninhibit(self->client,
				    helper->inhibit_id,
				    self->cancellable,
				    &error_local)) {
		g_warning("failed to auto-uninhibit: %s", error_local->message);
	}
	g_main_loop_quit(self->loop);
	return G_SOURCE_REMOVE;
}

static gboolean
fu_util_inhibit(FuUtil *self, gchar **values, GError **error)
{
	const gchar *reason = "not set";
	guint64 timeout_ms = 0;
	g_autoptr(FuUtilInhibitHelper) helper = g_new0(FuUtilInhibitHelper, 1);
	g_autoptr(GString) str = g_string_new(NULL);

	if (g_strv_length(values) > 0)
		reason = values[0];
	if (g_strv_length(values) > 1) {
		if (!fu_strtoull(values[1],
				 &timeout_ms,
				 0,
				 G_MAXUINT32,
				 FU_INTEGER_BASE_AUTO,
				 error))
			return FALSE;
	}

	/* inhibit then wait */
	helper->self = self;
	helper->inhibit_id = fwupd_client_inhibit(self->client, reason, self->cancellable, error);
	if (helper->inhibit_id == NULL)
		return FALSE;
	if (timeout_ms > 0) {
		g_autoptr(GSource) source = g_timeout_source_new(timeout_ms);
		g_source_set_callback(source,
				      (GSourceFunc)fu_util_inhibit_timeout_cb,
				      helper,
				      NULL);
		g_source_attach(source, self->main_ctx);
	}

	/* TRANSLATORS: the inhibit ID is a short string like dbus-123456 */
	g_string_append_printf(str, _("Inhibit ID is %s."), helper->inhibit_id);
	g_string_append(str, "\n");
	if (timeout_ms > 0) {
		g_string_append_printf(str,
				       /* TRANSLATORS: we can auto-uninhibit after a timeout */
				       _("Automatically uninhibiting in %ums…"),
				       (guint)timeout_ms);
		g_string_append(str, "\n");
	}
	/* TRANSLATORS: CTRL^C [holding control, and then pressing C] will exit the program */
	g_string_append(str, _("Use CTRL^C to cancel."));
	/* TRANSLATORS: this CLI tool is now preventing system updates */
	fu_console_box(self->console, _("System Update Inhibited"), str->str, 80);
	g_main_loop_run(self->loop);
	return TRUE;
}

static gboolean
fu_util_uninhibit(FuUtil *self, gchar **values, GError **error)
{
	/* one argument required */
	if (g_strv_length(values) != 1) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INVALID_ARGS,
				    "Invalid arguments, expected INHIBIT-ID");
		return FALSE;
	}

	/* just uninhibit with the token */
	return fwupd_client_uninhibit(self->client, values[0], self->cancellable, error);
}

typedef struct {
	FuUtil *self;
	const gchar *value;
	FwupdDevice *device; /* no-ref */
} FuUtilWaitHelper;

static void
fu_util_device_wait_added_cb(FwupdClient *client, FwupdDevice *device, FuUtilWaitHelper *helper)
{
	FuUtil *self = helper->self;
	if (g_strcmp0(fwupd_device_get_id(device), helper->value) == 0 ||
	    fwupd_device_has_guid(device, helper->value)) {
		helper->device = device;
		g_main_loop_quit(self->loop);
		return;
	}
}

static gboolean
fu_util_device_wait_timeout_cb(gpointer user_data)
{
	FuUtil *self = (FuUtil *)user_data;
	g_main_loop_quit(self->loop);
	return G_SOURCE_REMOVE;
}

static gboolean
fu_util_device_wait(FuUtil *self, gchar **values, GError **error)
{
	g_autoptr(FwupdDevice) device = NULL;
	g_autoptr(GPtrArray) devices = NULL;
	g_autoptr(GSource) source = g_timeout_source_new_seconds(30);
	g_autoptr(GTimer) timer = g_timer_new();
	FuUtilWaitHelper helper = {.self = self, .value = values[0]};

	/* one argument required */
	if (g_strv_length(values) != 1) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INVALID_ARGS,
				    "Invalid arguments, expected GUID|DEVICE-ID");
		return FALSE;
	}

	/* check if the device already exists */
	device = fwupd_client_get_device_by_id(self->client, helper.value, NULL, NULL);
	if (device != NULL) {
		/* TRANSLATORS: the device is already connected */
		fu_console_print_literal(self->console, _("Device already exists"));
		return TRUE;
	}
	devices = fwupd_client_get_devices_by_guid(self->client, helper.value, NULL, NULL);
	if (devices != NULL) {
		/* TRANSLATORS: the device is already connected */
		fu_console_print_literal(self->console, _("Device already exists"));
		return TRUE;
	}

	/* wait for device to show up */
	fu_console_set_progress(self->console, FWUPD_STATUS_IDLE, 0);
	g_signal_connect(FWUPD_CLIENT(self->client),
			 "device-added",
			 G_CALLBACK(fu_util_device_wait_added_cb),
			 &helper);
	g_source_set_callback(source, fu_util_device_wait_timeout_cb, self, NULL);
	g_source_attach(source, self->main_ctx);
	g_main_loop_run(self->loop);

	/* timed out */
	if (helper.device == NULL) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_INVALID_ARGS,
			    "Stopped waiting for %s after %.0fms",
			    helper.value,
			    g_timer_elapsed(timer, NULL) * 1000.f);
		return FALSE;
	}

	/* success */
	fu_console_print(self->console,
			 /* TRANSLATORS: the device showed up in time */
			 _("Successfully waited %.0fms for device"),
			 g_timer_elapsed(timer, NULL) * 1000.f);
	return TRUE;
}

static gboolean
fu_util_quit(FuUtil *self, gchar **values, GError **error)
{
	/* success */
	return fwupd_client_quit(self->client, self->cancellable, error);
}

static gboolean
fu_util_device_test_full(FuUtil *self,
			 gchar **values,
			 FuUtilDeviceTestHelper *helper,
			 GError **error)
{
	g_autoptr(FwupdJsonArray) json_array_results = fwupd_json_array_new();

	/* required for interactive devices */
	self->current_operation = FU_UTIL_OPERATION_UPDATE;

	/* at least one argument required */
	if (g_strv_length(values) == 0) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INVALID_ARGS,
				    "Invalid arguments");
		return FALSE;
	}

	/* get the report metadata */
	helper->report_metadata =
	    fwupd_client_get_report_metadata(self->client, self->cancellable, error);
	if (helper->report_metadata == NULL)
		return FALSE;

	/* process all the files */
	for (guint i = 0; values[i] != NULL; i++) {
		g_autoptr(FwupdJsonObject) json_object_result = fwupd_json_object_new();
		fwupd_json_array_add_object(json_array_results, json_object_result);
		if (!fu_util_device_test_filename(self,
						  helper,
						  values[i],
						  json_object_result,
						  error)) {
			g_prefix_error(error, "%s failed: ", values[i]);
			return FALSE;
		}
	}

	/* dump to screen as JSON format */
	if (self->as_json) {
		g_autoptr(FwupdJsonObject) json_object_results = fwupd_json_object_new();
		fwupd_json_object_add_array(json_object_results, "results", json_array_results);
		fu_util_print_json_object(self->console, json_object_results);
		return TRUE;
	}

	/* just warning */
	if (helper->nr_skipped > 0) {
		g_autoptr(GString) str = g_string_new(NULL);
		g_string_append_printf(
		    str,
		    /* TRANSLATORS: device tests can be specific to a CPU type */
		    ngettext("%u test was skipped", "%u tests were skipped", helper->nr_skipped),
		    helper->nr_skipped);
		fu_console_print_full(self->console,
				      FU_CONSOLE_PRINT_FLAG_WARNING,
				      "%s\n",
				      str->str);
	}

	/* we need all to pass for a zero return code */
	if (helper->nr_missing > 0) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_NOT_SUPPORTED,
			    "%u devices required for %u tests were not found",
			    helper->nr_missing,
			    g_strv_length(values));
		return FALSE;
	}
	if (helper->nr_success == 0 && helper->nr_skipped == 0) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOT_SUPPORTED,
				    "None of the tests were successful");
		return FALSE;
	}

	/* nag? */
	if (!fu_util_perhaps_show_unreported(self, error))
		return FALSE;

	/* success */
	return TRUE;
}

static gboolean
fu_util_device_emulate(FuUtil *self, gchar **values, GError **error)
{
	g_autoptr(FuUtilDeviceTestHelper) helper = fu_util_device_test_helper_new();
	helper->use_emulation = TRUE;
	self->flags |= FWUPD_INSTALL_FLAG_ONLY_EMULATED;
	self->filter_device_include |= FWUPD_DEVICE_FLAG_EMULATED;
	return fu_util_device_test_full(self, values, helper, error);
}

static gboolean
fu_util_device_test(FuUtil *self, gchar **values, GError **error)
{
	g_autoptr(FuUtilDeviceTestHelper) helper = fu_util_device_test_helper_new();
	self->filter_device_exclude |= FWUPD_DEVICE_FLAG_EMULATED;
	return fu_util_device_test_full(self, values, helper, error);
}

static gboolean
fu_util_download(FuUtil *self, gchar **values, GError **error)
{
	g_autofree gchar *basename = NULL;
	g_autoptr(GBytes) blob = NULL;

	/* one argument required */
	if (g_strv_length(values) != 1) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INVALID_ARGS,
				    "Invalid arguments");
		return FALSE;
	}

	/* file already exists */
	basename = g_path_get_basename(values[0]);
	if ((self->flags & FWUPD_INSTALL_FLAG_FORCE) == 0 &&
	    g_file_test(basename, G_FILE_TEST_EXISTS)) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_INVALID_ARGS,
			    "%s already exists",
			    basename);
		return FALSE;
	}
	blob = fwupd_client_download_bytes(self->client,
					   values[0],
					   self->download_flags,
					   self->cancellable,
					   error);
	if (blob == NULL)
		return FALSE;
	return g_file_set_contents(basename,
				   g_bytes_get_data(blob, NULL),
				   g_bytes_get_size(blob),
				   error);
}

static gboolean
fu_util_local_install(FuUtil *self, gchar **values, GError **error)
{
	const gchar *id;
	g_autofree gchar *filename = NULL;
	g_autoptr(FwupdDevice) dev = NULL;

	/* handle both forms */
	if (g_strv_length(values) == 1) {
		id = FWUPD_DEVICE_ID_ANY;
	} else if (g_strv_length(values) == 2) {
		dev = fu_util_get_device_by_id(self, values[1], error);
		if (dev == NULL)
			return FALSE;
		id = fwupd_device_get_id(dev);
	} else {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INVALID_ARGS,
				    "Invalid arguments");
		return FALSE;
	}

	self->current_operation = FU_UTIL_OPERATION_INSTALL;

	/* install with flags chosen by the user */
	filename = fu_util_download_if_required(self, values[0], error);
	if (filename == NULL)
		return FALSE;

	/* detect bitlocker */
	if (dev != NULL && !self->no_safety_check && !self->assume_yes) {
		if (!fu_util_prompt_warning_fde(self->console, dev, error))
			return FALSE;
	}

	if (!fwupd_client_install(self->client,
				  id,
				  filename,
				  self->flags,
				  self->cancellable,
				  error))
		return FALSE;

	fu_util_display_current_message(self);

	/* we don't want to ask anything */
	if (self->no_reboot_check) {
		g_debug("skipping reboot check");
		return TRUE;
	}

	/* show reboot if needed */
	return fu_util_prompt_complete(self->console, self->completion_flags, TRUE, error);
}

static gboolean
fu_util_get_details(FuUtil *self, gchar **values, GError **error)
{
	g_autoptr(GPtrArray) array = NULL;
	g_autoptr(FuUtilNode) root = g_node_new(NULL);

	/* check args */
	if (g_strv_length(values) != 1) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INVALID_ARGS,
				    "Invalid arguments");
		return FALSE;
	}

	/* implied, important for get-details on a device not in your system */
	self->show_all = TRUE;

	array = fwupd_client_get_details(self->client, values[0], self->cancellable, error);
	if (array == NULL)
		return FALSE;
	if (self->as_json) {
		g_autoptr(FwupdJsonObject) json_obj = fwupd_json_object_new();
		fwupd_codec_array_to_json(array, "Devices", json_obj, FWUPD_CODEC_FLAG_TRUSTED);
		fu_util_print_json_object(self->console, json_obj);
		return TRUE;
	}

	fu_util_build_device_tree(self, root, array, NULL);
	fu_util_print_node(self->console, self->client, root);

	return TRUE;
}

static gboolean
fu_util_report_history_for_remote(FuUtil *self,
				  GPtrArray *devices,
				  FwupdRemote *remote_filter,
				  FwupdRemote *remote_upload,
				  GError **error)
{
	g_autofree gchar *data = NULL;
	g_autofree gchar *report_uri = NULL;
	g_autofree gchar *sig = NULL;
	g_autofree gchar *uri = NULL;
	g_autoptr(GHashTable) metadata = NULL;

	/* convert to JSON */
	metadata = fwupd_client_get_report_metadata(self->client, self->cancellable, error);
	if (metadata == NULL)
		return FALSE;
	data = fwupd_client_build_report_history(self->client,
						 devices,
						 remote_filter,
						 metadata,
						 error);
	if (data == NULL)
		return FALSE;

	/* self sign data */
	if (self->sign) {
		sig = fwupd_client_self_sign(self->client,
					     data,
					     FWUPD_SELF_SIGN_FLAG_ADD_TIMESTAMP,
					     self->cancellable,
					     error);
		if (sig == NULL)
			return FALSE;
	}

	/* ask for permission */
	report_uri = fwupd_remote_build_report_uri(remote_upload, error);
	if (report_uri == NULL)
		return FALSE;
	if (!self->assume_yes &&
	    !fwupd_remote_has_flag(remote_upload, FWUPD_REMOTE_FLAG_AUTOMATIC_REPORTS)) {
		fu_console_print_kv(self->console, _("Target"), report_uri);
		fu_console_print_kv(self->console, _("Payload"), data);
		if (sig != NULL)
			fu_console_print_kv(self->console, _("Signature"), sig);
		if (!fu_console_input_bool(self->console, TRUE, "%s", _("Proceed with upload?"))) {
			g_set_error_literal(error,
					    FWUPD_ERROR,
					    FWUPD_ERROR_PERMISSION_DENIED,
					    "User declined action");
			return FALSE;
		}
	}

	/* POST request and parse reply */
	uri = fwupd_client_upload_report(self->client,
					 report_uri,
					 data,
					 sig,
					 FWUPD_CLIENT_UPLOAD_FLAG_NONE,
					 self->cancellable,
					 error);
	if (uri == NULL)
		return FALSE;

	/* server wanted us to see a message */
	if (g_strcmp0(uri, "") != 0) {
		fu_console_print(
		    self->console,
		    "%s %s",
		    /* TRANSLATORS: the server sent the user a small message */
		    _("Update failure is a known issue, visit this URL for more information:"),
		    uri);
	}

	/* success */
	return TRUE;
}

static gboolean
fu_util_report_history_force(FuUtil *self, GPtrArray *devices, GError **error)
{
	g_autoptr(FwupdRemote) remote_upload = NULL;
	g_autoptr(GString) str = g_string_new(NULL);

	/* just assume every report goes to this remote */
	remote_upload =
	    fwupd_client_get_remote_by_id(self->client, "lvfs", self->cancellable, error);
	if (remote_upload == NULL)
		return FALSE;
	if (!fu_util_report_history_for_remote(self,
					       devices,
					       NULL, /* no filter */
					       remote_upload,
					       error))
		return FALSE;

	/* mark each device as reported */
	for (guint i = 0; i < devices->len; i++) {
		FwupdDevice *device = g_ptr_array_index(devices, i);
		g_debug("setting flag on %s", fwupd_device_get_id(device));
		if (!fwupd_client_modify_device(self->client,
						fwupd_device_get_id(device),
						"Flags",
						"reported",
						self->cancellable,
						error))
			return FALSE;
	}

	/* success */
	g_string_append_printf(str,
			       /* TRANSLATORS: success message -- where the user has uploaded
				* success and/or failure reports to the remote server */
			       ngettext("Successfully uploaded %u report",
					"Successfully uploaded %u reports",
					devices->len),
			       devices->len);
	fu_console_print_literal(self->console, str->str);
	return TRUE;
}

static gboolean
fu_util_report_export(FuUtil *self, gchar **values, GError **error)
{
	g_autoptr(GHashTable) metadata = NULL;
	g_autoptr(GPtrArray) devices_filtered =
	    g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref);
	g_autoptr(GPtrArray) devices = NULL;

	/* get all devices from the history database, then filter them and export to JSON */
	devices = fwupd_client_get_history(self->client, self->cancellable, error);
	if (devices == NULL)
		return FALSE;
	g_debug("%u devices with history", devices->len);

	for (guint i = 0; i < devices->len; i++) {
		FwupdDevice *dev = g_ptr_array_index(devices, i);
		gboolean dev_skip_byid = TRUE;

		/* only process particular DEVICE-ID or GUID if specified */
		for (guint idx = 0; idx < g_strv_length(values); idx++) {
			const gchar *tmpid = values[idx];
			const gchar *device_id = fwupd_device_get_id(dev);
			if (fwupd_device_has_guid(dev, tmpid) || g_strcmp0(device_id, tmpid) == 0) {
				dev_skip_byid = FALSE;
				break;
			}
		}
		if (g_strv_length(values) > 0 && dev_skip_byid)
			continue;

		/* filter, if not forcing */
		if (!fwupd_device_match_flags(dev,
					      self->filter_device_include,
					      self->filter_device_exclude))
			continue;
		if ((self->flags & FWUPD_INSTALL_FLAG_FORCE) == 0) {
			if (fwupd_device_has_flag(dev, FWUPD_DEVICE_FLAG_REPORTED)) {
				g_debug("%s has already been reported", fwupd_device_get_id(dev));
				continue;
			}
		}

		/* only send success and failure */
		if (fwupd_device_get_update_state(dev) != FWUPD_UPDATE_STATE_FAILED &&
		    fwupd_device_get_update_state(dev) != FWUPD_UPDATE_STATE_SUCCESS) {
			g_debug("ignoring %s with UpdateState %s",
				fwupd_device_get_id(dev),
				fwupd_update_state_to_string(fwupd_device_get_update_state(dev)));
			continue;
		}
		g_ptr_array_add(devices_filtered, g_object_ref(dev));
	}

	/* nothing to report, but try harder with --force */
	if (devices_filtered->len == 0 && (self->flags & FWUPD_INSTALL_FLAG_FORCE) == 0) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOTHING_TO_DO,
				    "No reports require exporting");
		return FALSE;
	}

	/* get metadata */
	metadata = fwupd_client_get_report_metadata(self->client, self->cancellable, error);
	if (metadata == NULL)
		return FALSE;

	/* write each device report as a new file */
	for (guint i = 0; i < devices->len; i++) {
		FwupdDevice *dev = g_ptr_array_index(devices, i);
		g_autofree gchar *data = NULL;
		g_autofree gchar *filename = NULL;
		g_autoptr(FuFirmware) archive = fu_zip_firmware_new();
		g_autoptr(FuFirmware) payload_img = fu_zip_file_new();
		g_autoptr(GBytes) payload_blob = NULL;
		g_autoptr(GFile) file = NULL;
		g_autoptr(GPtrArray) devices_tmp = g_ptr_array_new();

		/* convert single device to JSON */
		g_ptr_array_add(devices_tmp, dev);
		data = fwupd_client_build_report_history(self->client,
							 devices,
							 NULL, /* remote */
							 metadata,
							 error);
		if (data == NULL)
			return FALSE;
		payload_blob = g_bytes_new(data, strlen(data));
		fu_firmware_set_id(payload_img, "report.json");
		fu_firmware_set_bytes(payload_img, payload_blob);
		fu_zip_file_set_compression(FU_ZIP_FILE(payload_img), FU_ZIP_COMPRESSION_DEFLATE);
		if (!fu_firmware_add_image(archive, payload_img, error))
			return FALSE;

		/* self sign data */
		if (self->sign) {
			g_autofree gchar *sig = NULL;
			g_autoptr(FuFirmware) sig_img = fu_zip_file_new();
			g_autoptr(GBytes) sig_blob = NULL;

			sig = fwupd_client_self_sign(self->client,
						     data,
						     FWUPD_SELF_SIGN_FLAG_ADD_TIMESTAMP,
						     self->cancellable,
						     error);
			if (sig == NULL)
				return FALSE;
			sig_blob = g_bytes_new(sig, strlen(sig));
			fu_firmware_set_id(sig_img, "report.json.p7c");
			fu_firmware_set_bytes(sig_img, sig_blob);
			fu_zip_file_set_compression(FU_ZIP_FILE(sig_img),
						    FU_ZIP_COMPRESSION_DEFLATE);
			if (!fu_firmware_add_image(archive, sig_img, error))
				return FALSE;
		}

		/* save to local file */
		filename = g_strdup_printf("%s.fwupdreport", fwupd_device_get_id(dev));
		file = g_file_new_for_path(filename);
		if (!fu_firmware_write_file(archive, file, error))
			return FALSE;

		/* TRANSLATORS: key for a offline report filename */
		fu_console_print_kv(self->console, _("Saved report"), filename);
	}

	/* success */
	return TRUE;
}

static gboolean
fu_util_report_history_full(FuUtil *self, gboolean only_automatic_reports, GError **error)
{
	guint cnt = 0;
	g_autoptr(GPtrArray) devices = NULL;
	g_autoptr(GPtrArray) remotes = NULL;

	/* get all devices from the history database, then filter them,
	 * adding to a hash map of report-ids */
	devices = fwupd_client_get_history(self->client, self->cancellable, error);
	if (devices == NULL)
		return FALSE;
	g_debug("%u devices with history", devices->len);

	/* ignore the previous reported flag */
	if (self->flags & FWUPD_INSTALL_FLAG_FORCE) {
		for (guint i = 0; i < devices->len; i++) {
			FwupdDevice *dev = g_ptr_array_index(devices, i);
			fwupd_device_remove_flag(dev, FWUPD_DEVICE_FLAG_REPORTED);
		}
	}

	/* needs an extra action, show something to the user */
	for (guint i = 0; i < devices->len; i++) {
		FwupdDevice *dev = g_ptr_array_index(devices, i);
		if (fwupd_device_has_flag(dev, FWUPD_DEVICE_FLAG_NEEDS_ACTIVATION)) {
			g_autofree gchar *cmd = g_strdup_printf("%s activate", g_get_prgname());
			fu_console_print(
			    self->console,
			    /* TRANSLATORS: %1 is a device name, e.g. "ThinkPad Universal
			     * ThunderBolt 4 Dock" and %2 is "fwupdmgr activate" */
			    _("%s is pending activation; use %s to complete the update."),
			    fwupd_device_get_name(dev),
			    cmd);
		}
	}

	/* get all remotes */
	remotes = fwupd_client_get_remotes(self->client, self->cancellable, error);
	if (remotes == NULL)
		return FALSE;
	for (guint i = 0; i < remotes->len; i++) {
		FwupdRemote *remote = g_ptr_array_index(remotes, i);
		g_autoptr(GError) error_local = NULL;

		/* filter this so we can use it from fwupd-refresh */
		if (only_automatic_reports &&
		    !fwupd_remote_has_flag(remote, FWUPD_REMOTE_FLAG_AUTOMATIC_REPORTS)) {
			g_debug("%s has no AutomaticReports set", fwupd_remote_get_id(remote));
			continue;
		}

		/* try to upload */
		if (!fu_util_report_history_for_remote(self,
						       devices,
						       remote, /* filter */
						       remote, /* upload */
						       &error_local)) {
			if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO))
				continue;
			g_propagate_error(error, g_steal_pointer(&error_local));
			return FALSE;
		}

		/* keep track to make sure *something* worked */
		cnt += 1;
	}

	/* nothing to report, but try harder with --force */
	if (cnt == 0) {
		if (!only_automatic_reports && self->flags & FWUPD_INSTALL_FLAG_FORCE)
			return fu_util_report_history_force(self, devices, error);
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOTHING_TO_DO,
				    "No reports require uploading");
		return FALSE;
	}

	/* mark each device as reported */
	for (guint i = 0; i < devices->len; i++) {
		FwupdDevice *dev = g_ptr_array_index(devices, i);
		g_debug("setting flag on %s", fwupd_device_get_id(dev));
		if (!fwupd_client_modify_device(self->client,
						fwupd_device_get_id(dev),
						"Flags",
						"reported",
						self->cancellable,
						error))
			return FALSE;
	}

	/* TRANSLATORS: where the user has uploaded success and/or failure report to the server */
	fu_console_print_literal(self->console, "Successfully uploaded report");
	return TRUE;
}

static gboolean
fu_util_report_history(FuUtil *self, gchar **values, GError **error)
{
	if (values != NULL && g_strv_length(values) != 0) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INVALID_ARGS,
				    "Invalid arguments");
		return FALSE;
	}
	return fu_util_report_history_full(self, FALSE, error);
}

static gboolean
fu_util_get_history(FuUtil *self, gchar **values, GError **error)
{
	g_autoptr(GPtrArray) devices = NULL;
	g_autoptr(FuUtilNode) root = g_node_new(NULL);

	/* get all devices from the history database */
	devices = fwupd_client_get_history(self->client, self->cancellable, error);
	if (devices == NULL)
		return FALSE;

	/* not for human consumption */
	if (self->as_json) {
		g_autoptr(FwupdJsonObject) json_obj = fwupd_json_object_new();
		fwupd_codec_array_to_json(devices, "Devices", json_obj, FWUPD_CODEC_FLAG_TRUSTED);
		fu_util_print_json_object(self->console, json_obj);
		return TRUE;
	}

	/* show each device */
	for (guint i = 0; i < devices->len; i++) {
		FwupdDevice *dev = g_ptr_array_index(devices, i);
		FwupdRelease *rel;
		FuUtilNode *child;

		if (!fwupd_device_match_flags(dev,
					      self->filter_device_include,
					      self->filter_device_exclude))
			continue;
		child = g_node_append_data(root, g_object_ref(dev));

		rel = fwupd_device_get_release_default(dev);
		if (rel == NULL)
			continue;
		g_node_append_data(child, g_object_ref(rel));
	}

	fu_util_print_node(self->console, self->client, root);

	return TRUE;
}

static FwupdDevice *
fu_util_get_device_by_id(FuUtil *self, const gchar *id, GError **error)
{
	if (fwupd_guid_is_valid(id)) {
		g_autoptr(GPtrArray) devices = NULL;
		devices =
		    fwupd_client_get_devices_by_guid(self->client, id, self->cancellable, error);
		if (devices == NULL)
			return NULL;
		return fu_util_prompt_for_device(self, devices, error);
	}
	/* did this look like a GUID? */
	for (guint i = 0; id[i] != '\0'; i++) {
		if (id[i] == '-') {
			g_set_error_literal(error,
					    FWUPD_ERROR,
					    FWUPD_ERROR_INVALID_ARGS,
					    "Invalid arguments");
			return NULL;
		}
	}
	return fwupd_client_get_device_by_id(self->client, id, self->cancellable, error);
}

static FwupdDevice *
fu_util_get_device_or_prompt(FuUtil *self, gchar **values, GError **error)
{
	g_autoptr(GPtrArray) devices = NULL;

	/* get device to use */
	if (g_strv_length(values) >= 1) {
		if (g_strv_length(values) > 1) {
			for (guint i = 1; i < g_strv_length(values); i++)
				g_debug("ignoring extra input %s", values[i]);
		}
		return fu_util_get_device_by_id(self, values[0], error);
	}

	if (self->as_json) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INVALID_ARGS,
				    "device ID required");
		return NULL;
	}

	/* get all devices from daemon */
	devices = fwupd_client_get_devices(self->client, self->cancellable, error);
	if (devices == NULL)
		return NULL;
	return fu_util_prompt_for_device(self, devices, error);
}

static FwupdRelease *
fu_util_get_release_for_device_version(FuUtil *self,
				       FwupdDevice *device,
				       const gchar *version,
				       GError **error)
{
	g_autoptr(GPtrArray) releases = NULL;

	/* get all releases */
	releases = fwupd_client_get_releases(self->client,
					     fwupd_device_get_id(device),
					     self->cancellable,
					     error);
	if (releases == NULL)
		return NULL;

	/* find using vercmp */
	for (guint j = 0; j < releases->len; j++) {
		FwupdRelease *release = g_ptr_array_index(releases, j);
		if (fu_version_compare(fwupd_release_get_version(release),
				       version,
				       fwupd_device_get_version_format(device)) == 0) {
			return g_object_ref(release);
		}
	}

	/* did not find */
	g_set_error(error,
		    FWUPD_ERROR,
		    FWUPD_ERROR_NOT_SUPPORTED,
		    "Unable to locate release %s for %s",
		    version,
		    fwupd_device_get_name(device));
	return NULL;
}

static gboolean
fu_util_clear_results(FuUtil *self, gchar **values, GError **error)
{
	g_autoptr(FwupdDevice) dev = NULL;

	dev = fu_util_get_device_or_prompt(self, values, error);
	if (dev == NULL)
		return FALSE;

	return fwupd_client_clear_results(self->client,
					  fwupd_device_get_id(dev),
					  self->cancellable,
					  error);
}

static gboolean
fu_util_verify_update(FuUtil *self, gchar **values, GError **error)
{
	g_autoptr(FwupdDevice) dev = NULL;

	self->filter_device_include |= FWUPD_DEVICE_FLAG_CAN_VERIFY;
	dev = fu_util_get_device_or_prompt(self, values, error);
	if (dev == NULL)
		return FALSE;

	if (!fwupd_client_verify_update(self->client,
					fwupd_device_get_id(dev),
					self->cancellable,
					error)) {
		g_prefix_error(error, "failed to verify update %s: ", fwupd_device_get_name(dev));
		return FALSE;
	}

	/* TRANSLATORS: success message when user refreshes device checksums */
	fu_console_print_literal(self->console, _("Successfully updated device checksums"));

	return TRUE;
}

static gboolean
fu_util_download_metadata_enable_lvfs(FuUtil *self, GError **error)
{
	g_autoptr(FwupdRemote) remote = NULL;

	/* is the LVFS available but disabled? */
	remote = fwupd_client_get_remote_by_id(self->client, "lvfs", self->cancellable, error);
	if (remote == NULL)
		return TRUE;
	fu_console_print_literal(
	    self->console,
	    /* TRANSLATORS: explain why no metadata available */
	    _("No remotes are currently enabled so no metadata is available."));
	fu_console_print_literal(
	    self->console,
	    /* TRANSLATORS: explain why no metadata available */
	    _("Metadata can be obtained from the Linux Vendor Firmware Service."));

	/* TRANSLATORS: Turn on the remote */
	if (!fu_console_input_bool(self->console, TRUE, "%s", _("Enable this remote?")))
		return TRUE;
	if (!fwupd_client_modify_remote(self->client,
					fwupd_remote_get_id(remote),
					"Enabled",
					"true",
					self->cancellable,
					error))
		return FALSE;
	if (!fu_util_modify_remote_warning(self->console, remote, self->assume_yes, error))
		return FALSE;

	/* refresh the newly-enabled remote */
	return fwupd_client_refresh_remote(self->client,
					   remote,
					   self->download_flags,
					   self->cancellable,
					   error);
}

static gboolean
fu_util_check_oldest_remote(FuUtil *self, guint64 *age_oldest, GError **error)
{
	g_autoptr(GPtrArray) remotes = NULL;
	gboolean checked = FALSE;

	/* get the age of the oldest enabled remotes */
	remotes = fwupd_client_get_remotes(self->client, self->cancellable, error);
	if (remotes == NULL)
		return FALSE;
	for (guint i = 0; i < remotes->len; i++) {
		FwupdRemote *remote = g_ptr_array_index(remotes, i);
		if (!fwupd_remote_has_flag(remote, FWUPD_REMOTE_FLAG_ENABLED))
			continue;
		if (fwupd_remote_get_kind(remote) != FWUPD_REMOTE_KIND_DOWNLOAD)
			continue;
		checked = TRUE;
		if (!fwupd_remote_needs_refresh(remote))
			continue;
		g_debug("%s is age %u",
			fwupd_remote_get_id(remote),
			(guint)fwupd_remote_get_age(remote));
		if (fwupd_remote_get_age(remote) > *age_oldest)
			*age_oldest = fwupd_remote_get_age(remote);
	}
	if (!checked) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOTHING_TO_DO,
				    /* TRANSLATORS: error message for a user who ran fwupdmgr
				       refresh recently but no remotes */
				    "No remotes enabled.");
		return FALSE;
	}
	return TRUE;
}

static gboolean
fu_util_download_metadata(FuUtil *self, GError **error)
{
	gboolean download_remote_enabled = FALSE;
	guint devices_supported_cnt = 0;
	guint devices_updatable_cnt = 0;
	guint refresh_cnt = 0;
	g_autoptr(GPtrArray) devs = NULL;
	g_autoptr(GPtrArray) remotes = NULL;
	g_autoptr(GError) error_local = NULL;

	remotes = fwupd_client_get_remotes(self->client, self->cancellable, error);
	if (remotes == NULL)
		return FALSE;
	for (guint i = 0; i < remotes->len; i++) {
		FwupdRemote *remote = g_ptr_array_index(remotes, i);
		if (!fwupd_remote_has_flag(remote, FWUPD_REMOTE_FLAG_ENABLED))
			continue;
		if (fwupd_remote_get_kind(remote) != FWUPD_REMOTE_KIND_DOWNLOAD)
			continue;
		download_remote_enabled = TRUE;
		if (fwupd_remote_has_flag(remote, FWUPD_REMOTE_FLAG_REQUIRES_AUTH) &&
		    fwupd_remote_get_username(remote) == NULL &&
		    fwupd_remote_get_password(remote) == NULL) {
			g_debug("skipping remote %s as auth required but not supplied",
				fwupd_remote_get_id(remote));
			continue;
		}
		if ((self->flags & FWUPD_INSTALL_FLAG_FORCE) == 0 &&
		    !fwupd_remote_needs_refresh(remote)) {
			g_debug("skipping as remote %s age is %us",
				fwupd_remote_get_id(remote),
				(guint)fwupd_remote_get_age(remote));
			continue;
		}
		if (!self->as_json)
			fu_console_print(self->console,
					 "%s %s",
					 _("Updating"),
					 fwupd_remote_get_id(remote));
		if (!fwupd_client_refresh_remote(self->client,
						 remote,
						 self->download_flags,
						 self->cancellable,
						 error))
			return FALSE;
		refresh_cnt++;
	}

	/* no web remote is declared; try to enable LVFS */
	if (!download_remote_enabled) {
		/* we don't want to ask anything */
		if (self->no_remote_check) {
			g_debug("skipping remote check");
			return TRUE;
		}

		if (!fu_util_download_metadata_enable_lvfs(self, error))
			return FALSE;
	}

	/* metadata refreshed recently */
	if ((self->flags & FWUPD_INSTALL_FLAG_FORCE) == 0 && refresh_cnt == 0) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_NOTHING_TO_DO,
			    /* TRANSLATORS: error message for a user who ran fwupdmgr
			     * refresh recently -- %1 is '--force' */
			    _("Metadata is up to date; use %s to refresh again."),
			    "--force");
		return FALSE;
	}

	if (self->as_json)
		return TRUE;

	/* get devices from daemon */
	devs = fwupd_client_get_devices(self->client, self->cancellable, error);
	if (devs == NULL)
		return FALSE;

	/* get results */
	for (guint i = 0; i < devs->len; i++) {
		FwupdDevice *dev = g_ptr_array_index(devs, i);
		if (fwupd_device_has_flag(dev, FWUPD_DEVICE_FLAG_SUPPORTED))
			devices_supported_cnt++;
		if (fwupd_device_has_flag(dev, FWUPD_DEVICE_FLAG_UPDATABLE))
			devices_updatable_cnt++;
	}

	/* TRANSLATORS: success message -- where 'metadata' is information
	 * about available firmware on the remote server */
	fu_console_print_literal(self->console, _("Successfully downloaded new metadata:"));

	if (devices_updatable_cnt == 0) {
		fu_console_print_full(
		    self->console,
		    FU_CONSOLE_PRINT_FLAG_LIST_ITEM | FU_CONSOLE_PRINT_FLAG_NEWLINE,
		    "%s",
		    /* TRANSLATORS: no devices that can be upgraded with new firmware */
		    _("No devices are updatable"));
	} else {
		fu_console_print_full(self->console,
				      FU_CONSOLE_PRINT_FLAG_LIST_ITEM |
					  FU_CONSOLE_PRINT_FLAG_NEWLINE,
				      /* TRANSLATORS: how many devices could be updated in theory if
					 we had the firmware locally */
				      ngettext("%u device is updatable",
					       "%u devices are updatable",
					       devices_updatable_cnt),
				      devices_updatable_cnt);
		fu_console_print_full(self->console,
				      FU_CONSOLE_PRINT_FLAG_LIST_ITEM |
					  FU_CONSOLE_PRINT_FLAG_NEWLINE,
				      /* TRANSLATORS: how many devices have published updates on
					 something like the LVFS */
				      ngettext("%u device is supported in the enabled remotes (an "
					       "update has been published)",
					       "%u devices are supported in the enabled remotes "
					       "(an update has been published)",
					       devices_supported_cnt),
				      devices_supported_cnt);
	}

	/* auto-upload any reports */
	if (!fu_util_report_history_full(self, TRUE, &error_local)) {
		if (!g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO)) {
			g_propagate_error(error, g_steal_pointer(&error_local));
			return FALSE;
		}
		g_debug("failed to auto-upload reports: %s", error_local->message);
	}

	/* success */
	return TRUE;
}

static gboolean
fu_util_refresh(FuUtil *self, gchar **values, GError **error)
{
	if (g_strv_length(values) == 0)
		return fu_util_download_metadata(self, error);
	if (g_strv_length(values) != 3) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INVALID_ARGS,
				    "Invalid arguments");
		return FALSE;
	}

	/* open file */
	if (!fwupd_client_update_metadata(self->client,
					  values[2],
					  values[0],
					  values[1],
					  self->cancellable,
					  error))
		return FALSE;

	if (self->as_json)
		return TRUE;

	/* TRANSLATORS: success message -- the user can do this by-hand too */
	fu_console_print_literal(self->console, _("Successfully refreshed metadata manually"));
	return TRUE;
}

static gboolean
fu_util_get_results(FuUtil *self, gchar **values, GError **error)
{
	g_autofree gchar *tmp = NULL;
	g_autoptr(FwupdDevice) dev = NULL;
	g_autoptr(FwupdDevice) rel = NULL;

	dev = fu_util_get_device_or_prompt(self, values, error);
	if (dev == NULL)
		return FALSE;

	rel = fwupd_client_get_results(self->client,
				       fwupd_device_get_id(dev),
				       self->cancellable,
				       error);
	if (rel == NULL)
		return FALSE;
	if (self->as_json) {
		g_autoptr(FwupdJsonObject) json_obj = fwupd_json_object_new();
		fwupd_codec_to_json(FWUPD_CODEC(rel), json_obj, FWUPD_CODEC_FLAG_TRUSTED);
		fu_util_print_json_object(self->console, json_obj);
		return TRUE;
	}
	tmp = fu_util_device_to_string(self->client, rel, 0);
	fu_console_print_literal(self->console, tmp);
	return TRUE;
}

static gboolean
fu_util_get_releases(FuUtil *self, gchar **values, GError **error)
{
	g_autoptr(FwupdDevice) dev = NULL;
	g_autoptr(GPtrArray) rels = NULL;

	self->filter_device_include |= FWUPD_DEVICE_FLAG_SUPPORTED;
	dev = fu_util_get_device_or_prompt(self, values, error);
	if (dev == NULL)
		return FALSE;

	/* get the releases for this device */
	rels = fwupd_client_get_releases(self->client,
					 fwupd_device_get_id(dev),
					 self->cancellable,
					 error);
	if (rels == NULL)
		return FALSE;

	/* not for human consumption */
	if (self->as_json) {
		fu_util_get_releases_as_json(self, rels);
		return TRUE;
	}

	if (rels->len == 0) {
		/* TRANSLATORS: no repositories to download from */
		fu_console_print_literal(self->console, _("No releases available"));
		return TRUE;
	}
	if (g_log_get_debug_enabled()) {
		for (guint i = 0; i < rels->len; i++) {
			FwupdRelease *rel = g_ptr_array_index(rels, i);
			g_autofree gchar *tmp = NULL;
			if (!fwupd_release_match_flags(rel,
						       self->filter_release_include,
						       self->filter_release_exclude))
				continue;
			tmp = fwupd_codec_to_string(FWUPD_CODEC(rel));
			fu_console_print_literal(self->console, tmp);
		}
	} else {
		g_autoptr(FuUtilNode) root = g_node_new(NULL);
		for (guint i = 0; i < rels->len; i++) {
			FwupdRelease *rel = g_ptr_array_index(rels, i);
			if (!fwupd_release_match_flags(rel,
						       self->filter_release_include,
						       self->filter_release_exclude))
				continue;
			g_node_append_data(root, g_object_ref(rel));
		}
		fu_util_print_node(self->console, self->client, root);
	}

	return TRUE;
}

static gboolean
fu_util_search(FuUtil *self, gchar **values, GError **error)
{
	g_autoptr(GPtrArray) rels = NULL;

	/* sanity check */
	if (g_strv_length(values) < 1) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INVALID_ARGS,
				    "Invalid arguments, expected WORD");
		return FALSE;
	}

	/* get the releases for this device */
	rels = fwupd_client_search(self->client, values[0], self->cancellable, error);
	if (rels == NULL)
		return FALSE;

	/* not for human consumption */
	if (self->as_json) {
		fu_util_get_releases_as_json(self, rels);
		return TRUE;
	}

	if (rels->len == 0) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOT_FOUND,
				    /* TRANSLATORS: no repositories to download from */
				    _("No matching releases for search token"));
		return FALSE;
	}
	if (g_log_get_debug_enabled()) {
		for (guint i = 0; i < rels->len; i++) {
			FwupdRelease *rel = g_ptr_array_index(rels, i);
			g_autofree gchar *tmp = NULL;
			if (!fwupd_release_match_flags(rel,
						       self->filter_release_include,
						       self->filter_release_exclude))
				continue;
			tmp = fwupd_codec_to_string(FWUPD_CODEC(rel));
			fu_console_print_literal(self->console, tmp);
		}
	} else {
		g_autoptr(FuUtilNode) root = g_node_new(NULL);
		for (guint i = 0; i < rels->len; i++) {
			FwupdRelease *rel = g_ptr_array_index(rels, i);
			if (!fwupd_release_match_flags(rel,
						       self->filter_release_include,
						       self->filter_release_exclude))
				continue;
			g_node_append_data(root, g_object_ref(rel));
		}
		fu_util_print_node(self->console, self->client, root);
	}

	return TRUE;
}

static FwupdRelease *
fu_util_prompt_for_release(FuUtil *self, GPtrArray *rels_unfiltered, GError **error)
{
	FwupdRelease *rel;
	guint idx;
	g_autoptr(GPtrArray) rels = NULL;

	/* filter */
	rels = fwupd_release_array_filter_flags(rels_unfiltered,
						self->filter_release_include,
						self->filter_release_exclude,
						error);
	if (rels == NULL)
		return NULL;

	/* exactly one */
	if (rels->len == 1) {
		rel = g_ptr_array_index(rels, 0);
		return g_object_ref(rel);
	}

	/* TRANSLATORS: this is to abort the interactive prompt */
	fu_console_print(self->console, "0.\t%s", _("Cancel"));
	for (guint i = 0; i < rels->len; i++) {
		FwupdRelease *rel_tmp = g_ptr_array_index(rels, i);
		fu_console_print(self->console,
				 "%u.\t%s",
				 i + 1,
				 fwupd_release_get_version(rel_tmp));
	}
	/* TRANSLATORS: get interactive prompt */
	idx = fu_console_input_uint(self->console, rels->len, "%s", _("Choose release"));
	if (idx == 0) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOTHING_TO_DO,
				    "Request canceled");
		return NULL;
	}
	rel = g_ptr_array_index(rels, idx - 1);
	return g_object_ref(rel);
}

static gboolean
fu_util_verify(FuUtil *self, gchar **values, GError **error)
{
	g_autoptr(FwupdDevice) dev = NULL;

	self->filter_device_include |= FWUPD_DEVICE_FLAG_CAN_VERIFY;
	dev = fu_util_get_device_or_prompt(self, values, error);
	if (dev == NULL)
		return FALSE;

	if (!fwupd_client_verify(self->client,
				 fwupd_device_get_id(dev),
				 self->cancellable,
				 error)) {
		g_prefix_error(error, "failed to verify %s: ", fwupd_device_get_name(dev));
		return FALSE;
	}

	/* TRANSLATORS: success message when user verified device checksums */
	fu_console_print_literal(self->console, _("Successfully verified device checksums"));

	return TRUE;
}

static gboolean
fu_util_unlock(FuUtil *self, gchar **values, GError **error)
{
	g_autoptr(FwupdDevice) dev = NULL;

	self->filter_device_include |= FWUPD_DEVICE_FLAG_LOCKED;
	dev = fu_util_get_device_or_prompt(self, values, error);
	if (dev == NULL)
		return FALSE;

	if (!fwupd_client_unlock(self->client, fwupd_device_get_id(dev), self->cancellable, error))
		return FALSE;

	/* check flags after unlocking in case the operation changes them */
	if (fwupd_device_has_flag(dev, FWUPD_DEVICE_FLAG_NEEDS_SHUTDOWN))
		self->completion_flags |= FWUPD_DEVICE_FLAG_NEEDS_SHUTDOWN;
	if (fwupd_device_has_flag(dev, FWUPD_DEVICE_FLAG_NEEDS_REBOOT))
		self->completion_flags |= FWUPD_DEVICE_FLAG_NEEDS_REBOOT;

	return fu_util_prompt_complete(self->console, self->completion_flags, TRUE, error);
}

static gboolean
fu_util_perhaps_refresh_remotes(FuUtil *self, GError **error)
{
	guint64 age_oldest = 0;
	const guint64 age_limit_days = 30;

	/* we don't want to ask anything */
	if (self->no_metadata_check || self->as_json) {
		g_debug("skipping metadata check");
		return TRUE;
	}

	if (!fu_util_check_oldest_remote(self, &age_oldest, NULL))
		return TRUE;

	/* metadata is new enough */
	if (age_oldest < 60 * 60 * 24 * age_limit_days)
		return TRUE;

	/* ask for permission */
	if (!self->assume_yes) {
		fu_console_print(
		    self->console,
		    /* TRANSLATORS: the metadata is very out of date; %u is a number > 1 */
		    ngettext("Firmware metadata has not been updated for %u"
			     " day and may not be up to date.",
			     "Firmware metadata has not been updated for %u"
			     " days and may not be up to date.",
			     (gint)age_limit_days),
		    (guint)age_limit_days);
		if (!fu_console_input_bool(self->console,
					   FALSE,
					   "%s (%s)",
					   /* TRANSLATORS: ask if we can update metadata */
					   _("Update now?"),
					   /* TRANSLATORS: metadata is downloaded */
					   _("Requires internet connection")))
			return TRUE;
	}

	/* downloads new metadata */
	return fu_util_download_metadata(self, error);
}

static void
fu_util_get_updates_as_json(FuUtil *self, GPtrArray *devices)
{
	g_autoptr(FwupdJsonObject) json_obj = fwupd_json_object_new();
	g_autoptr(FwupdJsonArray) json_arr = fwupd_json_array_new();

	for (guint i = 0; i < devices->len; i++) {
		FwupdDevice *dev = g_ptr_array_index(devices, i);
		g_autoptr(GPtrArray) rels = NULL;
		g_autoptr(GError) error_local = NULL;
		g_autoptr(FwupdJsonObject) json_obj_tmp = fwupd_json_object_new();

		/* not going to have results, so save a D-Bus round-trip */
		if (!fwupd_device_has_flag(dev, FWUPD_DEVICE_FLAG_SUPPORTED))
			continue;

		/* get the releases for this device and filter for validity */
		rels = fwupd_client_get_upgrades(self->client,
						 fwupd_device_get_id(dev),
						 self->cancellable,
						 &error_local);
		if (rels == NULL) {
			g_debug("no upgrades: %s", error_local->message);
			continue;
		}
		for (guint j = 0; j < rels->len; j++) {
			FwupdRelease *rel = g_ptr_array_index(rels, j);
			if (!fwupd_release_match_flags(rel,
						       self->filter_release_include,
						       self->filter_release_exclude))
				continue;
			fwupd_device_add_release(dev, rel);
		}

		/* add to builder */
		fwupd_codec_to_json(FWUPD_CODEC(dev), json_obj_tmp, FWUPD_CODEC_FLAG_TRUSTED);
		fwupd_json_array_add_object(json_arr, json_obj_tmp);
	}
	fwupd_json_object_add_array(json_obj, "Devices", json_arr);
	fu_util_print_json_object(self->console, json_obj);
}

static gboolean
fu_util_get_updates(FuUtil *self, gchar **values, GError **error)
{
	g_autoptr(GPtrArray) devices = NULL;
	gboolean supported = FALSE;
	g_autoptr(FuUtilNode) root = g_node_new(NULL);
	g_autoptr(GPtrArray) devices_no_support = g_ptr_array_new();
	g_autoptr(GPtrArray) devices_no_upgrades = g_ptr_array_new();

	/* are the remotes very old */
	if (!fu_util_perhaps_refresh_remotes(self, error))
		return FALSE;

	/* handle both forms */
	if (g_strv_length(values) == 0) {
		devices = fwupd_client_get_devices(self->client, self->cancellable, error);
		if (devices == NULL)
			return FALSE;
	} else {
		devices = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref);
		for (guint idx = 0; idx < g_strv_length(values); idx++) {
			FwupdDevice *device = fu_util_get_device_by_id(self, values[idx], error);
			if (device == NULL) {
				g_set_error(error,
					    FWUPD_ERROR,
					    FWUPD_ERROR_INVALID_ARGS,
					    "'%s' is not a valid GUID nor DEVICE-ID",
					    values[idx]);
				return FALSE;
			}
			g_ptr_array_add(devices, device);
		}
	}
	g_ptr_array_sort(devices, fu_util_sort_devices_by_flags_cb);

	/* not for human consumption */
	if (self->as_json) {
		fu_util_get_updates_as_json(self, devices);
		return TRUE;
	}

	for (guint i = 0; i < devices->len; i++) {
		FwupdDevice *dev = g_ptr_array_index(devices, i);
		g_autoptr(GPtrArray) rels = NULL;
		g_autoptr(GError) error_local = NULL;
		FuUtilNode *child;

		/* not going to have results, so save a D-Bus round-trip */
		if (!fwupd_device_has_flag(dev, FWUPD_DEVICE_FLAG_UPDATABLE) &&
		    !fwupd_device_has_flag(dev, FWUPD_DEVICE_FLAG_UPDATABLE_HIDDEN))
			continue;
		if (!fwupd_device_match_flags(dev,
					      self->filter_device_include,
					      self->filter_device_exclude))
			continue;
		if (!fwupd_device_has_flag(dev, FWUPD_DEVICE_FLAG_SUPPORTED)) {
			g_ptr_array_add(devices_no_support, dev);
			continue;
		}
		supported = TRUE;

		/* get the releases for this device and filter for validity */
		rels = fwupd_client_get_upgrades(self->client,
						 fwupd_device_get_id(dev),
						 self->cancellable,
						 &error_local);
		if (rels == NULL) {
			g_ptr_array_add(devices_no_upgrades, dev);
			/* discard the actual reason from user, but leave for debugging */
			g_debug("%s", error_local->message);
			continue;
		}
		child = g_node_append_data(root, g_object_ref(dev));

		/* add all releases */
		for (guint j = 0; j < rels->len; j++) {
			FwupdRelease *rel = g_ptr_array_index(rels, j);
			if (!fwupd_release_match_flags(rel,
						       self->filter_release_include,
						       self->filter_release_exclude))
				continue;
			g_node_append_data(child, g_object_ref(rel));
		}
	}

	/* devices that have no updates available for whatever reason */
	if (devices_no_support->len > 0) {
		fu_console_print_literal(self->console,
					 /* TRANSLATORS: message letting the user know no device
					  * upgrade available due to missing on LVFS */
					 _("Devices with no available firmware updates:"));
		for (guint i = 0; i < devices_no_support->len; i++) {
			FwupdDevice *dev = g_ptr_array_index(devices_no_support, i);
			fu_console_print(self->console, " • %s", fwupd_device_get_name(dev));
		}
	}
	if (devices_no_upgrades->len > 0) {
		fu_console_print_literal(
		    self->console,
		    /* TRANSLATORS: message letting the user know no device upgrade available */
		    _("Devices with the latest available firmware version:"));
		for (guint i = 0; i < devices_no_upgrades->len; i++) {
			FwupdDevice *dev = g_ptr_array_index(devices_no_upgrades, i);
			fu_console_print(self->console, " • %s", fwupd_device_get_name(dev));
		}
	}

	/* nag? */
	if (!fu_util_perhaps_show_unreported(self, error))
		return FALSE;

	/* no devices supported by LVFS or all are filtered */
	if (!supported) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOTHING_TO_DO,
				    /* TRANSLATORS: this is an error string */
				    _("No updatable devices"));
		return FALSE;
	}
	/* no updates available */
	if (g_node_n_nodes(root, G_TRAVERSE_ALL) <= 1) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOTHING_TO_DO,
				    /* TRANSLATORS: this is an error string */
				    _("No updates available"));
		return FALSE;
	}

	fu_util_print_node(self->console, self->client, root);

	/* success */
	return TRUE;
}

static gboolean
fu_util_get_remotes(FuUtil *self, gchar **values, GError **error)
{
	g_autoptr(FuUtilNode) root = g_node_new(NULL);
	g_autoptr(GPtrArray) remotes = NULL;

	remotes = fwupd_client_get_remotes(self->client, self->cancellable, error);
	if (remotes == NULL)
		return FALSE;
	if (self->as_json) {
		g_autoptr(FwupdJsonObject) json_obj = fwupd_json_object_new();
		fwupd_codec_array_to_json(remotes, "Remotes", json_obj, FWUPD_CODEC_FLAG_TRUSTED);
		fu_util_print_json_object(self->console, json_obj);
		return TRUE;
	}

	if (remotes->len == 0) {
		/* TRANSLATORS: no repositories to download from */
		fu_console_print_literal(self->console, _("No remotes available"));
		return TRUE;
	}

	for (guint i = 0; i < remotes->len; i++) {
		FwupdRemote *remote_tmp = g_ptr_array_index(remotes, i);
		g_node_append_data(root, g_object_ref(remote_tmp));
	}
	fu_util_print_node(self->console, self->client, root);

	return TRUE;
}

static FwupdRelease *
fu_util_get_release_with_tag(FuUtil *self, FwupdDevice *dev, const gchar *host_bkc, GError **error)
{
	g_autoptr(GPtrArray) rels = NULL;
	g_auto(GStrv) host_bkcs = g_strsplit(host_bkc, ",", -1);

	/* find the newest release that matches */
	rels = fwupd_client_get_releases(self->client,
					 fwupd_device_get_id(dev),
					 self->cancellable,
					 error);
	if (rels == NULL)
		return NULL;
	for (guint i = 0; i < rels->len; i++) {
		FwupdRelease *rel = g_ptr_array_index(rels, i);
		if (!fwupd_release_match_flags(rel,
					       self->filter_release_include,
					       self->filter_release_exclude))
			continue;
		for (guint j = 0; host_bkcs[j] != NULL; j++) {
			if (fwupd_release_has_tag(rel, host_bkcs[j]))
				return g_object_ref(rel);
		}
	}

	/* no match */
	g_set_error_literal(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_NOT_SUPPORTED,
			    "no matching releases for device");
	return NULL;
}

static FwupdRelease *
fu_util_get_release_with_branch(FuUtil *self, FwupdDevice *dev, const gchar *branch, GError **error)
{
	g_autoptr(GPtrArray) rels = NULL;

	/* find the newest release that matches */
	rels = fwupd_client_get_releases(self->client,
					 fwupd_device_get_id(dev),
					 self->cancellable,
					 error);
	if (rels == NULL)
		return NULL;
	for (guint i = 0; i < rels->len; i++) {
		FwupdRelease *rel = g_ptr_array_index(rels, i);
		if (!fwupd_release_match_flags(rel,
					       self->filter_release_include,
					       self->filter_release_exclude))
			continue;
		if (g_strcmp0(branch, fwupd_release_get_branch(rel)) == 0)
			return g_object_ref(rel);
	}

	/* no match */
	g_set_error_literal(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_NOT_SUPPORTED,
			    "no matching releases for device");
	return NULL;
}

static gboolean
fu_util_prompt_warning_bkc(FuUtil *self, FwupdDevice *dev, FwupdRelease *rel, GError **error)
{
	const gchar *host_bkc = fwupd_client_get_host_bkc(self->client);
	g_autofree gchar *cmd = g_strdup_printf("%s sync", g_get_prgname());
	g_autoptr(FwupdRelease) rel_bkc = NULL;
	g_autoptr(GError) error_local = NULL;
	g_autoptr(GString) str = g_string_new(NULL);

	/* nothing to do */
	if (host_bkc == NULL)
		return TRUE;

	/* get the release that corresponds with the host BKC */
	rel_bkc = fu_util_get_release_with_tag(self, dev, host_bkc, &error_local);
	if (rel_bkc == NULL) {
		if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED) ||
		    g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO)) {
			g_debug("ignoring %s: %s", fwupd_device_get_id(dev), error_local->message);
			return TRUE;
		}
		g_propagate_error(error, g_steal_pointer(&error_local));
		return FALSE;
	}

	/* device is already on a different release */
	if (g_strcmp0(fwupd_device_get_version(dev), fwupd_release_get_version(rel)) != 0)
		return TRUE;

	/* TRANSLATORS: BKC is the industry name for the best known configuration and is a set
	 * of firmware that works together */
	g_string_append_printf(str, _("Your system is set up to the BKC of %s."), host_bkc);
	g_string_append(str, "\n\n");
	g_string_append_printf(
	    str,
	    /* TRANSLATORS: %1 is the current device version number, and %2 is the
	       command name, e.g. `fwupdmgr sync` */
	    _("This device will be reverted back to %s when the %s command is performed."),
	    fwupd_release_get_version(rel),
	    cmd);

	fu_console_box(
	    self->console,
	    /* TRANSLATORS: the best known configuration is a set of software that we know works
	     * well together. In the OEM and ODM industries it is often called a BKC */
	    _("Deviate from the best known configuration?"),
	    str->str,
	    80);

	/* TRANSLATORS: prompt to apply the update */
	if (!fu_console_input_bool(self->console, TRUE, "%s", _("Perform operation?"))) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOTHING_TO_DO,
				    "Request canceled");
		return FALSE;
	}

	/* success */
	return TRUE;
}

static gboolean
fu_util_prompt_warning_composite(FuUtil *self, FwupdDevice *dev, FwupdRelease *rel, GError **error)
{
	const gchar *rel_csum;
	g_autoptr(GPtrArray) devices = NULL;

	/* get the default checksum */
	rel_csum = fwupd_checksum_get_best(fwupd_release_get_checksums(rel));
	if (rel_csum == NULL) {
		g_debug("no checksum for release!");
		return TRUE;
	}

	/* find other devices matching the composite ID and the release checksum */
	devices = fwupd_client_get_devices(self->client, self->cancellable, error);
	if (devices == NULL)
		return FALSE;
	for (guint i = 0; i < devices->len; i++) {
		FwupdDevice *dev_tmp = g_ptr_array_index(devices, i);
		g_autoptr(GError) error_local = NULL;
		g_autoptr(GPtrArray) rels = NULL;

		/* not the parent device */
		if (g_strcmp0(fwupd_device_get_id(dev), fwupd_device_get_id(dev_tmp)) == 0)
			continue;

		/* not the same composite device */
		if (g_strcmp0(fwupd_device_get_composite_id(dev),
			      fwupd_device_get_composite_id(dev_tmp)) != 0)
			continue;

		/* get releases */
		if (!fwupd_device_has_flag(dev_tmp, FWUPD_DEVICE_FLAG_UPDATABLE))
			continue;
		rels = fwupd_client_get_releases(self->client,
						 fwupd_device_get_id(dev_tmp),
						 self->cancellable,
						 &error_local);
		if (rels == NULL) {
			g_debug("ignoring: %s", error_local->message);
			continue;
		}

		/* do any releases match this checksum */
		for (guint j = 0; j < rels->len; j++) {
			FwupdRelease *rel_tmp = g_ptr_array_index(rels, j);
			g_autofree gchar *title = NULL;
			gint vercmp;

			if (!fwupd_release_has_checksum(rel_tmp, rel_csum))
				continue;
			vercmp = fu_version_compare(fwupd_release_get_version(rel_tmp),
						    fu_device_get_version(dev_tmp),
						    fwupd_device_get_version_format(dev_tmp));
			if ((self->flags & FWUPD_INSTALL_FLAG_ALLOW_REINSTALL) == 0 && vercmp == 0)
				continue;
			title = g_strdup_printf("%s %s",
						fwupd_client_get_host_vendor(self->client),
						fwupd_client_get_host_product(self->client));
			if (!fu_util_prompt_warning(self->console, dev_tmp, rel_tmp, title, error))
				return FALSE;
			break;
		}
	}

	/* success */
	return TRUE;
}

static gboolean
fu_util_update_device_with_release(FuUtil *self,
				   FwupdDevice *dev,
				   FwupdRelease *rel,
				   GError **error)
{
	if (!fwupd_device_has_flag(dev, FWUPD_DEVICE_FLAG_UPDATABLE)) {
		const gchar *name = fwupd_device_get_name(dev);
		g_autoptr(GPtrArray) array = fu_util_device_problems_to_strings(self->client, dev);

		/* enumerate each problem to the console */
		fu_console_print(self->console,
				 /* TRANSLATORS: there are reasons the device can't update */
				 _("%s is not currently updatable:"),
				 name);
		for (guint i = 0; i < array->len; i++) {
			const gchar *str = g_ptr_array_index(array, i);
			fu_console_print_full(self->console,
					      FU_CONSOLE_PRINT_FLAG_LIST_ITEM |
						  FU_CONSOLE_PRINT_FLAG_NEWLINE,
					      "%s",
					      str);
		}
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOTHING_TO_DO,
				    /* TRANSLATORS: no update can be installed */
				    _("Nothing to do"));
		return FALSE;
	}
	if (!self->as_json && !self->no_safety_check && !self->assume_yes) {
		const gchar *title = fwupd_client_get_host_product(self->client);
		if (!fu_util_prompt_warning(self->console, dev, rel, title, error))
			return FALSE;
		if (!fu_util_prompt_warning_fde(self->console, dev, error))
			return FALSE;
		if (!fu_util_prompt_warning_composite(self, dev, rel, error))
			return FALSE;
		if (!fu_util_prompt_warning_bkc(self, dev, rel, error))
			return FALSE;
	}
	return fwupd_client_install_release(self->client,
					    dev,
					    rel,
					    self->flags,
					    self->download_flags,
					    self->cancellable,
					    error);
}

static gboolean
fu_util_maybe_send_reports(FuUtil *self, FwupdRelease *rel, GError **error)
{
	g_autoptr(FwupdRemote) remote = NULL;
	g_autoptr(GError) error_local = NULL;
	if (fwupd_release_get_remote_id(rel) == NULL) {
		g_debug("not sending reports, no remote");
		return TRUE;
	}
	remote = fwupd_client_get_remote_by_id(self->client,
					       fwupd_release_get_remote_id(rel),
					       self->cancellable,
					       error);
	if (remote == NULL)
		return FALSE;
	if (fwupd_remote_has_flag(remote, FWUPD_REMOTE_FLAG_AUTOMATIC_REPORTS)) {
		if (!fu_util_report_history(self, NULL, &error_local))
			if (!g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED))
				g_warning("%s", error_local->message);
	}

	return TRUE;
}

static gboolean
fu_util_update(FuUtil *self, gchar **values, GError **error)
{
	gboolean supported = FALSE;
	g_autoptr(GPtrArray) devices = NULL;
	g_autoptr(GPtrArray) devices_latest = g_ptr_array_new();
	g_autoptr(GPtrArray) devices_pending = g_ptr_array_new();
	g_autoptr(GPtrArray) devices_unsupported = g_ptr_array_new();

	if (self->flags & FWUPD_INSTALL_FLAG_ALLOW_OLDER) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INVALID_ARGS,
				    "--allow-older is not supported for this command");
		return FALSE;
	}

	if (self->flags & FWUPD_INSTALL_FLAG_ALLOW_REINSTALL) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INVALID_ARGS,
				    "--allow-reinstall is not supported for this command");
		return FALSE;
	}

	/* DEVICE-ID and GUID are acceptable args to update */
	for (guint idx = 0; idx < g_strv_length(values); idx++) {
		if (!fwupd_guid_is_valid(values[idx]) && !fwupd_device_id_is_valid(values[idx])) {
			g_set_error(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INVALID_ARGS,
				    "'%s' is not a valid GUID nor DEVICE-ID",
				    values[idx]);
			return FALSE;
		}
	}

	/* get devices from daemon */
	devices = fwupd_client_get_devices(self->client, self->cancellable, error);
	if (devices == NULL)
		return FALSE;
	self->current_operation = FU_UTIL_OPERATION_UPDATE;
	g_ptr_array_sort(devices, fu_util_sort_devices_by_flags_cb);
	for (guint i = 0; i < devices->len; i++) {
		FwupdDevice *dev = g_ptr_array_index(devices, i);
		const gchar *device_id = fwupd_device_get_id(dev);
		g_autoptr(FwupdRelease) rel = NULL;
		g_autoptr(GPtrArray) rels = NULL;
		g_autoptr(GError) error_install = NULL;
		g_autoptr(GError) error_report = NULL;
		gboolean dev_skip_byid = TRUE;
		gboolean ret;

		/* not going to have results, so save a D-Bus round-trip */
		if (!fwupd_device_has_flag(dev, FWUPD_DEVICE_FLAG_UPDATABLE) &&
		    !fwupd_device_has_flag(dev, FWUPD_DEVICE_FLAG_UPDATABLE_HIDDEN))
			continue;
		if (!fwupd_device_has_flag(dev, FWUPD_DEVICE_FLAG_SUPPORTED)) {
			g_ptr_array_add(devices_unsupported, dev);
			continue;
		}

		/* only process particular DEVICE-ID or GUID if specified */
		for (guint idx = 0; idx < g_strv_length(values); idx++) {
			const gchar *tmpid = values[idx];
			if (fwupd_device_has_guid(dev, tmpid) || g_strcmp0(device_id, tmpid) == 0) {
				dev_skip_byid = FALSE;
				break;
			}
		}
		if (g_strv_length(values) > 0 && dev_skip_byid)
			continue;
		if (!fwupd_device_match_flags(dev,
					      self->filter_device_include,
					      self->filter_device_exclude))
			continue;
		supported = TRUE;

		/* get the releases for this device and filter for validity */
		rels = fwupd_client_get_upgrades(self->client,
						 fwupd_device_get_id(dev),
						 self->cancellable,
						 &error_install);
		if (rels == NULL) {
			g_ptr_array_add(devices_latest, dev);
			/* discard the actual reason from user, but leave for debugging */
			g_debug("%s", error_install->message);
			continue;
		}
		for (guint j = 0; j < rels->len; j++) {
			FwupdRelease *rel_tmp = g_ptr_array_index(rels, j);
			if (!fwupd_release_match_flags(rel_tmp,
						       self->filter_release_include,
						       self->filter_release_exclude))
				continue;
			rel = g_object_ref(rel_tmp);
			break;
		}
		if (rel == NULL)
			continue;

		/* something is wrong */
		if (fwupd_device_get_problems(dev) != FWUPD_DEVICE_PROBLEM_NONE) {
			g_ptr_array_add(devices_pending, dev);
			continue;
		}

		ret = fu_util_update_device_with_release(self, dev, rel, &error_install);
		if (!ret &&
		    g_error_matches(error_install, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO)) {
			g_debug("ignoring %s: %s",
				fwupd_device_get_id(dev),
				error_install->message);
			continue;
		}
		if (ret)
			fu_util_display_current_message(self);

		/* send report if we're supposed to */
		if (!fu_util_maybe_send_reports(self, rel, &error_report)) {
			/* install failed, report failed */
			if (!ret) {
				g_warning("%s", error_report->message);
				/* install succeeded, but report failed */
			} else {
				g_propagate_error(error, g_steal_pointer(&error_report));
				return FALSE;
			}
		}

		if (!ret) {
			g_propagate_error(error, g_steal_pointer(&error_install));
			return FALSE;
		}
	}

	/* show warnings */
	if (devices_latest->len > 0 && !self->as_json) {
		fu_console_print_literal(self->console,
					 /* TRANSLATORS: message letting the user know no device
					  * upgrade available */
					 _("Devices with the latest available firmware version:"));
		for (guint i = 0; i < devices_latest->len; i++) {
			FwupdDevice *dev = g_ptr_array_index(devices_latest, i);
			fu_console_print(self->console, " • %s", fwupd_device_get_name(dev));
		}
	}
	if (devices_unsupported->len > 0 && !self->as_json) {
		fu_console_print_literal(self->console,
					 /* TRANSLATORS: message letting the user know no
					  * device upgrade available due to missing on LVFS */
					 _("Devices with no available firmware updates:"));
		for (guint i = 0; i < devices_unsupported->len; i++) {
			FwupdDevice *dev = g_ptr_array_index(devices_unsupported, i);
			fu_console_print(self->console, " • %s", fwupd_device_get_name(dev));
		}
	}
	if (devices_pending->len > 0 && !self->as_json) {
		fu_console_print_literal(self->console,
					 /* TRANSLATORS: message letting the user there is an update
					  * waiting, but there is a reason it cannot be deployed */
					 _("Devices with firmware updates that need user action:"));
		for (guint i = 0; i < devices_pending->len; i++) {
			FwupdDevice *dev = g_ptr_array_index(devices_pending, i);
			fu_console_print(self->console, " • %s", fwupd_device_get_name(dev));
			for (guint j = 0; j < 64; j++) {
				FwupdDeviceProblem problem = (guint64)1 << j;
				g_autofree gchar *desc = NULL;
				if (!fwupd_device_has_problem(dev, problem))
					continue;
				desc = fu_util_device_problem_to_string(self->client, dev, problem);
				if (desc == NULL)
					continue;
				fu_console_print(self->console, "   ‣ %s", desc);
			}
		}
	}

	/* no devices supported by LVFS or all are filtered */
	if (!supported) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOTHING_TO_DO,
				    "No updatable devices");
		return FALSE;
	}

	/* we don't want to ask anything */
	if (self->no_reboot_check || self->as_json) {
		g_debug("skipping reboot check");
		return TRUE;
	}

	return fu_util_prompt_complete(self->console, self->completion_flags, TRUE, error);
}

static gboolean
fu_util_remote_modify(FuUtil *self, gchar **values, GError **error)
{
	g_autoptr(FwupdRemote) remote = NULL;
	if (g_strv_length(values) < 3) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INVALID_ARGS,
				    "Invalid arguments");
		return FALSE;
	}

	/* ensure the remote exists */
	remote = fwupd_client_get_remote_by_id(self->client, values[0], self->cancellable, error);
	if (remote == NULL)
		return FALSE;
	if (!fwupd_client_modify_remote(self->client,
					fwupd_remote_get_id(remote),
					values[1],
					values[2],
					self->cancellable,
					error))
		return FALSE;

	if (self->as_json)
		return TRUE;

	/* TRANSLATORS: success message for a per-remote setting change */
	fu_console_print_literal(self->console, _("Successfully modified remote"));
	return TRUE;
}

static gboolean
fu_util_remote_enable(FuUtil *self, gchar **values, GError **error)
{
	g_autoptr(FwupdRemote) remote = NULL;
	if (g_strv_length(values) != 1) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INVALID_ARGS,
				    "Invalid arguments");
		return FALSE;
	}
	remote = fwupd_client_get_remote_by_id(self->client, values[0], self->cancellable, error);
	if (remote == NULL)
		return FALSE;
	if (!fu_util_modify_remote_warning(self->console, remote, self->assume_yes, error))
		return FALSE;
	if (!fwupd_client_modify_remote(self->client,
					fwupd_remote_get_id(remote),
					"Enabled",
					"true",
					self->cancellable,
					error))
		return FALSE;

	if (self->as_json)
		return TRUE;

	/* ask for permission to refresh */
	if (self->no_remote_check || fwupd_remote_get_kind(remote) != FWUPD_REMOTE_KIND_DOWNLOAD) {
		/* TRANSLATORS: success message */
		fu_console_print_literal(self->console, _("Successfully enabled remote"));
		return TRUE;
	}
	if (!self->assume_yes) {
		if (!fu_console_input_bool(self->console,
					   TRUE,
					   "%s (%s)",
					   /* TRANSLATORS: ask if we can update the metadata */
					   _("Do you want to refresh this remote now?"),
					   /* TRANSLATORS: metadata is downloaded */
					   _("Requires internet connection"))) {
			/* TRANSLATORS: success message */
			fu_console_print_literal(self->console, _("Successfully enabled remote"));
			return TRUE;
		}
	}
	if (!fwupd_client_refresh_remote(self->client,
					 remote,
					 self->download_flags,
					 self->cancellable,
					 error))
		return FALSE;

	/* TRANSLATORS: success message */
	fu_console_print_literal(self->console, _("Successfully enabled and refreshed remote"));
	return TRUE;
}

static gboolean
fu_util_remote_clean(FuUtil *self, gchar **values, GError **error)
{
	g_autoptr(FwupdRemote) remote = NULL;
	if (g_strv_length(values) != 1) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INVALID_ARGS,
				    "Invalid arguments");
		return FALSE;
	}
	remote = fwupd_client_get_remote_by_id(self->client, values[0], self->cancellable, error);
	if (remote == NULL)
		return FALSE;
	if (!fwupd_client_clean_remote(self->client,
				       fwupd_remote_get_id(remote),
				       self->cancellable,
				       error))
		return FALSE;

	if (self->as_json)
		return TRUE;

	/* TRANSLATORS: success message */
	fu_console_print_literal(self->console, _("Successfully cleaned remote"));
	return TRUE;
}

static gboolean
fu_util_remote_disable(FuUtil *self, gchar **values, GError **error)
{
	g_autoptr(FwupdRemote) remote = NULL;

	if (g_strv_length(values) != 1) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INVALID_ARGS,
				    "Invalid arguments");
		return FALSE;
	}

	/* ensure the remote exists */
	remote = fwupd_client_get_remote_by_id(self->client, values[0], self->cancellable, error);
	if (remote == NULL)
		return FALSE;
	if (!fwupd_client_modify_remote(self->client,
					values[0],
					"Enabled",
					"false",
					self->cancellable,
					error))
		return FALSE;

	if (self->as_json)
		return TRUE;

	/* TRANSLATORS: success message */
	fu_console_print_literal(self->console, _("Successfully disabled remote"));

	/* delete the now-unused cache files? */
	if (fwupd_remote_get_kind(remote) == FWUPD_REMOTE_KIND_DOWNLOAD &&
	    fwupd_remote_get_age(remote) != G_MAXUINT64) {
		if (self->assume_yes ||
		    fu_console_input_bool(self->console,
					  FALSE,
					  "%s",
					  /* TRANSLATORS: this is now useless */
					  _("Delete the now-unused remote cache files?"))) {
			if (!fwupd_client_clean_remote(self->client,
						       values[0],
						       self->cancellable,
						       error))
				return FALSE;
			fu_console_print_literal(self->console,
						 /* TRANSLATORS: success message */
						 _("Successfully cleaned remote"));
		}
	}

	/* success */
	return TRUE;
}

static gboolean
fu_util_downgrade(FuUtil *self, gchar **values, GError **error)
{
	gboolean ret;
	g_autoptr(FwupdDevice) dev = NULL;
	g_autoptr(FwupdRelease) rel = NULL;
	g_autoptr(GPtrArray) rels = NULL;
	g_autoptr(GError) error_report = NULL;

	if (self->flags & FWUPD_INSTALL_FLAG_ALLOW_REINSTALL) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INVALID_ARGS,
				    "--allow-reinstall is not supported for this command");
		return FALSE;
	}

	self->filter_device_include |= FWUPD_DEVICE_FLAG_SUPPORTED;
	dev = fu_util_get_device_or_prompt(self, values, error);
	if (dev == NULL)
		return FALSE;

	/* get the releases for this device and filter for validity */
	rels = fwupd_client_get_downgrades(self->client,
					   fwupd_device_get_id(dev),
					   self->cancellable,
					   error);
	if (rels == NULL) {
		g_autofree gchar *downgrade_str =
		    /* TRANSLATORS: message letting the user know no device downgrade available
		     * %1 is the device name */
		    g_strdup_printf(_("No downgrades for %s"), fwupd_device_get_name(dev));
		g_prefix_error(error, "%s: ", downgrade_str); /* nocheck:error */
		return FALSE;
	}

	/* get the chosen release */
	rel = fu_util_prompt_for_release(self, rels, error);
	if (rel == NULL)
		return FALSE;

	/* update the console if composite devices are also updated */
	self->current_operation = FU_UTIL_OPERATION_DOWNGRADE;
	self->flags |= FWUPD_INSTALL_FLAG_ALLOW_OLDER;
	ret = fu_util_update_device_with_release(self, dev, rel, error);
	if (ret)
		fu_util_display_current_message(self);

	/* send report if we're supposed to */
	if (!fu_util_maybe_send_reports(self, rel, &error_report)) {
		/* install failed, report failed */
		if (!ret) {
			g_warning("%s", error_report->message);
			/* install succeeded, but report failed */
		} else {
			g_propagate_error(error, g_steal_pointer(&error_report));
			return FALSE;
		}
	}

	/* install failed */
	if (!ret)
		return FALSE;

	/* we don't want to ask anything */
	if (self->no_reboot_check) {
		g_debug("skipping reboot check");
		return TRUE;
	}

	return fu_util_prompt_complete(self->console, self->completion_flags, TRUE, error);
}

static gboolean
fu_util_reinstall(FuUtil *self, gchar **values, GError **error)
{
	gboolean ret;
	g_autoptr(FwupdRelease) rel = NULL;
	g_autoptr(FwupdDevice) dev = NULL;
	g_autoptr(GError) error_report = NULL;

	self->filter_device_include |= FWUPD_DEVICE_FLAG_SUPPORTED;
	dev = fu_util_get_device_or_prompt(self, values, error);
	if (dev == NULL)
		return FALSE;

	/* try to lookup/match release from client */
	rel =
	    fu_util_get_release_for_device_version(self, dev, fwupd_device_get_version(dev), error);
	if (rel == NULL)
		return FALSE;

	/* update the console if composite devices are also updated */
	self->current_operation = FU_UTIL_OPERATION_INSTALL;
	self->flags |= FWUPD_INSTALL_FLAG_ALLOW_REINSTALL;
	ret = fu_util_update_device_with_release(self, dev, rel, error);
	if (ret)
		fu_util_display_current_message(self);

	/* send report if we're supposed to */
	if (!fu_util_maybe_send_reports(self, rel, &error_report)) {
		/* install failed, report failed */
		if (!ret) {
			g_warning("%s", error_report->message);
			/* install succeeded, but report failed */
		} else {
			g_propagate_error(error, g_steal_pointer(&error_report));
			return FALSE;
		}
	}

	/* install failed */
	if (!ret)
		return FALSE;

	/* we don't want to ask anything */
	if (self->no_reboot_check) {
		g_debug("skipping reboot check");
		return TRUE;
	}

	return fu_util_prompt_complete(self->console, self->completion_flags, TRUE, error);
}

static gboolean
fu_util_install(FuUtil *self, gchar **values, GError **error)
{
	gboolean ret;
	g_autoptr(FwupdDevice) dev = NULL;
	g_autoptr(FwupdRelease) rel = NULL;
	g_autoptr(GError) error_report = NULL;

	/* fall back for CLI compatibility */
	if (g_strv_length(values) >= 1) {
		if (g_file_test(values[0], G_FILE_TEST_EXISTS) || fu_util_is_url(values[0]))
			return fu_util_local_install(self, values, error);
	}

	/* find device */
	self->filter_device_include |= FWUPD_DEVICE_FLAG_SUPPORTED;
	dev = fu_util_get_device_or_prompt(self, values, error);
	if (dev == NULL)
		return FALSE;

	/* find release */
	if (g_strv_length(values) >= 2) {
		rel = fu_util_get_release_for_device_version(self, dev, values[1], error);
		if (rel == NULL)
			return FALSE;
	} else {
		g_autoptr(GPtrArray) rels = NULL;
		rels = fwupd_client_get_releases(self->client,
						 fwupd_device_get_id(dev),
						 self->cancellable,
						 error);
		if (rels == NULL)
			return FALSE;
		rel = fu_util_prompt_for_release(self, rels, error);
		if (rel == NULL)
			return FALSE;
	}

	/* allow all actions */
	self->current_operation = FU_UTIL_OPERATION_INSTALL;
	ret = fu_util_update_device_with_release(self, dev, rel, error);
	if (ret)
		fu_util_display_current_message(self);

	/* send report if we're supposed to */
	if (!fu_util_maybe_send_reports(self, rel, &error_report)) {
		/* install failed, report failed */
		if (!ret) {
			g_warning("%s", error_report->message);
			/* install succeeded, but report failed */
		} else {
			g_propagate_error(error, g_steal_pointer(&error_report));
			return FALSE;
		}
	}

	/* install failed */
	if (!ret)
		return FALSE;

	/* we don't want to ask anything */
	if (self->no_reboot_check) {
		g_debug("skipping reboot check");
		return TRUE;
	}

	return fu_util_prompt_complete(self->console, self->completion_flags, TRUE, error);
}

static gboolean
_g_str_equal0(gconstpointer str1, gconstpointer str2)
{
	return g_strcmp0(str1, str2) == 0;
}

static gboolean
fu_util_switch_branch(FuUtil *self, gchar **values, GError **error)
{
	const gchar *branch;
	gboolean ret;
	g_autoptr(FwupdRelease) rel = NULL;
	g_autoptr(GPtrArray) rels = NULL;
	g_autoptr(GPtrArray) branches = g_ptr_array_new_with_free_func(g_free);
	g_autoptr(FwupdDevice) dev = NULL;
	g_autoptr(GError) error_report = NULL;

	/* find the device and check it has multiple branches */
	self->filter_device_include |= FWUPD_DEVICE_FLAG_HAS_MULTIPLE_BRANCHES;
	self->filter_device_include |= FWUPD_DEVICE_FLAG_UPDATABLE;
	dev = fu_util_get_device_or_prompt(self, values, error);
	if (dev == NULL)
		return FALSE;

	/* get all releases, including the alternate branch versions */
	rels = fwupd_client_get_releases(self->client,
					 fwupd_device_get_id(dev),
					 self->cancellable,
					 error);
	if (rels == NULL)
		return FALSE;

	/* get all the unique branches */
	for (guint i = 0; i < rels->len; i++) {
		FwupdRelease *rel_tmp = g_ptr_array_index(rels, i);
		const gchar *branch_tmp = fwupd_release_get_branch(rel_tmp);
		if (!fwupd_release_match_flags(rel_tmp,
					       self->filter_release_include,
					       self->filter_release_exclude))
			continue;
		if (g_ptr_array_find_with_equal_func(branches, branch_tmp, _g_str_equal0, NULL))
			continue;
		g_ptr_array_add(branches, g_strdup(branch_tmp));
	}

	/* branch name is optional */
	if (g_strv_length(values) > 1) {
		branch = values[1];
	} else if (branches->len == 1) {
		branch = g_ptr_array_index(branches, 0);
	} else {
		guint idx;

		/* TRANSLATORS: this is to abort the interactive prompt */
		fu_console_print(self->console, "0.\t%s", _("Cancel"));
		for (guint i = 0; i < branches->len; i++) {
			const gchar *branch_tmp = g_ptr_array_index(branches, i);
			fu_console_print(self->console,
					 "%u.\t%s",
					 i + 1,
					 fu_util_branch_for_display(branch_tmp));
		}
		/* TRANSLATORS: get interactive prompt, where branch is the
		 * supplier of the firmware, e.g. "non-free" or "free" */
		idx = fu_console_input_uint(self->console, branches->len, "%s", _("Choose branch"));
		if (idx == 0) {
			g_set_error_literal(error,
					    FWUPD_ERROR,
					    FWUPD_ERROR_NOTHING_TO_DO,
					    "Request canceled");
			return FALSE;
		}
		branch = g_ptr_array_index(branches, idx - 1);
	}

	/* sanity check */
	if (g_strcmp0(branch, fwupd_device_get_branch(dev)) == 0) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_NOT_SUPPORTED,
			    "Device %s is already on branch %s",
			    fwupd_device_get_name(dev),
			    fu_util_branch_for_display(branch));
		return FALSE;
	}

	/* the releases are ordered by version */
	for (guint j = 0; j < rels->len; j++) {
		FwupdRelease *rel_tmp = g_ptr_array_index(rels, j);
		if (g_strcmp0(fwupd_release_get_branch(rel_tmp), branch) == 0) {
			rel = g_object_ref(rel_tmp);
			break;
		}
	}
	if (rel == NULL) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_NOT_SUPPORTED,
			    "No releases for branch %s",
			    fu_util_branch_for_display(branch));
		return FALSE;
	}

	/* we're switching branch */
	if (!fu_util_switch_branch_warning(self->console, dev, rel, self->assume_yes, error))
		return FALSE;

	/* update the console if composite devices are also updated */
	self->current_operation = FU_UTIL_OPERATION_INSTALL;
	self->flags |= FWUPD_INSTALL_FLAG_ALLOW_REINSTALL;
	self->flags |= FWUPD_INSTALL_FLAG_ALLOW_BRANCH_SWITCH;
	ret = fu_util_update_device_with_release(self, dev, rel, error);
	if (ret)
		fu_util_display_current_message(self);

	/* send report if we're supposed to */
	if (!fu_util_maybe_send_reports(self, rel, &error_report)) {
		/* install failed, report failed */
		if (!ret) {
			g_warning("%s", error_report->message);
			/* install succeeded, but report failed */
		} else {
			g_propagate_error(error, g_steal_pointer(&error_report));
			return FALSE;
		}
	}

	/* install failed */
	if (!ret)
		return FALSE;

	/* we don't want to ask anything */
	if (self->no_reboot_check) {
		g_debug("skipping reboot check");
		return TRUE;
	}

	return fu_util_prompt_complete(self->console, self->completion_flags, TRUE, error);
}

static gboolean
fu_util_activate(FuUtil *self, gchar **values, GError **error)
{
	g_autoptr(GPtrArray) devices = NULL;
	gboolean has_pending = FALSE;

	/* handle both forms */
	if (g_strv_length(values) == 0) {
		/* activate anything with _NEEDS_ACTIVATION */
		devices = fwupd_client_get_devices(self->client, self->cancellable, error);
		if (devices == NULL)
			return FALSE;
		for (guint i = 0; i < devices->len; i++) {
			FwupdDevice *device = g_ptr_array_index(devices, i);
			if (fwupd_device_has_flag(device, FWUPD_DEVICE_FLAG_NEEDS_ACTIVATION)) {
				has_pending = TRUE;
				break;
			}
		}
	} else if (g_strv_length(values) == 1) {
		FwupdDevice *device = fu_util_get_device_by_id(self, values[0], error);
		if (device == NULL)
			return FALSE;
		devices = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref);
		g_ptr_array_add(devices, device);
		if (fwupd_device_has_flag(device, FWUPD_DEVICE_FLAG_NEEDS_ACTIVATION))
			has_pending = TRUE;
	} else {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INVALID_ARGS,
				    "Invalid arguments");
		return FALSE;
	}

	/* nothing to do */
	if (!has_pending) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOTHING_TO_DO,
				    "No firmware to activate");
		return FALSE;
	}

	/* activate anything with _NEEDS_ACTIVATION */
	for (guint i = 0; i < devices->len; i++) {
		FwupdDevice *device = g_ptr_array_index(devices, i);
		if (!fwupd_device_match_flags(device,
					      self->filter_device_include,
					      self->filter_device_exclude))
			continue;
		if (!fwupd_device_has_flag(device, FWUPD_DEVICE_FLAG_NEEDS_ACTIVATION))
			continue;
		fu_console_print(
		    self->console,
		    "%s %s…",
		    /* TRANSLATORS: shown when shutting down to switch to the new version */
		    _("Activating firmware update for"),
		    fwupd_device_get_name(device));
		if (!fwupd_client_activate(self->client,
					   self->cancellable,
					   fwupd_device_get_id(device),
					   error))
			return FALSE;
	}

	if (self->as_json)
		return TRUE;

	/* TRANSLATORS: success message -- where activation is making the new
	 * firmware take effect, usually after updating offline */
	fu_console_print_literal(self->console, _("Successfully activated all devices"));
	return TRUE;
}

static gboolean
fu_util_set_approved_firmware(FuUtil *self, gchar **values, GError **error)
{
	g_auto(GStrv) checksums = NULL;

	/* check args */
	if (g_strv_length(values) != 1) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INVALID_ARGS,
				    "Invalid arguments: filename or list of checksums expected");
		return FALSE;
	}

	/* filename */
	if (g_file_test(values[0], G_FILE_TEST_EXISTS)) {
		g_autofree gchar *data = NULL;
		if (!g_file_get_contents(values[0], &data, NULL, error))
			return FALSE;
		checksums = g_strsplit(data, "\n", -1);
	} else {
		checksums = g_strsplit(values[0], ",", -1);
	}

	/* call into daemon */
	return fwupd_client_set_approved_firmware(self->client,
						  checksums,
						  self->cancellable,
						  error);
}

static void
fu_util_get_checksums_as_json(FuUtil *self, gchar **csums)
{
	g_autoptr(FwupdJsonObject) json_obj = fwupd_json_object_new();
	g_autoptr(FwupdJsonArray) json_arr = fwupd_json_array_new();

	for (guint i = 0; csums[i] != NULL; i++)
		fwupd_json_array_add_string(json_arr, csums[i]);
	fwupd_json_object_add_array(json_obj, "Checksums", json_arr);
	fu_util_print_json_object(self->console, json_obj);
}

static gboolean
fu_util_get_approved_firmware(FuUtil *self, gchar **values, GError **error)
{
	g_auto(GStrv) checksums = NULL;

	/* check args */
	if (g_strv_length(values) != 0) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INVALID_ARGS,
				    "Invalid arguments: none expected");
		return FALSE;
	}

	/* call into daemon */
	checksums = fwupd_client_get_approved_firmware(self->client, self->cancellable, error);
	if (checksums == NULL)
		return FALSE;
	if (self->as_json) {
		fu_util_get_checksums_as_json(self, checksums);
		return TRUE;
	}
	if (g_strv_length(checksums) == 0) {
		/* TRANSLATORS: approved firmware has been checked by
		 * the domain administrator */
		fu_console_print_literal(self->console, _("There is no approved firmware."));
	} else {
		fu_console_print_literal(
		    self->console,
		    /* TRANSLATORS: approved firmware has been checked by
		     * the domain administrator */
		    ngettext("Approved firmware:", "Approved firmware:", g_strv_length(checksums)));
		for (guint i = 0; checksums[i] != NULL; i++)
			fu_console_print(self->console, " * %s", checksums[i]);
	}
	return TRUE;
}

static gboolean
fu_util_modify_config(FuUtil *self, gchar **values, GError **error)
{
	/* check args */
	if (g_strv_length(values) == 3) {
		if (!fwupd_client_modify_config(self->client,
						values[0],
						values[1],
						values[2],
						self->cancellable,
						error))
			return FALSE;
	} else if (g_strv_length(values) == 2) {
		if (!fwupd_client_modify_config(self->client,
						"fwupd",
						values[0],
						values[1],
						self->cancellable,
						error))
			return FALSE;
	} else {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INVALID_ARGS,
				    "Invalid arguments: [SECTION] KEY VALUE expected");
		return FALSE;
	}

	if (self->as_json)
		return TRUE;

	if (!self->assume_yes) {
		if (!fu_console_input_bool(self->console,
					   FALSE,
					   "%s",
					   /* TRANSLATORS: changes only take effect on restart */
					   _("Restart the daemon to make the change effective?")))
			return TRUE;
	}

	if (!fu_util_quit(self, NULL, error))
		return FALSE;
	if (!fwupd_client_connect(self->client, self->cancellable, error))
		return FALSE;

	/* TRANSLATORS: success message -- a per-system setting value */
	fu_console_print_literal(self->console, _("Successfully modified configuration value"));
	return TRUE;
}

static gboolean
fu_util_reset_config(FuUtil *self, gchar **values, GError **error)
{
	/* check args */
	if (g_strv_length(values) != 1) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INVALID_ARGS,
				    "Invalid arguments: SECTION expected");
		return FALSE;
	}
	if (!fwupd_client_reset_config(self->client, values[0], self->cancellable, error))
		return FALSE;

	if (self->as_json)
		return TRUE;

	if (!self->assume_yes) {
		if (!fu_console_input_bool(self->console,
					   FALSE,
					   "%s",
					   /* TRANSLATORS: changes only take effect on restart */
					   _("Restart the daemon to make the change effective?")))
			return TRUE;
	}
	if (!fu_util_quit(self, NULL, error))
		return FALSE;
	if (!fwupd_client_connect(self->client, self->cancellable, error))
		return FALSE;

	/* TRANSLATORS: success message -- a per-system setting value */
	fu_console_print_literal(self->console, _("Successfully reset configuration values"));
	return TRUE;
}

static FwupdRemote *
fu_util_get_remote_with_report_uri(FuUtil *self, GError **error)
{
	g_autoptr(GPtrArray) remotes = NULL;

	/* get all remotes */
	remotes = fwupd_client_get_remotes(self->client, self->cancellable, error);
	if (remotes == NULL)
		return NULL;

	for (guint i = 0; i < remotes->len; i++) {
		FwupdRemote *remote = g_ptr_array_index(remotes, i);
		if (!fwupd_remote_has_flag(remote, FWUPD_REMOTE_FLAG_ENABLED))
			continue;
		if (fwupd_remote_get_report_uri(remote) != NULL)
			return g_object_ref(remote);
	}

	/* failed */
	g_set_error_literal(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_NOT_SUPPORTED,
			    "No remotes specified ReportURI");
	return NULL;
}

static gboolean
fu_util_upload_security(FuUtil *self, GPtrArray *attrs, GError **error)
{
	g_autofree gchar *data = NULL;
	g_autofree gchar *report_uri = NULL;
	g_autofree gchar *sig = NULL;
	g_autofree gchar *uri = NULL;
	g_autoptr(FwupdRemote) remote = NULL;
	g_autoptr(GError) error_local = NULL;
	g_autoptr(GHashTable) metadata = NULL;

	/* can we find a remote with a security attr */
	remote = fu_util_get_remote_with_report_uri(self, &error_local);
	if (remote == NULL) {
		g_debug("failed to find suitable remote: %s", error_local->message);
		return TRUE;
	}

	/* export as a string */
	metadata = fwupd_client_get_report_metadata(self->client, self->cancellable, error);
	if (metadata == NULL)
		return FALSE;
	data = fwupd_client_build_report_security(self->client, attrs, metadata, error);
	if (data == NULL)
		return FALSE;

	/* ask for permission */
	if (!self->assume_yes &&
	    !fwupd_remote_has_flag(remote, FWUPD_REMOTE_FLAG_AUTOMATIC_SECURITY_REPORTS)) {
		if (!fu_console_input_bool(self->console,
					   FALSE,
					   /* TRANSLATORS: ask the user to share, %s is something
					    * like: "Linux Vendor Firmware Service" */
					   _("Upload these anonymous results to the %s to help "
					     "other users?"),
					   fwupd_remote_get_title(remote))) {
			return TRUE;
		}
	}

	/* self sign data */
	if (self->sign) {
		sig = fwupd_client_self_sign(self->client,
					     data,
					     FWUPD_SELF_SIGN_FLAG_ADD_TIMESTAMP,
					     self->cancellable,
					     error);
		if (sig == NULL)
			return FALSE;
	}

	/* ask for permission */
	if (!self->assume_yes &&
	    !fwupd_remote_has_flag(remote, FWUPD_REMOTE_FLAG_AUTOMATIC_SECURITY_REPORTS)) {
		fu_console_print_kv(self->console,
				    _("Target"),
				    fwupd_remote_get_report_uri(remote));
		fu_console_print_kv(self->console, _("Payload"), data);
		if (sig != NULL)
			fu_console_print_kv(self->console, _("Signature"), sig);
		if (!fu_console_input_bool(self->console, TRUE, "%s", _("Proceed with upload?"))) {
			g_set_error_literal(error,
					    FWUPD_ERROR,
					    FWUPD_ERROR_PERMISSION_DENIED,
					    "User declined action");
			return FALSE;
		}
	}

	/* POST request */
	report_uri = fwupd_remote_build_report_uri(remote, error);
	if (report_uri == NULL)
		return FALSE;
	uri = fwupd_client_upload_report(self->client,
					 report_uri,
					 data,
					 sig,
					 FWUPD_CLIENT_UPLOAD_FLAG_ALWAYS_MULTIPART,
					 self->cancellable,
					 error);
	if (uri == NULL)
		return FALSE;
	fu_console_print_literal(self->console,
				 /* TRANSLATORS: success, so say thank you to the user */
				 _("Host Security ID attributes uploaded successfully, thanks!"));

	/* as this worked, ask if the user want to do this every time */
	if (!fwupd_remote_has_flag(remote, FWUPD_REMOTE_FLAG_AUTOMATIC_SECURITY_REPORTS)) {
		if (fu_console_input_bool(self->console,
					  FALSE,
					  "%s",
					  /* TRANSLATORS: can we JFDI? */
					  _("Automatically upload every time?"))) {
			if (!fwupd_client_modify_remote(self->client,
							fwupd_remote_get_id(remote),
							"AutomaticSecurityReports",
							"true",
							self->cancellable,
							error))
				return FALSE;
		}
	}

	return TRUE;
}

static void
fu_util_security_as_json(FuUtil *self, GPtrArray *attrs, GPtrArray *events, GPtrArray *devices)
{
	g_autoptr(GPtrArray) devices_issues = NULL;
	g_autoptr(FwupdJsonObject) json_obj = fwupd_json_object_new();

	/* attrs */
	fwupd_codec_array_to_json(attrs, "SecurityAttributes", json_obj, FWUPD_CODEC_FLAG_TRUSTED);

	/* events */
	if (events != NULL && events->len > 0) {
		fwupd_codec_array_to_json(events,
					  "SecurityEvents",
					  json_obj,
					  FWUPD_CODEC_FLAG_TRUSTED);
	}

	/* devices */
	devices_issues = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref);
	for (guint i = 0; devices != NULL && i < devices->len; i++) {
		FwupdDevice *device = g_ptr_array_index(devices, i);
		GPtrArray *issues = fwupd_device_get_issues(device);
		if (issues->len == 0)
			continue;
		g_ptr_array_add(devices_issues, g_object_ref(device));
	}
	if (devices_issues->len > 0) {
		fwupd_codec_array_to_json(devices_issues,
					  "Devices",
					  json_obj,
					  FWUPD_CODEC_FLAG_TRUSTED);
	}
	fu_util_print_json_object(self->console, json_obj);
}

static gboolean
fu_util_sync(FuUtil *self, gchar **values, GError **error)
{
	const gchar *host_bkc = fwupd_client_get_host_bkc(self->client);
	guint cnt = 0;
	g_autoptr(GPtrArray) devices = NULL;

	/* update the console if composite devices are also updated */
	self->current_operation = FU_UTIL_OPERATION_INSTALL;
	self->flags |= FWUPD_INSTALL_FLAG_ALLOW_OLDER;

	devices = fwupd_client_get_devices(self->client, NULL, error);
	if (devices == NULL)
		return FALSE;
	for (guint i = 0; i < devices->len; i++) {
		FwupdDevice *dev = g_ptr_array_index(devices, i);
		g_autoptr(FwupdRelease) rel = NULL;
		g_autoptr(GError) error_local = NULL;

		/* find the release that matches the tag */
		if (host_bkc != NULL) {
			rel = fu_util_get_release_with_tag(self, dev, host_bkc, &error_local);
		} else if (fu_device_get_branch(dev) != NULL) {
			rel = fu_util_get_release_with_branch(self,
							      dev,
							      fu_device_get_branch(dev),
							      &error_local);
		} else {
			g_set_error_literal(&error_local,
					    FWUPD_ERROR,
					    FWUPD_ERROR_NOT_SUPPORTED,
					    "No device branch or system HostBkc set");
			/* nocheck:error-false-return */
		}
		if (rel == NULL) {
			if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED) ||
			    g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO)) {
				g_debug("ignoring %s: %s",
					fwupd_device_get_id(dev),
					error_local->message);
				continue;
			}
			g_propagate_error(error, g_steal_pointer(&error_local));
			return FALSE;
		}

		/* ignore if already on that release */
		if (g_strcmp0(fwupd_device_get_version(dev), fwupd_release_get_version(rel)) == 0)
			continue;

		/* install this new release */
		g_debug("need to move %s from %s to %s",
			fwupd_device_get_id(dev),
			fwupd_device_get_version(dev),
			fwupd_release_get_version(rel));
		if (!fu_util_update_device_with_release(self, dev, rel, &error_local)) {
			if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO)) {
				g_debug("ignoring %s: %s",
					fwupd_device_get_id(dev),
					error_local->message);
				continue;
			}
			g_propagate_error(error, g_steal_pointer(&error_local));
			return FALSE;
		}
		fu_util_display_current_message(self);
		cnt++;
	}

	/* nothing was done */
	if (cnt == 0) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOTHING_TO_DO,
				    "No devices required modification");
		return FALSE;
	}

	/* we don't want to ask anything */
	if (self->no_reboot_check) {
		g_debug("skipping reboot check");
		return TRUE;
	}

	/* show reboot if needed */
	return fu_util_prompt_complete(self->console, self->completion_flags, TRUE, error);
}

static gboolean
fu_util_security_fix_attr(FuUtil *self, FwupdSecurityAttr *attr, GError **error)
{
	g_autoptr(GString) body = g_string_new(NULL);
	g_autoptr(GString) title = g_string_new(NULL);

	g_string_append_printf(title,
			       "%s: %s",
			       /* TRANSLATORS: title prefix for the BIOS settings dialog */
			       _("Configuration Change Suggested"),
			       fwupd_security_attr_get_title(attr));

	g_string_append(body, fwupd_security_attr_get_description(attr));

	if (fwupd_security_attr_get_bios_setting_id(attr) != NULL &&
	    fwupd_security_attr_get_bios_setting_current_value(attr) != NULL &&
	    fwupd_security_attr_get_bios_setting_target_value(attr) != NULL) {
		g_string_append(body, "\n\n");
		g_string_append_printf(body,
				       /* TRANSLATORS: the %1 is a BIOS setting name.
					* %2 and %3 are the values, e.g. "True" or "Windows10" */
				       _("This tool can change the BIOS setting '%s' from '%s' "
					 "to '%s' automatically, but it will only be active after "
					 "restarting the computer."),
				       fwupd_security_attr_get_bios_setting_id(attr),
				       fwupd_security_attr_get_bios_setting_current_value(attr),
				       fwupd_security_attr_get_bios_setting_target_value(attr));
		g_string_append(body, "\n\n");
		g_string_append(body,
				/* TRANSLATORS: the user has to manually recover; we can't do it */
				_("You should ensure you are comfortable restoring the setting "
				  "from the system firmware setup, as this change may cause the "
				  "system to not boot into Linux or cause other system "
				  "instability."));
	} else if (fwupd_security_attr_get_kernel_target_value(attr) != NULL) {
		g_string_append(body, "\n\n");
		if (fwupd_security_attr_get_kernel_current_value(attr) != NULL) {
			g_string_append_printf(
			    body,
			    /* TRANSLATORS: the %1 is a kernel command line key=value */
			    _("This tool can change the kernel argument from '%s' to '%s', but "
			      "it will only be active after restarting the computer."),
			    fwupd_security_attr_get_kernel_current_value(attr),
			    fwupd_security_attr_get_kernel_target_value(attr));
		} else {
			g_string_append_printf(
			    body,
			    /* TRANSLATORS: the %1 is a kernel command line key=value */
			    _("This tool can add a kernel argument of '%s', but it will "
			      "only be active after restarting the computer."),
			    fwupd_security_attr_get_kernel_target_value(attr));
		}
		g_string_append(body, "\n\n");
		g_string_append(body,
				/* TRANSLATORS: the user has to manually recover; we can't do it */
				_("You should ensure you are comfortable restoring the setting "
				  "from a recovery or installation disk, as this change may cause "
				  "the system to not boot into Linux or cause other system "
				  "instability."));
	}

	fu_console_box(self->console, title->str, body->str, 80);

	/* TRANSLATORS: prompt to apply the update */
	if (!fu_console_input_bool(self->console, FALSE, "%s", _("Perform operation?")))
		return TRUE;
	if (!fwupd_client_fix_host_security_attr(self->client,
						 fwupd_security_attr_get_appstream_id(attr),
						 self->cancellable,
						 error))
		return FALSE;

	/* do not offer to upload the report */
	self->completion_flags |= FWUPD_DEVICE_FLAG_NEEDS_REBOOT;
	return TRUE;
}

static gboolean
fu_util_security(FuUtil *self, gchar **values, GError **error)
{
	FuSecurityAttrToStringFlags flags = FU_SECURITY_ATTR_TO_STRING_FLAG_NONE;
	g_autoptr(GPtrArray) attrs = NULL;
	g_autoptr(GPtrArray) devices = NULL;
	g_autoptr(GPtrArray) events = NULL;
	g_autoptr(GError) error_local = NULL;
	g_autofree gchar *str = NULL;

#ifndef HAVE_HSI
	g_set_error_literal(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_NOT_SUPPORTED,
			    /* TRANSLATORS: error message for unsupported feature */
			    _("Host Security ID (HSI) is not supported"));
	return FALSE;
#endif /* HAVE_HSI */

	/* the "why" */
	attrs = fwupd_client_get_host_security_attrs(self->client, self->cancellable, error);
	if (attrs == NULL)
		return FALSE;

	/* the "when" */
	events = fwupd_client_get_host_security_events(self->client,
						       10,
						       self->cancellable,
						       &error_local);
	if (events == NULL) {
		if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED)) {
			g_debug("ignoring failed events: %s", error_local->message);
		} else {
			g_propagate_error(error, g_steal_pointer(&error_local));
			return FALSE;
		}
	}

	/* the "also" */
	devices = fwupd_client_get_devices(self->client, self->cancellable, &error_local);
	if (devices == NULL) {
		if (!g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO)) {
			g_propagate_error(error, g_steal_pointer(&error_local));
			return FALSE;
		}
	}

	/* not for human consumption */
	if (self->as_json) {
		fu_util_security_as_json(self, attrs, events, devices);
		return TRUE;
	}

	fu_console_print(self->console,
			 "%s \033[1m%s\033[0m",
			 /* TRANSLATORS: this is a string like 'HSI:2-U' */
			 _("Host Security ID:"),
			 fwupd_client_get_host_security_id(self->client));

	/* show or hide different elements */
	if (self->show_all) {
		flags |= FU_SECURITY_ATTR_TO_STRING_FLAG_SHOW_OBSOLETES;
		flags |= FU_SECURITY_ATTR_TO_STRING_FLAG_SHOW_URLS;
	}
	str = fu_util_security_attrs_to_string(attrs, flags);
	fu_console_print_literal(self->console, str);

	/* events */
	if (events != NULL && events->len > 0) {
		g_autofree gchar *estr = fu_util_security_events_to_string(events, flags);
		if (estr != NULL)
			fu_console_print_literal(self->console, estr);
	}

	/* known CVEs */
	if (devices != NULL && devices->len > 0) {
		g_autofree gchar *estr = fu_util_security_issues_to_string(devices);
		if (estr != NULL)
			fu_console_print_literal(self->console, estr);
	}

	/* host emulation */
	for (guint j = 0; j < attrs->len; j++) {
		FwupdSecurityAttr *attr = g_ptr_array_index(attrs, j);
		if (g_strcmp0(fwupd_security_attr_get_appstream_id(attr),
			      FWUPD_SECURITY_ATTR_ID_HOST_EMULATION) == 0) {
			self->no_unreported_check = TRUE;
			break;
		}
	}

	/* any things we can fix? */
	if (!self->no_security_fix) {
		for (guint j = 0; j < attrs->len; j++) {
			FwupdSecurityAttr *attr = g_ptr_array_index(attrs, j);
			if (fwupd_security_attr_has_flag(attr, FWUPD_SECURITY_ATTR_FLAG_CAN_FIX) &&
			    !fwupd_security_attr_has_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS)) {
				if (!fu_util_security_fix_attr(self, attr, error))
					return FALSE;
			}
		}
	}

	/* upload, with confirmation */
	if (!self->no_unreported_check) {
		if (!fu_util_upload_security(self, attrs, error))
			return FALSE;
	}

	/* reboot is required? */
	if (!self->no_reboot_check &&
	    (self->completion_flags & FWUPD_DEVICE_FLAG_NEEDS_REBOOT) > 0) {
		if (!fu_util_prompt_complete(self->console, self->completion_flags, TRUE, error))
			return FALSE;
	}

	/* success */
	return TRUE;
}

static void
fu_util_ignore_cb(const gchar *log_domain,
		  GLogLevelFlags log_level,
		  const gchar *message,
		  gpointer user_data)
{
}

#ifdef HAVE_GIO_UNIX
static gboolean
fu_util_sigint_cb(gpointer user_data)
{
	FuUtil *self = (FuUtil *)user_data;
	g_debug("handling SIGINT");
	g_cancellable_cancel(self->cancellable);
	return FALSE;
}
#endif

static void
fu_util_setup_signal_handlers(FuUtil *self)
{
#ifdef HAVE_GIO_UNIX
	g_autoptr(GSource) source = g_unix_signal_source_new(SIGINT);
	g_source_set_callback(source, fu_util_sigint_cb, self, NULL);
	g_source_attach(g_steal_pointer(&source), self->main_ctx);
#endif
}

static void
fu_util_private_free(FuUtil *self)
{
	if (self->client != NULL) {
		/* when destroying GDBusProxy in a custom GMainContext, the context must be
		 * iterated enough after finalization of the proxies that any pending D-Bus traffic
		 * can be freed */
		fwupd_client_disconnect(self->client, NULL);
		while (g_main_context_iteration(self->main_ctx, FALSE)) {
			/* nothing needs to be done here */
		};
		g_object_unref(self->client);
	}
	if (self->current_device != NULL)
		g_object_unref(self->current_device);
	g_ptr_array_unref(self->post_requests);
	g_main_loop_unref(self->loop);
	g_main_context_unref(self->main_ctx);
	g_object_unref(self->cancellable);
	g_object_unref(self->console);
	g_option_context_free(self->context);
	g_free(self);
}

static gboolean
fu_util_check_daemon_version(FuUtil *self, GError **error)
{
	const gchar *daemon = fwupd_client_get_daemon_version(self->client);

	if (daemon == NULL) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOT_SUPPORTED,
				    /* TRANSLATORS: error message */
				    _("Unable to connect to service"));
		return FALSE;
	}

	if (g_strcmp0(daemon, PACKAGE_VERSION) != 0) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_NOT_SUPPORTED,
			    /* TRANSLATORS: error message */
			    _("Unsupported daemon version %s, client version is %s"),
			    daemon,
			    PACKAGE_VERSION);
		return FALSE;
	}

	return TRUE;
}

static gboolean
fu_util_check_polkit_actions(GError **error)
{
#ifdef POLKIT_ACTIONDIR
	g_autofree gchar *filename = NULL;

	if (g_getenv("FWUPD_POLKIT_NOCHECK") != NULL)
		return TRUE;

	filename = g_build_filename(POLKIT_ACTIONDIR, "org.freedesktop.fwupd.policy", NULL);
	if (!g_file_test(filename, G_FILE_TEST_IS_REGULAR)) {
		g_set_error_literal(
		    error,
		    FWUPD_ERROR,
		    FWUPD_ERROR_AUTH_FAILED,
		    "PolicyKit files are missing, see "
		    "https://github.com/fwupd/fwupd/wiki/PolicyKit-files-are-missing");
		return FALSE;
	}
#endif

	return TRUE;
}

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunused-function"
G_DEFINE_AUTOPTR_CLEANUP_FUNC(FuUtil, fu_util_private_free)
#pragma clang diagnostic pop

static void
fu_util_show_plugin_warnings(FuUtil *self)
{
	FwupdPluginFlags flags = FWUPD_PLUGIN_FLAG_NONE;
	g_autoptr(GPtrArray) plugins = NULL;

	if (self->as_json)
		return;

	/* get plugins from daemon, ignoring if the daemon is too old */
	plugins = fwupd_client_get_plugins(self->client, self->cancellable, NULL);
	if (plugins == NULL)
		return;

	/* get a superset so we do not show the same message more than once */
	for (guint i = 0; i < plugins->len; i++) {
		FwupdPlugin *plugin = g_ptr_array_index(plugins, i);
		if (fwupd_plugin_has_flag(plugin, FWUPD_PLUGIN_FLAG_DISABLED))
			continue;
		if (!fwupd_plugin_has_flag(plugin, FWUPD_PLUGIN_FLAG_USER_WARNING))
			continue;
		flags |= fwupd_plugin_get_flags(plugin);
	}

	/* never show these, they're way too generic */
	flags &= ~FWUPD_PLUGIN_FLAG_DISABLED;
	flags &= ~FWUPD_PLUGIN_FLAG_NO_HARDWARE;
	flags &= ~FWUPD_PLUGIN_FLAG_REQUIRE_HWID;
	flags &= ~FWUPD_PLUGIN_FLAG_MEASURE_SYSTEM_INTEGRITY;
	flags &= ~FWUPD_PLUGIN_FLAG_READY;

	/* print */
	for (guint i = 0; i < 64; i++) {
		FwupdPluginFlags flag = (guint64)1 << i;
		g_autofree gchar *tmp = NULL;
		g_autofree gchar *url = NULL;
		g_autoptr(GString) str = g_string_new(NULL);
		if ((flags & flag) == 0)
			continue;
		tmp = fu_util_plugin_flag_to_string(flag);
		if (tmp == NULL)
			continue;
		g_string_append_printf(str, "%s\n", tmp);
		url = g_strdup_printf("https://github.com/fwupd/fwupd/wiki/PluginFlag:%s",
				      fwupd_plugin_flag_to_string(flag));
		/* TRANSLATORS: %s is a link to a website */
		g_string_append_printf(str, _("See %s for more information."), url);
		fu_console_print_full(self->console,
				      FU_CONSOLE_PRINT_FLAG_WARNING,
				      "%s\n",
				      str->str);
	}
}

static gboolean
fu_util_set_bios_setting(FuUtil *self, gchar **input, GError **error)
{
	g_autoptr(GHashTable) settings = NULL;

	settings = fu_util_bios_settings_parse_argv(input, error);
	if (settings == NULL)
		return FALSE;
	if (!fwupd_client_modify_bios_setting(self->client, settings, self->cancellable, error)) {
		g_prefix_error_literal(error, "failed to set BIOS setting: ");
		return FALSE;
	}

	if (!self->as_json) {
		gpointer key;
		gpointer value;
		GHashTableIter iter;

		g_hash_table_iter_init(&iter, settings);
		while (g_hash_table_iter_next(&iter, &key, &value)) {
			g_autofree gchar *msg =
			    /* TRANSLATORS: Configured a BIOS setting to a value */
			    g_strdup_printf(_("Set BIOS setting '%s' using '%s'."),
					    (const gchar *)key,
					    (const gchar *)value);
			fu_console_print_literal(self->console, msg);
		}
	}
	self->completion_flags |= FWUPD_DEVICE_FLAG_NEEDS_REBOOT;

	if (self->no_reboot_check) {
		g_debug("skipping reboot check");
		return TRUE;
	}

	return fu_util_prompt_complete(self->console, self->completion_flags, TRUE, error);
}

static gboolean
fu_util_get_bios_setting(FuUtil *self, gchar **values, GError **error)
{
	g_autoptr(GPtrArray) attrs = NULL;
	gboolean found = FALSE;

	attrs = fwupd_client_get_bios_settings(self->client, self->cancellable, error);
	if (attrs == NULL)
		return FALSE;
	if (self->as_json) {
		fu_util_bios_setting_console_print(self->console, values, attrs);
		return TRUE;
	}

	for (guint i = 0; i < attrs->len; i++) {
		FwupdBiosSetting *attr = g_ptr_array_index(attrs, i);
		if (fu_util_bios_setting_matches_args(attr, values)) {
			g_autofree gchar *tmp = fu_util_bios_setting_to_string(attr, 0);
			fu_console_print_literal(self->console, tmp);
			found = TRUE;
		}
	}
	if (attrs->len == 0) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOTHING_TO_DO,
				    /* TRANSLATORS: error message */
				    _("This system doesn't support firmware settings"));
		return FALSE;
	}
	if (!found) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOT_FOUND,
				    /* TRANSLATORS: error message */
				    _("Unable to find attribute"));
		return FALSE;
	}
	return TRUE;
}

static gboolean
fu_util_security_fix(FuUtil *self, gchar **values, GError **error)
{
#ifndef HAVE_HSI
	g_set_error_literal(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_NOT_SUPPORTED,
			    /* TRANSLATORS: error message for unsupported feature */
			    _("Host Security ID (HSI) is not supported"));
	return FALSE;
#endif /* HAVE_HSI */

	if (g_strv_length(values) == 0) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INVALID_ARGS,
				    /* TRANSLATOR: This is the error message for
				     * incorrect parameter */
				    _("Invalid arguments, expected an AppStream ID"));
		return FALSE;
	}
	if (!fwupd_client_fix_host_security_attr(self->client, values[0], self->cancellable, error))
		return FALSE;

	if (self->as_json)
		return TRUE;

	/* TRANSLATORS: we've fixed a security problem on the machine */
	fu_console_print_literal(self->console, _("Fixed successfully"));
	return TRUE;
}

static void
fu_util_hwids_as_json(FuUtil *self, GStrv hwids_keys, GStrv hwids_values)
{
	g_autoptr(FwupdJsonObject) json_obj = fwupd_json_object_new();
	for (guint i = 0; hwids_keys[i] != NULL; i++)
		fwupd_json_object_add_string(json_obj, hwids_keys[i], hwids_values[i]);
	fu_util_print_json_object(self->console, json_obj);
}

static gboolean
fu_util_hwids(FuUtil *self, gchar **values, GError **error)
{
	g_auto(GStrv) hwids_keys = NULL;
	g_auto(GStrv) hwids_values = NULL;

	fwupd_client_get_hwids(self->client, &hwids_keys, &hwids_values);
	if (self->as_json) {
		fu_util_hwids_as_json(self, hwids_keys, hwids_values);
		return TRUE;
	}

	/* show debug output */
	fu_console_print_literal(self->console, "Computer Information");
	fu_console_print_literal(self->console, "--------------------");
	for (guint i = 0; hwids_keys[i] != NULL; i++) {
		if (fwupd_guid_is_valid(hwids_values[i]))
			continue;
		fu_console_print(self->console, "%s: %s", hwids_keys[i], hwids_values[i]);
	}

	/* show GUIDs */
	fu_console_print_literal(self->console, "Hardware IDs");
	fu_console_print_literal(self->console, "------------");
	for (guint i = 0; hwids_keys[i] != NULL; i++) {
		g_autofree gchar *hwids_keys_real = NULL;
		g_auto(GStrv) hwids_keys_strv = NULL;
		if (!fwupd_guid_is_valid(hwids_values[i]))
			continue;
		hwids_keys_strv = g_strsplit(hwids_keys[i], "&", -1);
		hwids_keys_real = g_strjoinv(" + ", hwids_keys_strv);
		fu_console_print(self->console, "{%s}   <- %s", hwids_values[i], hwids_keys_real);
	}

	/* success */
	return TRUE;
}

static gboolean
fu_util_report_devices(FuUtil *self, gchar **values, GError **error)
{
	g_autofree gchar *data = NULL;
	g_autofree gchar *report_uri = NULL;
	g_autofree gchar *uri = NULL;
	g_autoptr(FwupdRemote) remote = NULL;
	g_autoptr(GHashTable) metadata = NULL;
	g_autoptr(GPtrArray) devices = NULL;

	/* we only know how to upload to the LVFS */
	remote = fwupd_client_get_remote_by_id(self->client, "lvfs", self->cancellable, error);
	if (remote == NULL)
		return FALSE;
	report_uri = fwupd_remote_build_report_uri(remote, error);
	if (report_uri == NULL)
		return FALSE;

	/* include all the devices */
	devices = fwupd_client_get_devices(self->client, self->cancellable, error);
	if (devices == NULL)
		return FALSE;
	metadata = fwupd_client_get_report_metadata(self->client, self->cancellable, error);
	if (metadata == NULL)
		return FALSE;
	data = fwupd_client_build_report_devices(self->client, devices, metadata, error);
	if (data == NULL)
		return FALSE;

	if (self->as_json) {
		if (!self->assume_yes) {
			g_set_error_literal(error,
					    FWUPD_ERROR,
					    FWUPD_ERROR_INVALID_ARGS,
					    "pass --yes to enable uploads");
			return FALSE;
		}
	} else {
		/* show the user the entire data blob */
		fu_console_print_kv(self->console, _("Target"), report_uri);
		fu_console_print_kv(self->console, _("Payload"), data);
		fu_console_print(
		    self->console,
		    /* TRANSLATORS: explain why we want to upload */
		    _("Uploading a device list allows the %s team to know what hardware "
		      "exists, and allows us to put pressure on vendors that do not upload "
		      "firmware updates for their hardware."),
		    fwupd_remote_get_title(remote));
		if (!fu_console_input_bool(self->console,
					   TRUE,
					   "%s (%s)",
					   /* TRANSLATORS: ask the user to upload */
					   _("Upload data now?"),
					   /* TRANSLATORS: metadata is downloaded */
					   _("Requires internet connection"))) {
			g_set_error_literal(error,
					    FWUPD_ERROR,
					    FWUPD_ERROR_NOTHING_TO_DO,
					    "Declined upload");
			return FALSE;
		}
	}

	/* send to the LVFS */
	uri = fwupd_client_upload_report(self->client,
					 report_uri,
					 data,
					 NULL,
					 FWUPD_CLIENT_UPLOAD_FLAG_ALWAYS_MULTIPART,
					 self->cancellable,
					 error);
	if (uri == NULL)
		return FALSE;

	/* success */
	if (!self->as_json) {
		fu_console_print_literal(self->console,
					 /* TRANSLATORS: success, so say thank you to the user */
					 _("Device list uploaded successfully, thanks!"));
	}

	return TRUE;
}

static gboolean
fu_util_security_undo(FuUtil *self, gchar **values, GError **error)
{
#ifndef HAVE_HSI
	g_set_error_literal(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_NOT_SUPPORTED,
			    /* TRANSLATORS: error message for unsupported feature */
			    _("Host Security ID (HSI) is not supported"));
	return FALSE;
#endif /* HAVE_HSI */

	if (g_strv_length(values) == 0) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INVALID_ARGS,
				    /* TRANSLATOR: This is the error message for
				     * incorrect parameter */
				    _("Invalid arguments, expected an AppStream ID"));
		return FALSE;
	}
	if (!fwupd_client_undo_host_security_attr(self->client,
						  values[0],
						  self->cancellable,
						  error))
		return FALSE;

	if (self->as_json)
		return TRUE;

	/* TRANSLATORS: we've fixed a security problem on the machine */
	fu_console_print_literal(self->console, _("Fix reverted successfully"));
	return TRUE;
}

static gboolean
fu_util_emulation_tag(FuUtil *self, gchar **values, GError **error)
{
	g_autoptr(FwupdDevice) dev = NULL;

	/* set the flag */
	self->filter_device_include |= FWUPD_DEVICE_FLAG_CAN_EMULATION_TAG;
	dev = fu_util_get_device_or_prompt(self, values, error);
	if (dev == NULL)
		return FALSE;
	return fwupd_client_modify_device(self->client,
					  fwupd_device_get_id(dev),
					  "Flags",
					  "emulation-tag",
					  self->cancellable,
					  error);
}

static gboolean
fu_util_emulation_untag(FuUtil *self, gchar **values, GError **error)
{
	g_autoptr(FwupdDevice) dev = NULL;

	/* set the flag */
	self->filter_device_include |= FWUPD_DEVICE_FLAG_EMULATION_TAG;
	dev = fu_util_get_device_or_prompt(self, values, error);
	if (dev == NULL)
		return FALSE;
	return fwupd_client_modify_device(self->client,
					  fwupd_device_get_id(dev),
					  "Flags",
					  "~emulation-tag",
					  self->cancellable,
					  error);
}

static gboolean
fu_util_emulation_save(FuUtil *self, gchar **values, GError **error)
{
	/* check args */
	if (g_strv_length(values) != 1) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INVALID_ARGS,
				    "Invalid arguments, expected FILENAME");
		return FALSE;
	}

	/* save */
	return fwupd_client_emulation_save(self->client, values[0], self->cancellable, error);
}

static gboolean
fu_util_emulation_load(FuUtil *self, gchar **values, GError **error)
{
	/* check args */
	if (g_strv_length(values) != 1) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_INVALID_ARGS,
				    "Invalid arguments, expected FILENAME");
		return FALSE;
	}
	return fwupd_client_emulation_load(self->client, values[0], self->cancellable, error);
}

static gboolean
fu_util_enable_remote_auth(FuUtil *self, gchar **values, GError **error)
{
	const gchar *remote_id = "lvfs-embargo";
	g_autoptr(FwupdRemote) remote = NULL;
	g_autofree gchar *username = NULL;
	g_autofree gchar *password = NULL;
	g_autoptr(GError) error_local = NULL;

	/* find remote */
	if (g_strv_length(values) > 0)
		remote_id = values[0];
	remote = fwupd_client_get_remote_by_id(self->client, remote_id, self->cancellable, error);
	if (remote == NULL)
		return FALSE;

	/* get username */
	if (g_strv_length(values) > 1) {
		username = g_strdup(values[1]);
	} else {
		username = fu_console_input_string(self->console,
						   /* TRANSLATORS: remote refers to a website
						      distributing fw, %1 is a name e.g. 'lvfs' */
						   _("Enter your email address for remote '%s'"),
						   remote_id);
		if (username == NULL || username[0] == '\0') {
			g_set_error_literal(error,
					    FWUPD_ERROR,
					    FWUPD_ERROR_INVALID_ARGS,
					    "A username is required");
			return FALSE;
		}
	}

	/* get password */
	if (g_strv_length(values) > 2) {
		password = g_strdup(values[2]);
	} else {
		if (g_strcmp0(remote_id, "lvfs-embargo") == 0) {
			const gchar *uri = "https://fwupd.org/lvfs/profile";
			fu_console_print(
			    self->console,
			    /* TRANSLATORS: token refers to a password thing; %1 is a URL */
			    _("To get a new token please go to %s and click 'Generate Token'"),
			    uri);
		}
		password = fu_console_input_string(self->console,
						   /* TRANSLATORS: remote refers to a website
						      distributing fw, %1 is a name e.g. 'lvfs' */
						   _("Enter your token for remote '%s'"),
						   remote_id);
		if (password == NULL || password[0] == '\0') {
			g_set_error_literal(error,
					    FWUPD_ERROR,
					    FWUPD_ERROR_INVALID_ARGS,
					    "A password is required");
			return FALSE;
		}
	}

	/* check this works */
	fwupd_remote_set_username(remote, username);
	fwupd_remote_set_password(remote, password);
	if (!fwupd_client_refresh_remote(self->client,
					 remote,
					 self->download_flags,
					 self->cancellable,
					 &error_local)) {
		if (!fwupd_remote_has_flag(remote, FWUPD_REMOTE_FLAG_ENABLED) &&
		    !g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED)) {
			g_propagate_error(error, g_steal_pointer(&error_local));
			return FALSE;
		}
		g_debug("ignoring: %s", error_local->message);
	}

	/* save secrets and enable remote */
	if (!fu_util_modify_remote_warning(self->console, remote, self->assume_yes, error))
		return FALSE;
	if (!fwupd_remote_save_user_secrets(remote, error))
		return FALSE;
	if (!fwupd_client_modify_remote(self->client,
					fwupd_remote_get_id(remote),
					"Enabled",
					"true",
					self->cancellable,
					error))
		return FALSE;
	if (!fwupd_client_refresh_remote(self->client,
					 remote,
					 self->download_flags,
					 self->cancellable,
					 error))
		return FALSE;

	/* TRANSLATORS: we've gained access to a new firmware remote */
	fu_console_print_literal(self->console, _("Remote enabled and refreshed successfully"));
	return TRUE;
}

static gboolean
fu_util_version(FuUtil *self, GError **error)
{
	g_autoptr(GHashTable) metadata = NULL;
	g_autofree gchar *str = NULL;

	/* get metadata */
	metadata = fwupd_client_get_report_metadata(self->client, self->cancellable, error);
	if (metadata == NULL)
		return FALSE;

	/* dump to the screen in the most appropriate format */
	if (self->as_json) {
		fu_util_project_versions_as_json(self->console, metadata);
		return TRUE;
	}
	str = fu_util_project_versions_to_string(metadata);
	fu_console_print_literal(self->console, str);
	return TRUE;
}

static gboolean
fu_util_setup_interactive(FuUtil *self, GError **error)
{
	if (self->as_json) {
		g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "using --json");
		return FALSE;
	}
	return fu_console_setup(self->console, error);
}

static void
fu_util_cancelled_cb(GCancellable *cancellable, gpointer user_data)
{
	FuUtil *self = (FuUtil *)user_data;
	if (!g_main_loop_is_running(self->loop))
		return;
	/* TRANSLATORS: this is from ctrl+c */
	fu_console_print_literal(self->console, _("Cancelled"));
	g_main_loop_quit(self->loop);
}

static void
fu_util_print_error(FuUtil *self, const GError *error)
{
	if (self->as_json) {
		fu_util_print_error_as_json(self->console, error);
		return;
	}
	fu_console_print_full(self->console, FU_CONSOLE_PRINT_FLAG_STDERR, "%s\n", error->message);
}

int
main(int argc, char *argv[])
{ /* nocheck:lines */
	gboolean force = FALSE;
	gboolean allow_branch_switch = FALSE;
	gboolean allow_older = FALSE;
	gboolean allow_reinstall = FALSE;
	gboolean only_emulated = FALSE;
	gboolean only_p2p = FALSE;
	gboolean is_interactive = FALSE;
	gboolean no_history = FALSE;
	gboolean no_authenticate = FALSE;
	gboolean ret;
	gboolean verbose = FALSE;
	gboolean version = FALSE;
	guint download_retries = 0;
	g_autoptr(FuUtil) self = g_new0(FuUtil, 1);
	g_autoptr(GDateTime) dt_now = g_date_time_new_now_utc();
	g_autoptr(GError) error = NULL;
	g_autoptr(GError) error_console = NULL;
	g_autoptr(GPtrArray) cmd_array = fu_util_cmd_array_new();
#ifdef HAVE_POLKIT
	g_autoptr(FuPolkitAgent) polkit_agent = fu_polkit_agent_new();
#endif
	g_autofree gchar *cmd_descriptions = NULL;
	g_autofree gchar *filter_device = NULL;
	g_autofree gchar *filter_release = NULL;
	const GOptionEntry options[] = {
	    {"verbose",
	     'v',
	     0,
	     G_OPTION_ARG_NONE,
	     &verbose,
	     /* TRANSLATORS: command line option */
	     N_("Show extra debugging information"),
	     NULL},
	    {"version",
	     '\0',
	     0,
	     G_OPTION_ARG_NONE,
	     &version,
	     /* TRANSLATORS: command line option */
	     N_("Show client and daemon versions"),
	     NULL},
	    {"download-retries",
	     '\0',
	     0,
	     G_OPTION_ARG_INT,
	     &download_retries,
	     /* TRANSLATORS: command line option */
	     N_("Set the download retries for transient errors"),
	     NULL},
	    {"allow-reinstall",
	     '\0',
	     0,
	     G_OPTION_ARG_NONE,
	     &allow_reinstall,
	     /* TRANSLATORS: command line option */
	     N_("Allow reinstalling existing firmware versions"),
	     NULL},
	    {"allow-older",
	     '\0',
	     0,
	     G_OPTION_ARG_NONE,
	     &allow_older,
	     /* TRANSLATORS: command line option */
	     N_("Allow downgrading firmware versions"),
	     NULL},
	    {"allow-branch-switch",
	     '\0',
	     0,
	     G_OPTION_ARG_NONE,
	     &allow_branch_switch,
	     /* TRANSLATORS: command line option */
	     N_("Allow switching firmware branch"),
	     NULL},
	    {"only-emulated",
	     '\0',
	     0,
	     G_OPTION_ARG_NONE,
	     &only_emulated,
	     /* TRANSLATORS: command line option */
	     N_("Only install onto emulated devices"),
	     NULL},
	    {"force",
	     '\0',
	     0,
	     G_OPTION_ARG_NONE,
	     &force,
	     /* TRANSLATORS: command line option */
	     N_("Force the action by relaxing some runtime checks"),
	     NULL},
	    {"assume-yes",
	     'y',
	     0,
	     G_OPTION_ARG_NONE,
	     &self->assume_yes,
	     /* TRANSLATORS: command line option */
	     N_("Answer yes to all questions"),
	     NULL},
	    {"sign",
	     '\0',
	     0,
	     G_OPTION_ARG_NONE,
	     &self->sign,
	     /* TRANSLATORS: command line option */
	     N_("Sign the uploaded data with the client certificate"),
	     NULL},
	    {"no-unreported-check",
	     '\0',
	     0,
	     G_OPTION_ARG_NONE,
	     &self->no_unreported_check,
	     /* TRANSLATORS: command line option */
	     N_("Do not check for unreported history"),
	     NULL},
	    {"no-metadata-check",
	     '\0',
	     0,
	     G_OPTION_ARG_NONE,
	     &self->no_metadata_check,
	     /* TRANSLATORS: command line option */
	     N_("Do not check for old metadata"),
	     NULL},
	    {"no-remote-check",
	     '\0',
	     0,
	     G_OPTION_ARG_NONE,
	     &self->no_remote_check,
	     /* TRANSLATORS: command line option */
	     N_("Do not check if download remotes should be enabled"),
	     NULL},
	    {"no-reboot-check",
	     '\0',
	     0,
	     G_OPTION_ARG_NONE,
	     &self->no_reboot_check,
	     /* TRANSLATORS: command line option */
	     N_("Do not check or prompt for reboot after update"),
	     NULL},
	    {"no-safety-check",
	     '\0',
	     0,
	     G_OPTION_ARG_NONE,
	     &self->no_safety_check,
	     /* TRANSLATORS: command line option */
	     N_("Do not perform device safety checks"),
	     NULL},
	    {"no-device-prompt",
	     '\0',
	     0,
	     G_OPTION_ARG_NONE,
	     &self->no_device_prompt,
	     /* TRANSLATORS: command line option */
	     N_("Do not prompt for devices"),
	     NULL},
	    {"no-history",
	     '\0',
	     0,
	     G_OPTION_ARG_NONE,
	     &no_history,
	     /* TRANSLATORS: command line option */
	     N_("Do not write to the history database"),
	     NULL},
	    {"show-all",
	     '\0',
	     0,
	     G_OPTION_ARG_NONE,
	     &self->show_all,
	     /* TRANSLATORS: command line option */
	     N_("Show all results"),
	     NULL},
	    {"show-all-devices",
	     '\0',
	     G_OPTION_FLAG_HIDDEN,
	     G_OPTION_ARG_NONE,
	     &self->show_all,
	     /* TRANSLATORS: command line option */
	     N_("Show devices that are not updatable"),
	     NULL},
	    {"disable-ssl-strict",
	     '\0',
	     0,
	     G_OPTION_ARG_NONE,
	     &self->disable_ssl_strict,
	     /* TRANSLATORS: command line option */
	     N_("Ignore SSL strict checks when downloading files"),
	     NULL},
	    {"p2p",
	     '\0',
	     0,
	     G_OPTION_ARG_NONE,
	     &only_p2p,
	     /* TRANSLATORS: command line option */
	     N_("Only use peer-to-peer networking when downloading files"),
	     NULL},
	    {"filter",
	     '\0',
	     0,
	     G_OPTION_ARG_STRING,
	     &filter_device,
	     /* TRANSLATORS: command line option */
	     N_("Filter with a set of device flags using a ~ prefix to "
		"exclude, e.g. 'internal,~needs-reboot'"),
	     NULL},
	    {"filter-release",
	     '\0',
	     0,
	     G_OPTION_ARG_STRING,
	     &filter_release,
	     /* TRANSLATORS: command line option */
	     N_("Filter with a set of release flags using a ~ prefix to "
		"exclude, e.g. 'trusted-release,~trusted-metadata'"),
	     NULL},
	    {"json",
	     '\0',
	     0,
	     G_OPTION_ARG_NONE,
	     &self->as_json,
	     /* TRANSLATORS: command line option */
	     N_("Output in JSON format (disables all interactive prompts)"),
	     NULL},
	    {"no-security-fix",
	     '\0',
	     0,
	     G_OPTION_ARG_NONE,
	     &self->no_security_fix,
	     /* TRANSLATORS: command line option */
	     N_("Do not prompt to fix security issues"),
	     NULL},
	    {"no-authenticate",
	     '\0',
	     0,
	     G_OPTION_ARG_NONE,
	     &no_authenticate,
	     /* TRANSLATORS: command line option */
	     N_("Don't prompt for authentication (less details may be shown)"),
	     NULL},
	    {NULL}};
	FwupdFeatureFlags feature_flags =
	    FWUPD_FEATURE_FLAG_CAN_REPORT | FWUPD_FEATURE_FLAG_SWITCH_BRANCH |
	    FWUPD_FEATURE_FLAG_FDE_WARNING | FWUPD_FEATURE_FLAG_COMMUNITY_TEXT |
	    FWUPD_FEATURE_FLAG_SHOW_PROBLEMS;

#ifdef _WIN32
	/* workaround Windows setting the codepage to 1252 */
	(void)g_setenv("LANG", "C.UTF-8", FALSE);
#endif

	setlocale(LC_ALL, "");

	bindtextdomain(GETTEXT_PACKAGE, FWUPD_LOCALEDIR);
	bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8");
	textdomain(GETTEXT_PACKAGE);
	g_set_prgname(fu_util_get_prgname(argv[0]));

	/* ensure D-Bus errors are registered */
	(void)fwupd_error_quark();

	/* create helper object */
	self->main_ctx = g_main_context_new();
	self->loop = g_main_loop_new(self->main_ctx, FALSE);
	self->console = fu_console_new();
	self->post_requests = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref);
	fu_console_set_main_context(self->console, self->main_ctx);

	/* add commands */
	fu_util_cmd_array_add(cmd_array,
			      "check-reboot-needed",
			      _("[DEVICE-ID|GUID]"),
			      /* TRANSLATORS: command description */
			      _("Check if any devices are pending a reboot to complete update"),
			      fu_util_check_reboot_needed);
	fu_util_cmd_array_add(cmd_array,
			      "get-devices,get-topology",
			      NULL,
			      /* TRANSLATORS: command description */
			      _("Get all devices that support firmware updates"),
			      fu_util_get_devices);
	fu_util_cmd_array_add(cmd_array,
			      "get-history",
			      NULL,
			      /* TRANSLATORS: command description */
			      _("Show history of firmware updates"),
			      fu_util_get_history);
	fu_util_cmd_array_add(cmd_array,
			      "report-history",
			      NULL,
			      /* TRANSLATORS: command description */
			      _("Share firmware history with the developers"),
			      fu_util_report_history);
	fu_util_cmd_array_add(cmd_array,
			      "report-export",
			      NULL,
			      /* TRANSLATORS: command description */
			      _("Export firmware history for manual upload"),
			      fu_util_report_export);
	fu_util_cmd_array_add(cmd_array,
			      "install",
			      /* TRANSLATORS: command argument: uppercase, spaces->dashes */
			      _("[DEVICE-ID|GUID] [VERSION]"),
			      /* TRANSLATORS: command description */
			      _("Install a specific firmware file on all devices that match"),
			      fu_util_install);
	fu_util_cmd_array_add(cmd_array,
			      "local-install",
			      /* TRANSLATORS: command argument: uppercase, spaces->dashes */
			      _("FILE [DEVICE-ID|GUID]"),
			      /* TRANSLATORS: command description */
			      _("Install a firmware file in cabinet format on this hardware"),
			      fu_util_local_install);
	fu_util_cmd_array_add(cmd_array,
			      "get-details",
			      /* TRANSLATORS: command argument: uppercase, spaces->dashes */
			      _("FILE"),
			      /* TRANSLATORS: command description */
			      _("Gets details about a firmware file"),
			      fu_util_get_details);
	fu_util_cmd_array_add(cmd_array,
			      "get-updates,get-upgrades",
			      /* TRANSLATORS: command argument: uppercase, spaces->dashes */
			      _("[DEVICE-ID|GUID]"),
			      /* TRANSLATORS: command description */
			      _("Gets the list of updates for all specified devices, or all "
				"devices if unspecified"),
			      fu_util_get_updates);
	fu_util_cmd_array_add(cmd_array,
			      "update,upgrade",
			      /* TRANSLATORS: command argument: uppercase, spaces->dashes */
			      _("[DEVICE-ID|GUID]"),
			      /* TRANSLATORS: command description */
			      _("Updates all specified devices to latest firmware version, or all "
				"devices if unspecified"),
			      fu_util_update);
	fu_util_cmd_array_add(cmd_array,
			      "verify",
			      /* TRANSLATORS: command argument: uppercase, spaces->dashes */
			      _("[DEVICE-ID|GUID]"),
			      /* TRANSLATORS: command description */
			      _("Checks cryptographic hash matches firmware"),
			      fu_util_verify);
	fu_util_cmd_array_add(cmd_array,
			      "unlock",
			      /* TRANSLATORS: command argument: uppercase, spaces->dashes */
			      _("DEVICE-ID|GUID"),
			      /* TRANSLATORS: command description */
			      _("Unlocks the device for firmware access"),
			      fu_util_unlock);
	fu_util_cmd_array_add(cmd_array,
			      "clear-results",
			      /* TRANSLATORS: command argument: uppercase, spaces->dashes */
			      _("DEVICE-ID|GUID"),
			      /* TRANSLATORS: command description */
			      _("Clears the results from the last update"),
			      fu_util_clear_results);
	fu_util_cmd_array_add(cmd_array,
			      "get-results",
			      /* TRANSLATORS: command argument: uppercase, spaces->dashes */
			      _("DEVICE-ID|GUID"),
			      /* TRANSLATORS: command description */
			      _("Gets the results from the last update"),
			      fu_util_get_results);
	fu_util_cmd_array_add(cmd_array,
			      "get-releases",
			      /* TRANSLATORS: command argument: uppercase, spaces->dashes */
			      _("[DEVICE-ID|GUID]"),
			      /* TRANSLATORS: command description */
			      _("Gets the releases for a device"),
			      fu_util_get_releases);
	fu_util_cmd_array_add(cmd_array,
			      "search",
			      /* TRANSLATORS: command argument: uppercase, spaces->dashes */
			      _("WORD"),
			      /* TRANSLATORS: command description */
			      _("Finds firmware releases from the metadata"),
			      fu_util_search);
	fu_util_cmd_array_add(cmd_array,
			      "get-remotes",
			      NULL,
			      /* TRANSLATORS: command description */
			      _("Gets the configured remotes"),
			      fu_util_get_remotes);
	fu_util_cmd_array_add(cmd_array,
			      "downgrade",
			      /* TRANSLATORS: command argument: uppercase, spaces->dashes */
			      _("[DEVICE-ID|GUID]"),
			      /* TRANSLATORS: command description */
			      _("Downgrades the firmware on a device"),
			      fu_util_downgrade);
	fu_util_cmd_array_add(cmd_array,
			      "refresh",
			      /* TRANSLATORS: command argument: uppercase, spaces->dashes */
			      _("[FILE FILE_SIG REMOTE-ID]"),
			      /* TRANSLATORS: command description */
			      _("Refresh metadata from remote server"),
			      fu_util_refresh);
	fu_util_cmd_array_add(cmd_array,
			      "verify-update",
			      /* TRANSLATORS: command argument: uppercase, spaces->dashes */
			      _("[DEVICE-ID|GUID]"),
			      /* TRANSLATORS: command description */
			      _("Update the stored cryptographic hash with current ROM contents"),
			      fu_util_verify_update);
	fu_util_cmd_array_add(cmd_array,
			      "modify-remote",
			      /* TRANSLATORS: command argument: uppercase, spaces->dashes */
			      _("REMOTE-ID KEY VALUE"),
			      /* TRANSLATORS: command description */
			      _("Modifies a given remote"),
			      fu_util_remote_modify);
	fu_util_cmd_array_add(cmd_array,
			      "enable-remote",
			      /* TRANSLATORS: command argument: uppercase, spaces->dashes */
			      _("REMOTE-ID"),
			      /* TRANSLATORS: command description */
			      _("Enables a given remote"),
			      fu_util_remote_enable);
	fu_util_cmd_array_add(cmd_array,
			      "clean-remote",
			      /* TRANSLATORS: command argument: uppercase, spaces->dashes */
			      _("REMOTE-ID"),
			      /* TRANSLATORS: command description */
			      _("Cleans a given remote"),
			      fu_util_remote_clean);
	fu_util_cmd_array_add(cmd_array,
			      "disable-remote",
			      /* TRANSLATORS: command argument: uppercase, spaces->dashes */
			      _("REMOTE-ID"),
			      /* TRANSLATORS: command description */
			      _("Disables a given remote"),
			      fu_util_remote_disable);
	fu_util_cmd_array_add(cmd_array,
			      "enable-remote-auth",
			      /* TRANSLATORS: command argument: uppercase, spaces->dashes */
			      _("[REMOTE-ID] [USERNAME] [TOKEN]"),
			      /* TRANSLATORS: command description */
			      _("Enable an authenticated remote"),
			      fu_util_enable_remote_auth);
	fu_util_cmd_array_add(cmd_array,
			      "activate",
			      /* TRANSLATORS: command argument: uppercase, spaces->dashes */
			      _("[DEVICE-ID|GUID]"),
			      /* TRANSLATORS: command description */
			      _("Activate devices"),
			      fu_util_activate);
	fu_util_cmd_array_add(cmd_array,
			      "get-approved-firmware",
			      NULL,
			      /* TRANSLATORS: firmware approved by the admin */
			      _("Gets the list of approved firmware"),
			      fu_util_get_approved_firmware);
	fu_util_cmd_array_add(cmd_array,
			      "set-approved-firmware",
			      /* TRANSLATORS: command argument: uppercase, spaces->dashes */
			      _("FILENAME|CHECKSUM1[,CHECKSUM2][,CHECKSUM3]"),
			      /* TRANSLATORS: firmware approved by the admin */
			      _("Sets the list of approved firmware"),
			      fu_util_set_approved_firmware);
	fu_util_cmd_array_add(cmd_array,
			      "modify-config",
			      /* TRANSLATORS: command argument: uppercase, spaces->dashes */
			      _("[SECTION] KEY VALUE"),
			      /* TRANSLATORS: sets something in the daemon configuration file */
			      _("Modifies a daemon configuration value"),
			      fu_util_modify_config);
	fu_util_cmd_array_add(cmd_array,
			      "reset-config",
			      /* TRANSLATORS: command argument: uppercase, spaces->dashes */
			      _("SECTION"),
			      /* TRANSLATORS: sets something in the daemon configuration file */
			      _("Resets a daemon configuration section"),
			      fu_util_reset_config);
	fu_util_cmd_array_add(cmd_array,
			      "reinstall",
			      /* TRANSLATORS: command argument: uppercase, spaces->dashes */
			      _("[DEVICE-ID|GUID]"),
			      /* TRANSLATORS: command description */
			      _("Reinstall current firmware on the device"),
			      fu_util_reinstall);
	fu_util_cmd_array_add(cmd_array,
			      "switch-branch",
			      /* TRANSLATORS: command argument: uppercase, spaces->dashes */
			      _("[DEVICE-ID|GUID] [BRANCH]"),
			      /* TRANSLATORS: command description */
			      _("Switch the firmware branch on the device"),
			      fu_util_switch_branch);
	fu_util_cmd_array_add(cmd_array,
			      "security",
			      NULL,
			      /* TRANSLATORS: command description */
			      _("Gets the host security attributes"),
			      fu_util_security);
	fu_util_cmd_array_add(cmd_array,
			      "sync,sync-bkc",
			      NULL,
			      /* TRANSLATORS: command description */
			      _("Sync firmware versions to the chosen configuration"),
			      fu_util_sync);
	fu_util_cmd_array_add(cmd_array,
			      "get-plugins",
			      NULL,
			      /* TRANSLATORS: command description */
			      _("Get all enabled plugins registered with the system"),
			      fu_util_get_plugins);
	fu_util_cmd_array_add(cmd_array,
			      "download",
			      /* TRANSLATORS: command argument: uppercase, spaces->dashes */
			      _("LOCATION"),
			      /* TRANSLATORS: command description */
			      _("Download a file"),
			      fu_util_download);
	fu_util_cmd_array_add(cmd_array,
			      "device-test",
			      /* TRANSLATORS: command argument: uppercase, spaces->dashes */
			      _("[FILENAME1] [FILENAME2]"),
			      /* TRANSLATORS: command description */
			      _("Test a device using a JSON manifest"),
			      fu_util_device_test);
	fu_util_cmd_array_add(cmd_array,
			      "device-emulate",
			      /* TRANSLATORS: command argument: uppercase, spaces->dashes */
			      _("[FILENAME1] [FILENAME2]"),
			      /* TRANSLATORS: command description */
			      _("Emulate a device using a JSON manifest"),
			      fu_util_device_emulate);
	fu_util_cmd_array_add(cmd_array,
			      "inhibit",
			      /* TRANSLATORS: command argument: uppercase, spaces->dashes */
			      _("[REASON] [TIMEOUT]"),
			      /* TRANSLATORS: command description */
			      _("Inhibit the system to prevent upgrades"),
			      fu_util_inhibit);
	fu_util_cmd_array_add(cmd_array,
			      "uninhibit",
			      /* TRANSLATORS: command argument: uppercase, spaces->dashes */
			      _("INHIBIT-ID"),
			      /* TRANSLATORS: command description */
			      _("Uninhibit the system to allow upgrades"),
			      fu_util_uninhibit);
	fu_util_cmd_array_add(cmd_array,
			      "device-wait",
			      /* TRANSLATORS: command argument: uppercase, spaces->dashes */
			      _("GUID|DEVICE-ID"),
			      /* TRANSLATORS: command description */
			      _("Wait for a device to appear"),
			      fu_util_device_wait);
	fu_util_cmd_array_add(cmd_array,
			      "quit",
			      NULL,
			      /* TRANSLATORS: command description */
			      _("Asks the daemon to quit"),
			      fu_util_quit);
	fu_util_cmd_array_add(
	    cmd_array,
	    "get-bios-settings,get-bios-setting",
	    /* TRANSLATORS: command argument: uppercase, spaces->dashes */
	    _("[SETTING1] [SETTING2] [--no-authenticate]"),
	    /* TRANSLATORS: command description */
	    _("Retrieve BIOS settings.  If no arguments are passed all settings are returned"),
	    fu_util_get_bios_setting);
	fu_util_cmd_array_add(cmd_array,
			      "set-bios-setting",
			      /* TRANSLATORS: command argument: uppercase, spaces->dashes */
			      _("SETTING1 VALUE1 [SETTING2] [VALUE2]"),
			      /* TRANSLATORS: command description */
			      _("Sets one or more BIOS settings"),
			      fu_util_set_bios_setting);
	fu_util_cmd_array_add(cmd_array,
			      "emulation-load",
			      /* TRANSLATORS: command argument: uppercase, spaces->dashes */
			      _("FILENAME"),
			      /* TRANSLATORS: command description */
			      _("Load device emulation data"),
			      fu_util_emulation_load);
	fu_util_cmd_array_add(cmd_array,
			      "emulation-save",
			      /* TRANSLATORS: command argument: uppercase, spaces->dashes */
			      _("FILENAME"),
			      /* TRANSLATORS: command description */
			      _("Save device emulation data"),
			      fu_util_emulation_save);
	fu_util_cmd_array_add(cmd_array,
			      "emulation-tag",
			      /* TRANSLATORS: command argument: uppercase, spaces->dashes */
			      _("[DEVICE-ID|GUID]"),
			      /* TRANSLATORS: command description */
			      _("Adds devices to watch for future emulation"),
			      fu_util_emulation_tag);
	fu_util_cmd_array_add(cmd_array,
			      "emulation-untag",
			      /* TRANSLATORS: command argument: uppercase, spaces->dashes */
			      _("[DEVICE-ID|GUID]"),
			      /* TRANSLATORS: command description */
			      _("Removes devices to watch for future emulation"),
			      fu_util_emulation_untag);
	fu_util_cmd_array_add(cmd_array,
			      "security-fix",
			      /* TRANSLATORS: command argument: uppercase, spaces->dashes */
			      _("[APPSTREAM_ID]"),
			      /* TRANSLATORS: command description */
			      _("Fix a specific host security attribute"),
			      fu_util_security_fix);
	fu_util_cmd_array_add(cmd_array,
			      "security-undo",
			      /* TRANSLATORS: command argument: uppercase, spaces->dashes */
			      _("[APPSTREAM_ID]"),
			      /* TRANSLATORS: command description */
			      _("Undo the host security attribute fix"),
			      fu_util_security_undo);
	fu_util_cmd_array_add(cmd_array,
			      "report-devices",
			      NULL,
			      /* TRANSLATORS: command description */
			      _("Upload the list of updatable devices to a remote server"),
			      fu_util_report_devices);
	fu_util_cmd_array_add(cmd_array,
			      "hwids",
			      NULL,
			      /* TRANSLATORS: command description */
			      _("Return all the hardware IDs for the machine"),
			      fu_util_hwids);

	/* do stuff on ctrl+c */
	self->cancellable = g_cancellable_new();
	g_signal_connect(G_CANCELLABLE(self->cancellable),
			 "cancelled",
			 G_CALLBACK(fu_util_cancelled_cb),
			 self);

	/* sort by command name */
	fu_util_cmd_array_sort(cmd_array);

	/* non-TTY consoles cannot answer questions */
	if (!fu_util_setup_interactive(self, &error_console)) {
		g_info("failed to initialize interactive console: %s", error_console->message);
		self->no_unreported_check = TRUE;
		self->no_metadata_check = TRUE;
		self->no_reboot_check = TRUE;
		self->no_safety_check = TRUE;
		self->no_remote_check = TRUE;
		self->no_device_prompt = TRUE;
		self->no_emulation_check = TRUE;
		self->no_security_fix = TRUE;
	} else {
		is_interactive = TRUE;
	}
	fu_console_set_interactive(self->console, is_interactive);

	/* get a list of the commands */
	self->context = g_option_context_new(NULL);
	cmd_descriptions = fu_util_cmd_array_to_string(cmd_array);
	g_option_context_set_summary(self->context, cmd_descriptions);
	g_option_context_set_description(
	    self->context,
	    /* TRANSLATORS: CLI description */
	    _("This tool allows an administrator to query and control the "
	      "fwupd daemon, allowing them to perform actions such as "
	      "installing or downgrading firmware."));

	/* TRANSLATORS: program name */
	g_set_application_name(_("Firmware Utility"));
	g_option_context_add_main_entries(self->context, options, NULL);
	ret = g_option_context_parse(self->context, &argc, &argv, &error);
	if (!ret) {
		fu_console_print(self->console,
				 "%s: %s",
				 /* TRANSLATORS: the user didn't read the man page */
				 _("Failed to parse arguments"),
				 error->message);
		return EXIT_FAILURE;
	}

	/* allow disabling SSL strict mode for broken corporate proxies */
	if (self->disable_ssl_strict) {
		fu_console_print_full(self->console,
				      FU_CONSOLE_PRINT_FLAG_WARNING,
				      "%s\n",
				      /* TRANSLATORS: try to help */
				      _("Ignoring SSL strict checks, "
					"to do this automatically in the future "
					"export DISABLE_SSL_STRICT in your environment"));
		(void)g_setenv("DISABLE_SSL_STRICT", "1", TRUE);
	}

	/* this doesn't have to be precise (e.g. using the build-year) as we just
	 * want to check the clock is not set to the default of 1970-01-01... */
	if (g_date_time_get_year(dt_now) < 2021) {
		fu_console_print_full(
		    self->console,
		    FU_CONSOLE_PRINT_FLAG_WARNING,
		    "%s\n",
		    /* TRANSLATORS: try to help */
		    _("The system clock has not been set correctly and downloading "
		      "files may fail."));
	}

	/* parse filter flags */
	if (filter_device != NULL) {
		if (!fu_util_parse_filter_device_flags(filter_device,
						       &self->filter_device_include,
						       &self->filter_device_exclude,
						       &error)) {
			g_autofree gchar *str =
			    /* TRANSLATORS: the user didn't read the man page, %1 is '--filter' */
			    g_strdup_printf(_("Failed to parse flags for %s"), "--filter");
			g_prefix_error(&error, "%s: ", str);
			fu_util_print_error(self, error);
			return EXIT_FAILURE;
		}
	}
	if (filter_release != NULL) {
		if (!fu_util_parse_filter_release_flags(filter_release,
							&self->filter_release_include,
							&self->filter_release_exclude,
							&error)) {
			g_autofree gchar *str =
			    /* TRANSLATORS: the user didn't read the man page,
			     * %1 is '--filter-release' */
			    g_strdup_printf(_("Failed to parse flags for %s"), "--filter-release");
			g_prefix_error(&error, "%s: ", str);
			fu_util_print_error(self, error);
			return EXIT_FAILURE;
		}
	}

	/* set verbose? */
	if (verbose) {
#if GLIB_CHECK_VERSION(2, 72, 0)
		g_log_set_debug_enabled(TRUE);
#else
		(void)g_setenv("FWUPD_VERBOSE", "1", TRUE);
#endif
	} else {
		g_log_set_handler(G_LOG_DOMAIN, G_LOG_LEVEL_DEBUG, fu_util_ignore_cb, NULL);
	}

	/* set up ctrl+c */
	fu_util_setup_signal_handlers(self);

	/* set flags */
	if (allow_reinstall)
		self->flags |= FWUPD_INSTALL_FLAG_ALLOW_REINSTALL;
	if (allow_older)
		self->flags |= FWUPD_INSTALL_FLAG_ALLOW_OLDER;
	if (allow_branch_switch)
		self->flags |= FWUPD_INSTALL_FLAG_ALLOW_BRANCH_SWITCH;
	if (only_emulated)
		self->flags |= FWUPD_INSTALL_FLAG_ONLY_EMULATED;
	if (force) {
		self->flags |= FWUPD_INSTALL_FLAG_FORCE;
		self->flags |= FWUPD_INSTALL_FLAG_IGNORE_REQUIREMENTS;
	}
	if (no_history)
		self->flags |= FWUPD_INSTALL_FLAG_NO_HISTORY;

	/* use peer-to-peer for metadata and firmware *only* if specified */
	if (only_p2p)
		self->download_flags |= FWUPD_CLIENT_DOWNLOAD_FLAG_ONLY_P2P;

#ifdef HAVE_POLKIT
	/* start polkit tty agent to listen for password requests */
	if (is_interactive) {
		g_autoptr(GError) error_polkit = NULL;
		g_autoptr(FuPathStore) pstore = fu_path_store_new();
		fu_path_store_load_from_env(pstore);
		if (!fu_polkit_agent_open(polkit_agent, pstore, &error_polkit)) {
			fu_console_print(self->console,
					 "Failed to open polkit agent: %s",
					 error_polkit->message);
		}
	}
#endif

	/* connect to the daemon */
	self->client = fwupd_client_new();
	fwupd_client_set_main_context(self->client, self->main_ctx);
	fwupd_client_download_set_retries(self->client, download_retries);
	g_signal_connect(FWUPD_CLIENT(self->client),
			 "notify::percentage",
			 G_CALLBACK(fu_util_client_notify_cb),
			 self);
	g_signal_connect(FWUPD_CLIENT(self->client),
			 "notify::status",
			 G_CALLBACK(fu_util_client_notify_cb),
			 self);
	g_signal_connect(FWUPD_CLIENT(self->client),
			 "device-changed",
			 G_CALLBACK(fu_util_update_device_changed_cb),
			 self);
	g_signal_connect(FWUPD_CLIENT(self->client),
			 "device-request",
			 G_CALLBACK(fu_util_update_device_request_cb),
			 self);

	/* show a warning if the daemon is tainted */
	if (!fwupd_client_connect(self->client, self->cancellable, &error)) {
#ifdef _WIN32
		fu_console_print_literal(
		    self->console,
		    /* TRANSLATORS: error message for Windows */
		    _("Failed to connect to Windows service, please ensure it's running."));
		g_debug("%s", error->message);
#else
		/* TRANSLATORS: could not contact the fwupd service over D-Bus */
		g_prefix_error(&error, "%s: ", _("Failed to connect to daemon"));
		fu_util_print_error(self, error);
#endif
		return EXIT_FAILURE;
	}
	if (fwupd_client_get_tainted(self->client)) {
		fu_console_print_full(self->console,
				      FU_CONSOLE_PRINT_FLAG_WARNING,
				      "%s\n",
				      /* TRANSLATORS: the user is SOL for support... */
				      _("The daemon has loaded 3rd party code and "
					"is no longer supported by the upstream developers!"));
	}

	/* just show versions and exit */
	if (version) {
		if (!fu_util_version(self, &error)) {
			fu_util_print_error(self, error);
			return EXIT_FAILURE;
		}
		return EXIT_SUCCESS;
	}

	if (!self->as_json) {
		/* show user-visible warnings from the plugins */
		fu_util_show_plugin_warnings(self);

		/* show any unsupported warnings */
		fu_util_show_unsupported_warning(self->console);
	}

	/* we know the runtime daemon version now */
	fwupd_client_set_user_agent_for_package(self->client, g_get_prgname(), PACKAGE_VERSION);

	/* check that we have at least this version daemon running */
	if ((self->flags & FWUPD_INSTALL_FLAG_FORCE) == 0 &&
	    !fu_util_check_daemon_version(self, &error)) {
		fu_util_print_error(self, error);
		return EXIT_FAILURE;
	}

	/* make sure polkit actions were installed */
	if (!fu_util_check_polkit_actions(&error)) {
		fu_util_print_error(self, error);
		return EXIT_FAILURE;
	}

	/* send our implemented feature set */
	if (is_interactive) {
		feature_flags |=
		    FWUPD_FEATURE_FLAG_REQUESTS | FWUPD_FEATURE_FLAG_REQUESTS_NON_GENERIC |
		    FWUPD_FEATURE_FLAG_UPDATE_ACTION | FWUPD_FEATURE_FLAG_DETACH_ACTION;
		if (!no_authenticate)
			feature_flags |= FWUPD_FEATURE_FLAG_ALLOW_AUTHENTICATION;
	}
	if (!fwupd_client_set_feature_flags(self->client,
					    feature_flags,
					    self->cancellable,
					    &error)) {
		/* TRANSLATORS: a feature is something like "can show an image" */
		g_prefix_error(&error, "%s: ", _("Failed to set front-end features"));
		fu_util_print_error(self, error);
		return EXIT_FAILURE;
	}

	/* run the specified command */
	ret = fu_util_cmd_array_run(cmd_array, self, argv[1], (gchar **)&argv[2], &error);
	if (!ret) {
#ifdef SUPPORTED_BUILD
		/* sanity check */
		if (error == NULL) {
			g_critical("exec failed but no error set!");
			return EXIT_FAILURE;
		}
#endif
		fu_util_print_error(self, error);
		if (!self->as_json &&
		    g_error_matches(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS)) {
			g_autofree gchar *cmd = g_strdup_printf("%s --help", g_get_prgname());
			g_autoptr(GString) str = g_string_new("\n");
			/* TRANSLATORS: explain how to get help,
			 * where $1 is something like 'fwupdmgr --help' */
			g_string_append_printf(str, _("Use %s for help"), cmd);
			fu_console_print_literal(self->console, str->str);
		} else if (g_error_matches(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO)) {
			return EXIT_NOTHING_TO_DO;
		} else if (g_error_matches(error, FWUPD_ERROR, FWUPD_ERROR_NOT_REACHABLE)) {
			return EXIT_NOT_REACHABLE;
		} else if (g_error_matches(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND)) {
			return EXIT_NOT_FOUND;
		}
		return EXIT_FAILURE;
	}

	/* success */
	return EXIT_SUCCESS;
}
