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

#include <config.h>

#include "gs-flatpak-app.h"
#include "gs-flatpak-transaction.h"

struct _GsFlatpakTransaction {
	FlatpakTransaction	 parent_instance;
	GHashTable		*refhash;	/* ref:GsApp */
	GError			*first_operation_error;
	gboolean		 stop_on_first_error;
	FlatpakTransactionOperation *error_operation;  /* (nullable) (owned) */
};

enum {
	SIGNAL_REF_TO_APP,
	LAST_SIGNAL
};

static guint signals[LAST_SIGNAL] = { 0 };

G_DEFINE_TYPE (GsFlatpakTransaction, gs_flatpak_transaction, FLATPAK_TYPE_TRANSACTION)

typedef enum {
	PROP_STOP_ON_FIRST_ERROR = 1,
} GsFlatpakTransactionProperty;

static GParamSpec *props[PROP_STOP_ON_FIRST_ERROR + 1] = { NULL, };

static void
gs_flatpak_transaction_get_property (GObject    *object,
                                     guint       prop_id,
                                     GValue     *value,
                                     GParamSpec *pspec)
{
	GsFlatpakTransaction *self = GS_FLATPAK_TRANSACTION (object);

	switch ((GsFlatpakTransactionProperty) prop_id) {
	case PROP_STOP_ON_FIRST_ERROR:
		g_value_set_boolean (value, self->stop_on_first_error);
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
		break;
	}
}

static void
gs_flatpak_transaction_set_property (GObject      *object,
                                     guint         prop_id,
                                     const GValue *value,
                                     GParamSpec   *pspec)
{
	GsFlatpakTransaction *self = GS_FLATPAK_TRANSACTION (object);

	switch ((GsFlatpakTransactionProperty) prop_id) {
	case PROP_STOP_ON_FIRST_ERROR:
		/* Construct only. */
		self->stop_on_first_error = g_value_get_boolean (value);
		g_object_notify_by_pspec (object, props[prop_id]);
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
		break;
	}
}

static void
gs_flatpak_transaction_dispose (GObject *object)
{
	GsFlatpakTransaction *self = GS_FLATPAK_TRANSACTION (object);

	g_clear_object (&self->error_operation);

	G_OBJECT_CLASS (gs_flatpak_transaction_parent_class)->dispose (object);
}

static void
gs_flatpak_transaction_finalize (GObject *object)
{
	GsFlatpakTransaction *self;
	g_return_if_fail (GS_IS_FLATPAK_TRANSACTION (object));
	self = GS_FLATPAK_TRANSACTION (object);

	g_assert (self != NULL);
	g_hash_table_unref (self->refhash);
	if (self->first_operation_error != NULL)
		g_error_free (self->first_operation_error);

	G_OBJECT_CLASS (gs_flatpak_transaction_parent_class)->finalize (object);
}

GsApp *
gs_flatpak_transaction_get_app_by_ref (FlatpakTransaction *transaction, const gchar *ref)
{
	GsFlatpakTransaction *self = GS_FLATPAK_TRANSACTION (transaction);
	return g_hash_table_lookup (self->refhash, ref);
}

static void
gs_flatpak_transaction_add_app_internal (GsFlatpakTransaction *self, GsApp *app)
{
	g_autofree gchar *ref = gs_flatpak_app_get_ref_display (app);
	g_hash_table_insert (self->refhash, g_steal_pointer (&ref), g_object_ref (app));
}

void
gs_flatpak_transaction_add_app (FlatpakTransaction *transaction, GsApp *app)
{
	GsFlatpakTransaction *self = GS_FLATPAK_TRANSACTION (transaction);
	gs_flatpak_transaction_add_app_internal (self, app);
	if (gs_app_get_runtime (app) != NULL)
		gs_flatpak_transaction_add_app_internal (self, gs_app_get_runtime (app));
}

static GsApp *
_ref_to_app (GsFlatpakTransaction *self, const gchar *ref)
{
	GsApp *app = g_hash_table_lookup (self->refhash, ref);
	if (app != NULL)
		return g_object_ref (app);
	g_signal_emit (self, signals[SIGNAL_REF_TO_APP], 0, ref, &app);

	/* Cache the result */
	if (app != NULL)
		g_hash_table_insert (self->refhash, g_strdup (ref), g_object_ref (app));

	return app;
}

static void
_transaction_operation_set_app (FlatpakTransactionOperation *op, GsApp *app)
{
	g_object_set_data_full (G_OBJECT (op), "GsApp",
				g_object_ref (app), (GDestroyNotify) g_object_unref);
}

static GsApp *
_transaction_operation_get_app (FlatpakTransactionOperation *op)
{
	return g_object_get_data (G_OBJECT (op), "GsApp");
}

gboolean
gs_flatpak_transaction_run (FlatpakTransaction *transaction,
                            GCancellable *cancellable,
                            GError **error)

{
	GsFlatpakTransaction *self = GS_FLATPAK_TRANSACTION (transaction);
	g_autoptr(GError) error_local = NULL;

	if (!flatpak_transaction_run (transaction, cancellable, &error_local)) {
		/* whole transaction failed; restore the state for all the apps involved */
		g_autolist(GObject) ops = flatpak_transaction_get_operations (transaction);
		for (GList *l = ops; l != NULL; l = l->next) {
			FlatpakTransactionOperation *op = l->data;
			const gchar *ref = flatpak_transaction_operation_get_ref (op);
			g_autoptr(GsApp) app = _ref_to_app (self, ref);
			if (app == NULL) {
				g_warning ("failed to find app for %s", ref);
				continue;
			}
			gs_app_set_state_recover (app);
		}

		if (self->first_operation_error != NULL) {
			g_propagate_error (error, g_steal_pointer (&self->first_operation_error));
			return FALSE;
		} else {
			g_propagate_error (error, g_steal_pointer (&error_local));
			return FALSE;
		}
	}

	return TRUE;
}

static gboolean
_transaction_ready (FlatpakTransaction *transaction)
{
	GsFlatpakTransaction *self = GS_FLATPAK_TRANSACTION (transaction);
	g_autolist(GObject) ops = NULL;

	/* nothing to do */
	ops = flatpak_transaction_get_operations (transaction);
	if (ops == NULL)
		return TRUE; // FIXME: error?
	for (GList *l = ops; l != NULL; l = l->next) {
		FlatpakTransactionOperation *op = l->data;
		const gchar *ref = flatpak_transaction_operation_get_ref (op);
		g_autoptr(GsApp) app = _ref_to_app (self, ref);
		if (app != NULL) {
			_transaction_operation_set_app (op, app);
			/* if we're updating a component, then mark all the apps
			 * involved to ensure updating the button state */
			if (flatpak_transaction_operation_get_operation_type (op) == FLATPAK_TRANSACTION_OPERATION_UPDATE) {
				if (gs_app_get_state (app) == GS_APP_STATE_UNKNOWN ||
				    gs_app_get_state (app) == GS_APP_STATE_INSTALLED)
					gs_app_set_state (app, GS_APP_STATE_UPDATABLE_LIVE);

				gs_app_set_state (app, GS_APP_STATE_INSTALLING);
			}
		}

		/* Debug dump. */
		{
			GPtrArray *related_to_ops = flatpak_transaction_operation_get_related_to_ops (op);
			g_autoptr(GString) debug_message = g_string_new ("");

			g_string_append_printf (debug_message,
						"%s: op %p, app %s (%p), download size %" G_GUINT64_FORMAT ", related-to:",
						G_STRFUNC, op,
						app ? gs_app_get_unique_id (app) : "?",
						app,
						flatpak_transaction_operation_get_download_size (op));
			for (gsize i = 0; related_to_ops != NULL && i < related_to_ops->len; i++) {
				FlatpakTransactionOperation *related_to_op = g_ptr_array_index (related_to_ops, i);
				g_string_append_printf (debug_message,
							"\n ├ %s (%p)", flatpak_transaction_operation_get_ref (related_to_op), related_to_op);
			}
			g_string_append (debug_message, "\n └ (end)");
			g_assert (debug_message != NULL);  /* -Wnull-dereference false positive */
			g_debug ("%s", debug_message->str);
		}
	}
	return TRUE;
}

typedef struct
{
	GsFlatpakTransaction *transaction;  /* (owned) */
	FlatpakTransactionOperation *operation;  /* (owned) */
	GsApp *app;  /* (owned) */
} ProgressData;

static void
progress_data_free (ProgressData *data)
{
	g_clear_object (&data->operation);
	g_clear_object (&data->app);
	g_clear_object (&data->transaction);
	g_free (data);
}

G_DEFINE_AUTOPTR_CLEANUP_FUNC (ProgressData, progress_data_free)

static gboolean
op_is_related_to_op (FlatpakTransactionOperation *op,
		     FlatpakTransactionOperation *root_op,
		     GHashTable *checked_ops)
{
	GPtrArray *related_to_ops;  /* (element-type FlatpakTransactionOperation) */
	gpointer cached_is_related_to = NULL;

	if (op == root_op)
		return TRUE;

	/* FIXME: Detect infinite loops. These indicate a bug in libflatpak,
	 * but have been hard to track down so far.
	 * See: https://gitlab.gnome.org/GNOME/gnome-software/-/issues/2689 */
	if (g_hash_table_lookup_extended (checked_ops, op, NULL, &cached_is_related_to))
		return GPOINTER_TO_INT (cached_is_related_to);

	g_hash_table_insert (checked_ops, op, GINT_TO_POINTER (TRUE));

	related_to_ops = flatpak_transaction_operation_get_related_to_ops (op);
	for (gsize i = 0; related_to_ops != NULL && i < related_to_ops->len; i++) {
		FlatpakTransactionOperation *related_to_op = g_ptr_array_index (related_to_ops, i);
		if (related_to_op == root_op || op_is_related_to_op (related_to_op, root_op, checked_ops)) {
			g_hash_table_insert (checked_ops, op, GINT_TO_POINTER (TRUE));
			return TRUE;
		}
	}

	g_hash_table_insert (checked_ops, op, GINT_TO_POINTER (FALSE));

	return FALSE;
}

static guint64
saturated_uint64_add (guint64 a, guint64 b)
{
	return (a <= G_MAXUINT64 - b) ? a + b : G_MAXUINT64;
}

/*
 * update_progress_for_op:
 * @self: a #GsFlatpakTransaction
 * @current_progress: progress reporting object
 * @ops: results of calling flatpak_transaction_get_operations() on @self, for performance
 * @current_op: the #FlatpakTransactionOperation which the @current_progress is
 *    for; this is the operation currently being run by libflatpak
 * @root_op: the #FlatpakTransactionOperation at the root of the operation subtree
 *    to calculate progress for
 * @checked_ops: (element-type FlatpakTransactionOperation gboolean): hash table
 *    of already checked ops, to detect a dependency loop; an op being present in the table
 *    means it was already checked, and the stored value is the previous return value of
 *    op_is_related_to_op() for that op
 *
 * Calculate and update the #GsApp:progress for each app associated with
 * @root_op in a flatpak transaction. This will include the #GsApp for the app
 * being installed (for example), but also the #GsApps for all of its runtimes
 * and locales, and any other dependencies of them.
 *
 * Each #GsApp:progress is calculated based on the sum of the progress of all
 * the apps related to that one — so the progress for an app will factor in the
 * progress for all its runtimes.
 */
static void
update_progress_for_op (GsFlatpakTransaction        *self,
                        FlatpakTransactionProgress  *current_progress,
                        GList                       *ops,
                        FlatpakTransactionOperation *current_op,
                        FlatpakTransactionOperation *root_op,
                        GHashTable                  *checked_ops)
{
	g_autoptr(GsApp) root_app = NULL;
	guint64 related_prior_download_bytes = 0;
	guint64 related_download_bytes = 0;
	guint64 current_bytes_transferred = flatpak_transaction_progress_get_bytes_transferred (current_progress);
	gboolean seen_current_op = FALSE, seen_root_op = FALSE;
	gboolean root_op_skipped = flatpak_transaction_operation_get_is_skipped (root_op);
	guint percent;

	/* If @root_op is being skipped and its GsApp isn't being
	 * installed/removed, don't update the progress on it. It may be that
	 * @root_op is the runtime of an app and the app is the thing the
	 * transaction was created for.
	 */
	if (root_op_skipped) {
		/* _transaction_operation_set_app() is only called on non-skipped ops */
		const gchar *ref = flatpak_transaction_operation_get_ref (root_op);
		root_app = _ref_to_app (self, ref);
		if (root_app == NULL) {
			g_warning ("Couldn't find GsApp for transaction operation %s",
			           flatpak_transaction_operation_get_ref (root_op));
			return;
		}
		if (gs_app_get_state (root_app) != GS_APP_STATE_INSTALLING &&
		    gs_app_get_state (root_app) != GS_APP_STATE_REMOVING &&
		    gs_app_get_state (root_app) != GS_APP_STATE_DOWNLOADING)
			return;
	} else {
		GsApp *unskipped_root_app = _transaction_operation_get_app (root_op);
		if (unskipped_root_app == NULL) {
			g_warning ("Couldn't find GsApp for transaction operation %s",
			           flatpak_transaction_operation_get_ref (root_op));
			return;
		}
		root_app = g_object_ref (unskipped_root_app);
	}

	/* This relies on ops in a #FlatpakTransaction being run in the order
	 * they’re returned by flatpak_transaction_get_operations(), which is true. */
	for (GList *l = ops; l != NULL; l = l->next) {
		FlatpakTransactionOperation *op = FLATPAK_TRANSACTION_OPERATION (l->data);
		guint64 op_download_size = flatpak_transaction_operation_get_download_size (op);

		if (op == current_op)
			seen_current_op = TRUE;
		if (op == root_op)
			seen_root_op = TRUE;

		/* Currently libflatpak doesn't return skipped ops in
		 * flatpak_transaction_get_operations(), but check just in case.
		 */
		if (op == root_op && root_op_skipped)
			continue;

		if (op_is_related_to_op (op, root_op, checked_ops)) {
			/* Saturate instead of overflowing */
			related_download_bytes = saturated_uint64_add (related_download_bytes, op_download_size);
			if (!seen_current_op)
				related_prior_download_bytes = saturated_uint64_add (related_prior_download_bytes, op_download_size);
		}
	}

	g_assert (related_prior_download_bytes <= related_download_bytes);
	g_assert (seen_root_op || root_op_skipped);

	/* Avoid overflows when converting to percent, at the cost of losing
	 * some precision in the least significant digits. */
	if (related_prior_download_bytes > G_MAXUINT64 / 100 ||
	    current_bytes_transferred > G_MAXUINT64 / 100) {
		related_prior_download_bytes /= 100;
		    current_bytes_transferred /= 100;
		    related_download_bytes /= 100;
	}

	/* Update the progress of @root_app. */
	if (related_download_bytes > 0)
		percent = ((related_prior_download_bytes * 100 / related_download_bytes) +
		           (current_bytes_transferred * 100 / related_download_bytes));
	else
		percent = 0;

	if (gs_app_get_progress (root_app) == 100 ||
	    gs_app_get_progress (root_app) == GS_APP_PROGRESS_UNKNOWN ||
	    gs_app_get_progress (root_app) <= percent) {
		gs_app_set_progress (root_app, percent);
	} else {
		g_warning ("ignoring percentage %u%% -> %u%% as going down on app %s",
			   gs_app_get_progress (root_app), percent,
			   gs_app_get_unique_id (root_app));
	}
}

static void
update_progress_for_op_recurse_up (GsFlatpakTransaction        *self,
				   FlatpakTransactionProgress  *progress,
				   GList                       *ops,
				   FlatpakTransactionOperation *current_op,
				   FlatpakTransactionOperation *root_op,
				   GHashTable                  *checked_ops)
{
	GPtrArray *related_to_ops = flatpak_transaction_operation_get_related_to_ops (root_op);

	if (g_hash_table_contains (checked_ops, root_op))
		return;

	/* Update progress for @root_op */
	update_progress_for_op (self, progress, ops, current_op, root_op, checked_ops);

	/* Update progress for ops related to @root_op, e.g. apps whose runtime is @root_op */
	for (gsize i = 0; related_to_ops != NULL && i < related_to_ops->len; i++) {
		FlatpakTransactionOperation *related_to_op = g_ptr_array_index (related_to_ops, i);
		update_progress_for_op_recurse_up (self, progress, ops, current_op, related_to_op, checked_ops);
	}
}

static void
_transaction_progress_changed_cb (FlatpakTransactionProgress *progress,
				  gpointer user_data)
{
	ProgressData *data = user_data;
	GsApp *app = data->app;
	GsFlatpakTransaction *self = data->transaction;
	g_autoptr(GHashTable) checked_ops = NULL;
	g_autolist(FlatpakTransactionOperation) ops = NULL;

	if (flatpak_transaction_progress_get_is_estimating (progress)) {
		/* "Estimating" happens while fetching the metadata, which
		 * flatpak arbitrarily decides happens during the first 5% of
		 * each operation. At this point, no more detailed progress
		 * information is available. */
		gs_app_set_progress (app, GS_APP_PROGRESS_UNKNOWN);
		return;
	}

	/* Update the progress on this app, and then do the same for each
	 * related parent app up the hierarchy. For example, @data->operation
	 * could be for a runtime which was added to the transaction because of
	 * an app — so we need to update the progress on the app too.
	 *
	 * It’s important to note that a new @data->progress is created by
	 * libflatpak for each @data->operation, and there are multiple
	 * operations in a transaction. There is no #FlatpakTransactionProgress
	 * which represents the progress of the whole transaction.
	 *
	 * There may be arbitrary many levels of related-to ops. For example,
	 * one common situation would be to install an app which needs a new
	 * runtime, and that runtime needs a locale to be installed, which would
	 * give three levels of related-to relation:
	 *    locale → runtime → app → (null)
	 *
	 * In addition, libflatpak may decide to skip some operations (if they
	 * turn out to not be necessary). These skipped operations are not
	 * included in the list returned by flatpak_transaction_get_operations(),
	 * but they can be accessed via
	 * flatpak_transaction_operation_get_related_to_ops(), so have to be
	 * ignored manually.
	 */
	ops = flatpak_transaction_get_operations (FLATPAK_TRANSACTION (self));
	checked_ops = g_hash_table_new (NULL, NULL);
	update_progress_for_op_recurse_up (self, progress, ops, data->operation, data->operation, checked_ops);
}

static const gchar *
_flatpak_transaction_operation_type_to_string (FlatpakTransactionOperationType ot)
{
	if (ot == FLATPAK_TRANSACTION_OPERATION_INSTALL)
		return "install";
	if (ot == FLATPAK_TRANSACTION_OPERATION_UPDATE)
		return "update";
	if (ot == FLATPAK_TRANSACTION_OPERATION_INSTALL_BUNDLE)
		return "install-bundle";
	if (ot == FLATPAK_TRANSACTION_OPERATION_UNINSTALL)
		return "uninstall";
	return NULL;
}

static void
progress_data_free_closure (gpointer  user_data,
                            GClosure *closure)
{
	progress_data_free (user_data);
}

static void
_transaction_new_operation (FlatpakTransaction *transaction,
			    FlatpakTransactionOperation *operation,
			    FlatpakTransactionProgress *progress)
{
	GsApp *app;
	g_autoptr(ProgressData) progress_data = NULL;

	/* find app */
	app = _transaction_operation_get_app (operation);
	if (app == NULL) {
		FlatpakTransactionOperationType ot;
		ot = flatpak_transaction_operation_get_operation_type (operation);
		g_warning ("failed to find app for %s during %s",
			   flatpak_transaction_operation_get_ref (operation),
			   _flatpak_transaction_operation_type_to_string (ot));
		return;
	}

	/* report progress */
	progress_data = g_new0 (ProgressData, 1);
	progress_data->transaction = GS_FLATPAK_TRANSACTION (g_object_ref (transaction));
	progress_data->app = g_object_ref (app);
	progress_data->operation = g_object_ref (operation);

	g_signal_connect_data (progress, "changed",
			       G_CALLBACK (_transaction_progress_changed_cb),
			       g_steal_pointer (&progress_data),
			       progress_data_free_closure,
			       0  /* flags */);
	flatpak_transaction_progress_set_update_frequency (progress, 500); /* FIXME? */

	/* set app status */
	switch (flatpak_transaction_operation_get_operation_type (operation)) {
	case FLATPAK_TRANSACTION_OPERATION_INSTALL:
		if (gs_app_get_state (app) == GS_APP_STATE_UNKNOWN)
			gs_app_set_state (app, GS_APP_STATE_AVAILABLE);
		gs_app_set_state (app, GS_APP_STATE_INSTALLING);
		break;
	case FLATPAK_TRANSACTION_OPERATION_INSTALL_BUNDLE:
		if (gs_app_get_state (app) == GS_APP_STATE_UNKNOWN)
			gs_app_set_state (app, GS_APP_STATE_AVAILABLE_LOCAL);
		gs_app_set_state (app, GS_APP_STATE_INSTALLING);
		break;
	case FLATPAK_TRANSACTION_OPERATION_UPDATE:
		if (gs_app_get_state (app) == GS_APP_STATE_UNKNOWN ||
		    gs_app_get_state (app) == GS_APP_STATE_INSTALLED)
			gs_app_set_state (app, GS_APP_STATE_UPDATABLE_LIVE);
		gs_app_set_state (app, GS_APP_STATE_INSTALLING);
		break;
	case FLATPAK_TRANSACTION_OPERATION_UNINSTALL:
		gs_app_set_state (app, GS_APP_STATE_REMOVING);
		break;
	default:
		break;
	}
}

static gboolean
later_op_also_related (GList                       *ops,
		       FlatpakTransactionOperation *current_op,
		       FlatpakTransactionOperation *related_to_current_op)
{
	/* Here we're determining if anything in @ops which comes after
	 * @current_op is related to @related_to_current_op and not skipped
	 * (but all @ops are not skipped so no need to check explicitly)
	 */
	gboolean found_later_op = FALSE, seen_current_op = FALSE;
	for (GList *l = ops; l != NULL; l = l->next) {
		FlatpakTransactionOperation *op = l->data;
		GPtrArray *related_to_ops;
		if (current_op == op) {
			seen_current_op = TRUE;
			continue;
		}
		if (!seen_current_op)
			continue;

		related_to_ops = flatpak_transaction_operation_get_related_to_ops (op);
		for (gsize i = 0; related_to_ops != NULL && i < related_to_ops->len; i++) {
			FlatpakTransactionOperation *related_to_op = g_ptr_array_index (related_to_ops, i);
			if (related_to_op == related_to_current_op) {
				g_assert (flatpak_transaction_operation_get_is_skipped (related_to_op));
				found_later_op = TRUE;
			}
		}
	}

	return found_later_op;
}

static void
set_skipped_related_apps_to_installed (GsFlatpakTransaction        *self,
				       FlatpakTransaction          *transaction,
				       FlatpakTransactionOperation *operation)
{
	/* It's possible the thing being updated/installed, @operation, is a
	 * related ref (e.g. extension or runtime) of an app which itself doesn't
	 * need an update and therefore won't have _transaction_operation_done()
	 * called for it directly. So we have to set the main app to installed
	 * here.
	*/
	g_autolist(GObject) ops = flatpak_transaction_get_operations (transaction);
	GPtrArray *related_to_ops = flatpak_transaction_operation_get_related_to_ops (operation);

	for (gsize i = 0; related_to_ops != NULL && i < related_to_ops->len; i++) {
		FlatpakTransactionOperation *related_to_op = g_ptr_array_index (related_to_ops, i);
		if (flatpak_transaction_operation_get_is_skipped (related_to_op)) {
			const gchar *ref;
			g_autoptr(GsApp) related_to_app = NULL;

			/* Check that no later op is also related to related_to_op, in
			 * which case we want to let that operation finish before setting
			 * the main app to installed.
			 */
			if (later_op_also_related (ops, operation, related_to_op))
				continue;

			ref = flatpak_transaction_operation_get_ref (related_to_op);
			related_to_app = _ref_to_app (self, ref);
			if (related_to_app != NULL)
				gs_app_set_state (related_to_app, GS_APP_STATE_INSTALLED);
		}
	}
}

static void
_transaction_operation_done (FlatpakTransaction *transaction,
			     FlatpakTransactionOperation *operation,
			     const gchar *commit,
			     FlatpakTransactionResult details)
{
	GsFlatpakTransaction *self = GS_FLATPAK_TRANSACTION (transaction);

	/* invalidate */
	GsApp *app = _transaction_operation_get_app (operation);
	if (app == NULL) {
		g_warning ("failed to find app for %s",
			   flatpak_transaction_operation_get_ref (operation));
		return;
	}
	switch (flatpak_transaction_operation_get_operation_type (operation)) {
	case FLATPAK_TRANSACTION_OPERATION_INSTALL:
	case FLATPAK_TRANSACTION_OPERATION_INSTALL_BUNDLE:
		gs_app_set_state (app, GS_APP_STATE_INSTALLED);

		set_skipped_related_apps_to_installed (self, transaction, operation);
		break;
	case FLATPAK_TRANSACTION_OPERATION_UPDATE:
		gs_app_set_version (app, gs_app_get_update_version (app));
		gs_app_set_update_details_markup (app, NULL);
		gs_app_set_update_urgency (app, AS_URGENCY_KIND_UNKNOWN);
		gs_app_set_update_version (app, NULL);
		/* force getting the new runtime */
		gs_app_remove_kudo (app, GS_APP_KUDO_SANDBOXED);
                /* downloaded, but not yet installed */
		if (flatpak_transaction_get_no_deploy (transaction))
			gs_app_set_state (app, GS_APP_STATE_UPDATABLE_LIVE);
		else
			gs_app_set_state (app, GS_APP_STATE_INSTALLED);

		set_skipped_related_apps_to_installed (self, transaction, operation);
		break;
	case FLATPAK_TRANSACTION_OPERATION_UNINSTALL:
		/* we don't actually know if this app is re-installable */
		gs_flatpak_app_set_commit (app, NULL);
		gs_app_set_state (app, GS_APP_STATE_UNKNOWN);
		break;
	default:
		gs_app_set_state (app, GS_APP_STATE_UNKNOWN);
		break;
	}
}

static gboolean
_transaction_operation_error (FlatpakTransaction *transaction,
			      FlatpakTransactionOperation *operation,
			      const GError *error,
			      FlatpakTransactionErrorDetails detail)
{
	GsFlatpakTransaction *self = GS_FLATPAK_TRANSACTION (transaction);
	FlatpakTransactionOperationType operation_type = flatpak_transaction_operation_get_operation_type (operation);
	GsApp *app = _transaction_operation_get_app (operation);
	const gchar *ref = flatpak_transaction_operation_get_ref (operation);

	gs_app_set_state_recover (app);
	g_set_object (&self->error_operation, operation);

	if (g_error_matches (error, FLATPAK_ERROR, FLATPAK_ERROR_SKIPPED)) {
		g_debug ("skipped to %s %s: %s",
		         _flatpak_transaction_operation_type_to_string (operation_type),
		         ref,
		         error->message);
		return TRUE; /* continue */
	}

	/* If the transaction has been cancelled, bail out early rather
	 * than continuing to try operations which are all cancelled. */
	if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
		g_debug ("Transaction cancelled; stopping it");

		return FALSE;  /* stop */
	}

	if (detail & FLATPAK_TRANSACTION_ERROR_DETAILS_NON_FATAL) {
		g_warning ("failed to %s %s (non fatal): %s",
		           _flatpak_transaction_operation_type_to_string (operation_type),
		           ref,
		           error->message);
		return TRUE; /* continue */
	}

	if (self->first_operation_error == NULL) {
		g_propagate_error (&self->first_operation_error,
		                   g_error_copy (error));
		if (app != NULL)
			gs_utils_error_add_app_id (&self->first_operation_error, app);
	}

	if (!(detail & FLATPAK_TRANSACTION_ERROR_DETAILS_NON_FATAL) &&
	    self->stop_on_first_error)
		return FALSE;  /* stop */

	return TRUE; /* continue */
}

static int
_transaction_choose_remote_for_ref (FlatpakTransaction *transaction,
				    const char *for_ref,
				    const char *runtime_ref,
				    const char * const *remotes)
{
	//FIXME: do something smarter
	return 0;
}

static void
_transaction_end_of_lifed (FlatpakTransaction *transaction,
			   const gchar *ref,
			   const gchar *reason,
			   const gchar *rebase)
{
	if (rebase) {
		g_message ("%s is end-of-life, in favor of %s", ref, rebase);
	} else if (reason) {
		g_message ("%s is end-of-life, with reason: %s", ref, reason);
	}
	//FIXME: show something in the UI
}

static gboolean
_transaction_end_of_lifed_with_rebase (FlatpakTransaction  *transaction,
				       const gchar         *remote,
				       const gchar         *ref,
				       const gchar         *reason,
				       const gchar         *rebased_to_ref,
				       const gchar        **previous_ids)
{
	GsFlatpakTransaction *self = GS_FLATPAK_TRANSACTION (transaction);

	if (rebased_to_ref) {
		g_message ("%s is end-of-life, in favor of %s", ref, rebased_to_ref);
	} else if (reason) {
		g_message ("%s is end-of-life, with reason: %s", ref, reason);
	}

	if (rebased_to_ref && remote) {
		g_autoptr(GError) local_error = NULL;

#if FLATPAK_CHECK_VERSION(1, 15, 6)
		if (!flatpak_transaction_add_rebase_and_uninstall (transaction, remote, rebased_to_ref, ref,
								   NULL, previous_ids, &local_error)) {
#else
		if (!flatpak_transaction_add_rebase (transaction, remote, rebased_to_ref,
						     NULL, previous_ids, &local_error) ||
		    !flatpak_transaction_add_uninstall (transaction, ref, &local_error)) {
			/* NOT_INSTALLED error is expected in case the op that triggered this was install not update */
			if (g_error_matches (local_error, FLATPAK_ERROR, FLATPAK_ERROR_NOT_INSTALLED))
				g_clear_error (&local_error);
			else
#endif
			if (self->first_operation_error == NULL)
				g_propagate_prefixed_error (&self->first_operation_error,
							    g_steal_pointer (&local_error),
							    "Failed to rebase %s to %s: ",
							    ref, rebased_to_ref);

			return FALSE;
		}

		/* Note: A message about the rename will be shown in the UI
		 * thanks to code in gs_flatpak_refine_appstream() which
		 * sets gs_app_set_renamed_from().
		 */
		return TRUE;
	}

	return FALSE;
}

static gboolean
_transaction_add_new_remote (FlatpakTransaction *transaction,
			     FlatpakTransactionRemoteReason reason,
			     const char *from_id,
			     const char *remote_name,
			     const char *url)
{
	/* additional apps */
	if (reason == FLATPAK_TRANSACTION_REMOTE_GENERIC_REPO) {
		g_debug ("configuring %s as new generic remote", url);
		return TRUE; //FIXME?
	}

	/* runtime deps always make sense */
	if (reason == FLATPAK_TRANSACTION_REMOTE_RUNTIME_DEPS) {
		g_debug ("configuring %s as new remote for deps", url);
		return TRUE;
	}

	return FALSE;
}

static void
gs_flatpak_transaction_class_init (GsFlatpakTransactionClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS (klass);
	FlatpakTransactionClass *transaction_class = FLATPAK_TRANSACTION_CLASS (klass);

	object_class->get_property = gs_flatpak_transaction_get_property;
	object_class->set_property = gs_flatpak_transaction_set_property;
	object_class->dispose = gs_flatpak_transaction_dispose;
	object_class->finalize = gs_flatpak_transaction_finalize;

	transaction_class->ready = _transaction_ready;
	transaction_class->add_new_remote = _transaction_add_new_remote;
	transaction_class->new_operation = _transaction_new_operation;
	transaction_class->operation_done = _transaction_operation_done;
	transaction_class->operation_error = _transaction_operation_error;
	transaction_class->choose_remote_for_ref = _transaction_choose_remote_for_ref;
	transaction_class->end_of_lifed = _transaction_end_of_lifed;
	transaction_class->end_of_lifed_with_rebase = _transaction_end_of_lifed_with_rebase;

	/**
	 * GsFlatpakTransaction:stop-on-first-error:
	 *
	 * Stop the transaction on the first fatal error. If %FALSE, the
	 * transaction will continue running and ignore subsequent errors. Some
	 * operations may be automatically skipped if they are related to
	 * operations which have errored.
	 *
	 * Typically this should be %TRUE. It may be %FALSE for transactions
	 * where lots of apps are being updated, as typically updates should be
	 * mostly independent of each other, and we want as many of them to
	 * be attempted as possible.
	 *
	 * Since: 44
	 */
	props[PROP_STOP_ON_FIRST_ERROR] =
		g_param_spec_boolean ("stop-on-first-error",
				      "Stop on First Error",
				      "Stop the transaction on the first fatal error.",
				      TRUE,
				      G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
				      G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);

	g_object_class_install_properties (object_class, G_N_ELEMENTS (props), props);

	signals[SIGNAL_REF_TO_APP] =
		g_signal_new ("ref-to-app",
			      G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST,
			      0, NULL, NULL, NULL, G_TYPE_OBJECT, 1, G_TYPE_STRING);
}

static void
gs_flatpak_transaction_init (GsFlatpakTransaction *self)
{
	self->refhash = g_hash_table_new_full (g_str_hash, g_str_equal,
					       g_free, (GDestroyNotify) g_object_unref);
	self->stop_on_first_error = TRUE;
}

FlatpakTransaction *
gs_flatpak_transaction_new (FlatpakInstallation	*installation,
			    gboolean stop_on_first_error,
			    GCancellable *cancellable,
			    GError **error)
{
	GsFlatpakTransaction *self;
	self = g_initable_new (GS_TYPE_FLATPAK_TRANSACTION,
			       cancellable, error,
			       "installation", installation,
			       "stop-on-first-error", stop_on_first_error,
			       NULL);
	if (self == NULL)
		return NULL;
	return FLATPAK_TRANSACTION (self);
}

/**
 * gs_flatpak_transaction_get_error_operation:
 * @self: a #GsFlatpakTransaction
 * @out_app: (out) (transfer none) (optional) (nullable): return location for
 *     the #GsApp associated with the returned transaction operation, or %NULL
 *     to ignore; the returned value may be %NULL if no error operation is set,
 *     or if there’s no app associated with it
 *
 * Get the #FlatpakTransactionOperation which caused the most recent error in
 * the transaction.
 *
 * For transactions with #GsFlatpakTransaction:stop-on-first-error set, this
 * will be the operation that caused the fatal error.
 *
 * Returns: (nullable) (transfer none): the operation which caused the error, or
 *     %NULL if none
 * Since: 47
 */
FlatpakTransactionOperation *
gs_flatpak_transaction_get_error_operation (GsFlatpakTransaction  *self,
                                            GsApp                **out_app)
{
	g_return_val_if_fail (GS_IS_FLATPAK_TRANSACTION (self), NULL);

	if (out_app != NULL) {
		if (self->error_operation != NULL)
			*out_app = _transaction_operation_get_app (self->error_operation);
		else
			*out_app = NULL;
	}

	return self->error_operation;
}
