/* bz-application.c
 *
 * Copyright 2025 Adam Masciola
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 * SPDX-License-Identifier: GPL-3.0-or-later
 */

#define G_LOG_DOMAIN  "BAZAAR::CORE"
#define BAZAAR_MODULE "core"

#define MAX_IDS_PER_BLOCKLIST 2048

#define CACHE_ENUM_BATCH_SIZE 64

#define MIN_STARTUP_REFRESH_INTERVAL_SECONDS (3 * 60 * 60)

#include "config.h"

#include <glib/gi18n.h>
#include <malloc.h>

#include "bz-application-map-factory.h"
#include "bz-application.h"
#include "bz-appstream-parser.h"
#include "bz-auth-state.h"
#include "bz-backend-notification.h"
#include "bz-bundle-install-dialog.h"
#include "bz-content-provider.h"
#include "bz-donations-dialog.h"
#include "bz-download-worker.h"
#include "bz-entry-cache-manager.h"
#include "bz-entry-group.h"
#include "bz-favorites-page.h"
#include "bz-flathub-state.h"
#include "bz-flatpak-bundle-result.h"
#include "bz-flatpak-entry.h"
#include "bz-flatpak-instance.h"
#include "bz-hash-table-object.h"
#include "bz-inspector.h"
#include "bz-internal-config.h"
#include "bz-login-page.h"
#include "bz-malcontent-service.h"
#include "bz-metainfo-preview.h"
#include "bz-newline-parser.h"
#include "bz-parser.h"
#include "bz-preferences-dialog.h"
#include "bz-result.h"
#include "bz-root-blocklist.h"
#include "bz-root-curated-config.h"
#include "bz-serializable.h"
#include "bz-state-info.h"
#include "bz-transaction-manager.h"
#include "bz-window.h"
#include "bz-yaml-parser.h"
#include "env.h"
#include "error.h"
#include "io.h"
#include "progress-bar-designs/common.h"
#include "search-index-write.h"
#include "util.h"

struct _BzApplication
{
  AdwApplication parent_instance;

  BzApplicationMapFactory *application_factory;
  BzApplicationMapFactory *entry_factory;
  BzContentProvider       *blocklists_provider;
  BzContentProvider       *curated_provider;
  BzContentProvider       *txt_blocklists_provider;
  BzEntryCacheManager     *cache;
  BzFlathubState          *flathub;
  BzFlathubState          *tmp_flathub;
  BzFlatpakInstance       *flatpak;
  BzInternalConfig        *internal_config;
  BzMainConfig            *config;
  BzMalcontentService     *malcontent;
  BzNewlineParser         *txt_blocklist_parser;
  BzSearchEngine          *search_engine;
  BzStateInfo             *state;
  BzTransactionManager    *transactions;
  BzYamlParser            *blocklist_parser;
  BzYamlParser            *curated_parser;
  DexChannel              *flatpak_notifs;
  DexFuture               *notif_watch;
  DexFuture               *sync;
  DexPromise              *first_window_opened;
  DexPromise              *ready_to_open_files;
  GHashTable              *eol_runtimes;
  GHashTable              *ids_to_groups;
  GHashTable              *ignore_eol_set;
  GHashTable              *installed_set;
  GHashTable              *sys_name_to_addons;
  GHashTable              *sys_ref_to_addon_group_ids;
  GHashTable              *usr_name_to_addons;
  GHashTable              *usr_ref_to_addon_group_ids;
  GListStore              *groups;
  GListStore              *installed_apps;
  GListStore              *search_biases_backing;
  GNetworkMonitor         *network;
  GPtrArray               *blocklist_regexes;
  GPtrArray               *txt_blocked_id_sets;
  GSettings               *settings;
  GTimer                  *init_timer;
  GWeakRef                 main_window;
  GtkCustomFilter         *appid_filter;
  GtkCustomFilter         *group_filter;
  GtkFilterListModel      *group_filter_model;
  GtkFlattenListModel     *search_biases;
  GtkMapListModel         *blocklists_to_files;
  GtkMapListModel         *curated_configs_to_files;
  GtkMapListModel         *txt_blocklists_to_files;
  GtkStringList           *blocklists;
  GtkStringList           *curated_configs;
  GtkStringList           *txt_blocklists;
  gboolean                 flathub_remote_initialized;
  gboolean                 had_cache_on_init;
  gboolean                 running;
  guint                    periodic_timeout_source;
  int                      n_entries_incoming;
  int                      n_remotes_syncing;
};

G_DEFINE_FINAL_TYPE (BzApplication, bz_application, ADW_TYPE_APPLICATION)

BZ_DEFINE_DATA (
    blocklist_regex,
    BlocklistRegex,
    {
      int     priority;
      GRegex *block;
      GRegex *allow;
    },
    BZ_RELEASE_DATA (block, g_regex_unref);
    BZ_RELEASE_DATA (allow, g_regex_unref))

BZ_DEFINE_DATA (
    respond_to_flatpak,
    RespondToFlatpak,
    {
      GWeakRef              *self;
      BzBackendNotification *notif;
    },
    BZ_RELEASE_DATA (self, bz_weak_release);
    BZ_RELEASE_DATA (notif, g_object_unref))

BZ_DEFINE_DATA (
    cache_write_back,
    CacheWriteBack,
    {
      GWeakRef  *self;
      GPtrArray *notify_groups;
      gboolean   update_filters;
    },
    BZ_RELEASE_DATA (self, bz_weak_release);
    BZ_RELEASE_DATA (notify_groups, g_ptr_array_unref))

BZ_DEFINE_DATA (
    open_flatpakref,
    OpenFlatpakref,
    {
      GWeakRef *self;
      GFile    *file;
    },
    BZ_RELEASE_DATA (self, bz_weak_release);
    BZ_RELEASE_DATA (file, g_object_unref))

BZ_DEFINE_DATA (
    open_metainfo,
    OpenMetainfo,
    {
      GWeakRef *self;
      GFile    *file;
    },
    BZ_RELEASE_DATA (self, bz_weak_release);
    BZ_RELEASE_DATA (file, g_object_unref))

BZ_DEFINE_DATA (
    open_appstream,
    OpenAppstream,
    {
      GWeakRef *self;
      char     *id;
    },
    BZ_RELEASE_DATA (self, bz_weak_release);
    BZ_RELEASE_DATA (id, g_free))

BZ_DEFINE_DATA (
    enumerate_disk_io,
    EnumerateDiskIo,
    {
      BzEntryCacheManager *cache;
    },
    BZ_RELEASE_DATA (cache, g_object_unref))

static DexFuture *
init_fiber (GWeakRef *wr);

static DexFuture *
enumerate_disk_groups_fiber (GWeakRef *wr);

static DexFuture *
enumerate_disk_io_fiber (EnumerateDiskIoData *data);

static DexFuture *
enumerate_disk_entries_fiber (GWeakRef *wr);

static DexFuture *
check_for_updates_fiber (GWeakRef *wr);

static DexFuture *
cache_flathub_fiber (GWeakRef *wr);

static DexFuture *
cache_groups_fiber (GWeakRef *wr);

static DexFuture *
respond_to_flatpak_fiber (RespondToFlatpakData *data);

static DexFuture *
open_appstream_fiber (OpenAppstreamData *data);

static DexFuture *
open_flatpakref_fiber (OpenFlatpakrefData *data);

static DexFuture *
open_metainfo_fiber (OpenMetainfoData *data);

static void
open_metainfo_take (BzApplication *self,
                    GFile         *file);

static DexFuture *
backend_sync_finally (DexFuture *future,
                      GWeakRef  *wr);

static DexFuture *
backend_sync_save_groups_finally (DexFuture *future,
                                  GWeakRef  *wr);

static DexFuture *
init_fiber_finally (DexFuture *future,
                    GWeakRef  *wr);

static DexFuture *
init_sync_finally (DexFuture *future,
                   GWeakRef  *wr);

static DexFuture *
flathub_update_finally (DexFuture *future,
                        GWeakRef  *wr);

static DexFuture *
cache_write_back_finally (DexFuture          *future,
                          CacheWriteBackData *data);

static DexFuture *
sync_finally (DexFuture *future,
              GWeakRef  *wr);

static DexFuture *
watch_backend_notifs_then_loop_cb (DexFuture *future,
                                   GWeakRef  *wr);

static BzEntryGroup *
ensure_group_and_add (BzApplication *self,
                      const char    *id,
                      BzEntry       *entry,
                      BzEntry       *eol_runtime,
                      gboolean       ignore_eol,
                      gboolean       installed);

static void
fiber_replace_entry (BzApplication *self,
                     BzEntry       *entry);

static void
fiber_check_for_updates (BzApplication *self);

static GFile *
fiber_dup_cache_file (const char *name,
                      char      **path_out,
                      GError    **error);

static gboolean
periodic_timeout_cb (BzApplication *self);

static gboolean
scheduled_timeout_cb (GWeakRef *wr);

static void
network_status_changed (BzApplication   *self,
                        GParamSpec      *pspec,
                        GNetworkMonitor *network);

static void
disable_blocklists_changed (BzApplication *self,
                            GParamSpec    *pspec,
                            BzStateInfo   *state);

static void
show_hide_app_setting_changed (BzApplication *self,
                               const char    *key,
                               GSettings     *settings);

static gboolean
window_close_request (BzApplication *self,
                      GtkWidget     *window);

static void
blocklists_changed (BzApplication *self,
                    guint          position,
                    guint          removed,
                    guint          added,
                    GListModel    *model);

static void
txt_blocklists_changed (BzApplication *self,
                        guint          position,
                        guint          removed,
                        guint          added,
                        GListModel    *model);

static void
init_service_struct (BzApplication *self,
                     GtkStringList *blocklists,
                     GtkStringList *txt_blocklists,
                     GtkStringList *curated_configs);

static GtkWindow *
get_or_create_window (BzApplication *self);

static GtkWindow *
new_window (BzApplication *self);

static void
open_appstream_take (BzApplication *self,
                     char          *appstream);

static void
open_flatpakref_take (BzApplication *self,
                      GFile         *file);

static void
command_line_open_location (BzApplication           *self,
                            GApplicationCommandLine *cmdline,
                            const char              *path);

static void
open_generic_id (BzApplication *self,
                 const char    *generic_id);

static gpointer
map_strings_to_files (GtkStringObject *string,
                      gpointer         data);

static gpointer
map_generic_ids_to_groups (GtkStringObject *string,
                           BzApplication   *self);

static gpointer
map_ids_to_entries (GtkStringObject *string,
                    BzApplication   *self);

static gboolean
filter_application_ids (GtkStringObject *string,
                        BzApplication   *self);

static gboolean
filter_entry_groups (BzEntryGroup  *group,
                     BzApplication *self);

static gint
cmp_group (BzEntryGroup *a,
           BzEntryGroup *b,
           gpointer      user_data);

static gboolean
validate_group_for_ui (BzApplication *self,
                       BzEntryGroup  *group);

static DexFuture *
make_sync_future (BzApplication *self);

static void
finish_with_background_task_label (BzApplication *self);

static void
state_info_set_icon_themes (BzStateInfo *state);

static void
bz_application_dispose (GObject *object)
{
  BzApplication *self = BZ_APPLICATION (object);

  dex_clear (&self->first_window_opened);
  dex_clear (&self->flatpak_notifs);
  dex_clear (&self->notif_watch);
  dex_clear (&self->ready_to_open_files);
  dex_clear (&self->sync);
  g_clear_handle_id (&self->periodic_timeout_source, g_source_remove);
  g_clear_object (&self->appid_filter);
  g_clear_object (&self->application_factory);
  g_clear_object (&self->blocklist_parser);
  g_clear_object (&self->blocklists);
  g_clear_object (&self->blocklists_provider);
  g_clear_object (&self->blocklists_to_files);
  g_clear_object (&self->cache);
  g_clear_object (&self->curated_configs);
  g_clear_object (&self->curated_configs_to_files);
  g_clear_object (&self->curated_parser);
  g_clear_object (&self->curated_provider);
  g_clear_object (&self->entry_factory);
  g_clear_object (&self->flathub);
  g_clear_object (&self->flatpak);
  g_clear_object (&self->group_filter);
  g_clear_object (&self->group_filter_model);
  g_clear_object (&self->groups);
  g_clear_object (&self->installed_apps);
  g_clear_object (&self->malcontent);
  g_clear_object (&self->internal_config);
  g_clear_object (&self->network);
  g_clear_object (&self->search_biases);
  g_clear_object (&self->search_biases_backing);
  g_clear_object (&self->search_engine);
  g_clear_object (&self->settings);
  g_clear_object (&self->state);
  g_clear_object (&self->tmp_flathub);
  g_clear_object (&self->transactions);
  g_clear_object (&self->txt_blocklist_parser);
  g_clear_object (&self->txt_blocklists);
  g_clear_object (&self->txt_blocklists_provider);
  g_clear_object (&self->txt_blocklists_to_files);
  g_clear_pointer (&self->blocklist_regexes, g_ptr_array_unref);
  g_clear_pointer (&self->eol_runtimes, g_hash_table_unref);
  g_clear_pointer (&self->ids_to_groups, g_hash_table_unref);
  g_clear_pointer (&self->ignore_eol_set, g_hash_table_unref);
  g_clear_pointer (&self->init_timer, g_timer_destroy);
  g_clear_pointer (&self->installed_set, g_hash_table_unref);
  g_clear_pointer (&self->sys_name_to_addons, g_hash_table_unref);
  g_clear_pointer (&self->txt_blocked_id_sets, g_ptr_array_unref);
  g_clear_pointer (&self->usr_name_to_addons, g_hash_table_unref);
  g_clear_pointer (&self->sys_ref_to_addon_group_ids, g_hash_table_unref);
  g_clear_pointer (&self->usr_ref_to_addon_group_ids, g_hash_table_unref);
  g_weak_ref_clear (&self->main_window);

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

static void
bz_application_activate (GApplication *app)
{
  BzApplication *self = BZ_APPLICATION (app);

  new_window (self);
}

static int
bz_application_command_line (GApplication            *app,
                             GApplicationCommandLine *cmdline)
{
  BzApplication *self                 = BZ_APPLICATION (app);
  g_autoptr (GError) local_error      = NULL;
  gint argc                           = 0;
  g_auto (GStrv) argv                 = NULL;
  gboolean help                       = FALSE;
  g_auto (GStrv) blocklists_strv      = NULL;
  g_auto (GStrv) content_configs_strv = NULL;
  g_auto (GStrv) locations            = NULL;
  g_autofree char *search_term        = NULL;

  GOptionEntry main_entries[] = {
    { "help", 0, 0, G_OPTION_ARG_NONE, &help, "Print help" },
    { "no-window", 0, 0, G_OPTION_ARG_NONE, NULL, "Ensure the service is running without creating a new window (daemon)" },
    { "extra-blocklist", 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &blocklists_strv, "Add an extra blocklist to read from" },
    { "extra-curated-config", 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &content_configs_strv, "Add an extra yaml file with which to configure the app browser" },
    /* Here for backwards compat */
    { "extra-content-config", 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &content_configs_strv, "Add an extra yaml file with which to configure the app browser (backwards compat)" },
    { "search-for", 0, 0, G_OPTION_ARG_STRING, &search_term, "Open search with this term" },
    { G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &locations, "flatpakref file to open" },
    { NULL }
  };

  argv = g_application_command_line_get_arguments (cmdline, &argc);
  g_debug ("Handling gapplication command line; argc=%d, argv= \\", argc);
  for (guint i = 0; i < argc; i++)
    {
      g_debug ("  [%d] %s", i, argv[i]);
    }

  if (argv != NULL && argc > 0)
    {
      g_autofree GStrv argv_shallow      = NULL;
      g_autoptr (GOptionContext) context = NULL;

      argv_shallow = g_memdup2 (argv, sizeof (*argv) * argc);

      context = g_option_context_new ("- an app center for GNOME");
      g_option_context_set_help_enabled (context, FALSE);
      g_option_context_add_main_entries (context, main_entries, NULL);
      if (!g_option_context_parse (context, &argc, &argv_shallow, &local_error))
        {
          g_application_command_line_printerr (cmdline, "%s\n", local_error->message);
          return EXIT_FAILURE;
        }

      if (help)
        {
          g_autofree char *help_text = NULL;

          if (self->running)
            g_application_command_line_printerr (cmdline, "The Bazaar service is running.\n\n");
          else
            g_application_command_line_printerr (cmdline, "The Bazaar service is not running.\n\n");

          help_text = g_option_context_get_help (context, TRUE, NULL);
          g_application_command_line_printerr (cmdline, "%s\n", help_text);
          return EXIT_SUCCESS;
        }
    }

  if (!self->running)
    {
      g_autoptr (GtkStringList) blocklists      = NULL;
      g_autoptr (GtkStringList) txt_blocklists  = NULL;
      g_autoptr (GtkStringList) content_configs = NULL;
      g_autoptr (DexFuture) init                = NULL;

      self->running = TRUE;

      blocklists      = gtk_string_list_new (NULL);
      txt_blocklists  = gtk_string_list_new (NULL);
      content_configs = gtk_string_list_new (NULL);
      init_service_struct (self, blocklists, txt_blocklists, content_configs);

#ifdef HARDCODED_BLOCKLIST
      g_debug ("Bazaar was configured with a hardcoded txt blocklist at %s, adding that now...",
               HARDCODED_BLOCKLIST);
      gtk_string_list_append (txt_blocklists, HARDCODED_BLOCKLIST);
#endif
      if (blocklists_strv != NULL)
        gtk_string_list_splice (
            txt_blocklists,
            g_list_model_get_n_items (G_LIST_MODEL (txt_blocklists)),
            0,
            (const char *const *) blocklists_strv);

#ifdef HARDCODED_CONTENT_CONFIG
      g_debug ("Bazaar was configured with a hardcoded curated content config at %s, adding that now...",
               HARDCODED_CONTENT_CONFIG);
      gtk_string_list_append (content_configs, HARDCODED_CONTENT_CONFIG);
#endif
      if (content_configs_strv != NULL)
        gtk_string_list_splice (
            content_configs,
            g_list_model_get_n_items (G_LIST_MODEL (content_configs)),
            0,
            (const char *const *) content_configs_strv);

      g_timer_start (self->init_timer);
      init = dex_scheduler_spawn (
          dex_scheduler_get_default (),
          bz_get_dex_stack_size (),
          (DexFiberFunc) init_fiber,
          bz_track_weak (self),
          bz_weak_release);
      init = dex_future_finally (
          init,
          (DexFutureCallback) init_fiber_finally,
          bz_track_weak (self),
          bz_weak_release);
      dex_future_disown (g_steal_pointer (&init));
    }

  if ((locations == NULL || *locations == NULL) && search_term == NULL)
    new_window (self);
  else
    get_or_create_window (self);

  if (locations != NULL && *locations != NULL)
    command_line_open_location (self, cmdline, locations[0]);

  if (search_term != NULL)
    {
      GtkWindow *window = NULL;

      window = get_or_create_window (self);
      if (adw_application_window_get_visible_dialog (ADW_APPLICATION_WINDOW (window)) != NULL)
        window = new_window (self);

      bz_window_search (BZ_WINDOW (window), search_term);
    }

  return EXIT_SUCCESS;
}

static gboolean
bz_application_local_command_line (GApplication *application,
                                   gchar      ***arguments,
                                   int          *exit_status)
{
  return FALSE;
}

static void
bz_application_class_init (BzApplicationClass *klass)
{
  GObjectClass      *object_class = G_OBJECT_CLASS (klass);
  GApplicationClass *app_class    = G_APPLICATION_CLASS (klass);

  object_class->dispose = bz_application_dispose;

  app_class->activate           = bz_application_activate;
  app_class->command_line       = bz_application_command_line;
  app_class->local_command_line = bz_application_local_command_line;

  g_type_ensure (BZ_TYPE_RESULT);
}

static void
bz_application_toggle_debug_mode_action (GSimpleAction *action,
                                         GVariant      *parameter,
                                         gpointer       user_data)
{
  BzApplication *self       = user_data;
  gboolean       debug_mode = FALSE;

  debug_mode = bz_state_info_get_debug_mode (self->state);
  bz_state_info_set_debug_mode (self->state, !debug_mode);
}

static void
bz_application_bazaar_inspector_action (GSimpleAction *action,
                                        GVariant      *parameter,
                                        gpointer       user_data)
{
  BzApplication *self      = user_data;
  BzInspector   *inspector = NULL;

  g_assert (BZ_IS_APPLICATION (self));

  inspector = bz_inspector_new ();
  bz_inspector_set_state (inspector, self->state);

  gtk_application_add_window (GTK_APPLICATION (self), GTK_WINDOW (inspector));
  gtk_window_present (GTK_WINDOW (inspector));
}

static void
bz_application_donate_action (GSimpleAction *action,
                              GVariant      *parameter,
                              gpointer       user_data)
{
  BzApplication *self   = user_data;
  GtkWindow     *window = NULL;
  AdwDialog     *dialog = NULL;

  g_assert (BZ_IS_APPLICATION (self));

  window = get_or_create_window (self);
  dialog = bz_donations_dialog_new ();
  adw_dialog_present (dialog, GTK_WIDGET (window));

  bz_state_info_set_donation_prompt_dismissed (self->state, TRUE);
}

static void
bz_application_search_action (GSimpleAction *action,
                              GVariant      *parameter,
                              gpointer       user_data)
{
  BzApplication *self         = user_data;
  GtkWindow     *window       = NULL;
  const char    *initial_text = NULL;

  g_assert (BZ_IS_APPLICATION (self));

  window = get_or_create_window (self);

  if (parameter != NULL)
    initial_text = g_variant_get_string (parameter, NULL);

  bz_window_search (BZ_WINDOW (window), initial_text);
}

static void
bz_application_show_app_id_action (GSimpleAction *action,
                                   GVariant      *parameter,
                                   gpointer       user_data)
{
  BzApplication *self   = user_data;
  GtkWindow     *window = NULL;
  const char    *app_id = NULL;

  g_assert (BZ_IS_APPLICATION (self));

  window = get_or_create_window (self);

  if (parameter != NULL)
    {
      app_id = g_variant_get_string (parameter, NULL);
      bz_window_show_app_id (BZ_WINDOW (window), app_id);
    }
}

static void
bz_application_sync_remotes_action (GSimpleAction *action,
                                    GVariant      *parameter,
                                    gpointer       user_data)
{
  BzApplication *self = user_data;

  g_assert (BZ_IS_APPLICATION (self));

  if (self->sync != NULL &&
      dex_future_is_pending (self->sync))
    return;

  dex_clear (&self->sync);
  self->sync = make_sync_future (self);
}

static void
bz_application_about_action (GSimpleAction *action,
                             GVariant      *parameter,
                             gpointer       user_data)
{
  BzApplication *self   = user_data;
  GtkWindow     *window = NULL;
  AdwDialog     *dialog = NULL;

  const char *developers[] = {
    "Adam Masciola <kolunmi@posteo.net>",
    "Alexander Vanhee",
    NULL
  };

  const char *special_thanks[] = {
    "arewelibadwaitayet https://arewelibadwaitayet.com/",
    /* This array MUST be NULL terminated */
    NULL
  };

  g_assert (BZ_IS_APPLICATION (self));

  window = gtk_application_get_active_window (GTK_APPLICATION (self));
  dialog = adw_about_dialog_new ();

  g_object_set (
      dialog,
      "application-name", "Bazaar",
      "application-icon", "io.github.kolunmi.Bazaar",
      "developer-name", _ ("The Bazaar Contributors"),
      "developers", developers,
      // Translators: Put one translator per line, in the form NAME <EMAIL>, YEAR1, YEAR2
      "translator-credits", _ ("translator-credits"),
      "version", PACKAGE_VCS_VERSION,
      "copyright", "© 2025-2026 The Bazaar Contributors",
      "license-type", GTK_LICENSE_GPL_3_0,
      "website", "https://usebazaar.org",
      "issue-url", "https://github.com/bazaar-org/bazaar/issues",
      NULL);

  adw_about_dialog_add_acknowledgement_section (
      ADW_ABOUT_DIALOG (dialog),
      _ ("Special Thanks"),
      special_thanks);

  adw_dialog_present (dialog, GTK_WIDGET (window));
}

static void
bz_application_preferences_action (GSimpleAction *action,
                                   GVariant      *parameter,
                                   gpointer       user_data)
{
  BzApplication *self        = user_data;
  GtkWindow     *window      = NULL;
  AdwDialog     *preferences = NULL;

  g_assert (BZ_IS_APPLICATION (self));

  window      = gtk_application_get_active_window (GTK_APPLICATION (self));
  preferences = bz_preferences_dialog_new (self->state);

  adw_dialog_present (preferences, GTK_WIDGET (window));
}

static void
bz_application_flathub_login_action (GSimpleAction *action,
                                     GVariant      *parameter,
                                     gpointer       user_data)
{
  BzApplication     *self       = user_data;
  GtkWindow         *window     = NULL;
  BzAuthState       *auth_state = NULL;
  AdwNavigationPage *login_page = NULL;

  g_assert (BZ_IS_APPLICATION (self));

  window = gtk_application_get_active_window (GTK_APPLICATION (self));

  auth_state = bz_state_info_get_auth_state (self->state);
  login_page = bz_login_page_new (auth_state);

  bz_window_push_page (BZ_WINDOW (window), login_page);
}

static void
bz_application_flathub_logout_action (GSimpleAction *action,
                                      GVariant      *parameter,
                                      gpointer       user_data)
{
  BzApplication *self       = BZ_APPLICATION (user_data);
  GtkWindow     *window     = gtk_application_get_active_window (GTK_APPLICATION (self));
  BzAuthState   *auth_state = bz_state_info_get_auth_state (self->state);

  g_assert (BZ_IS_WINDOW (window));

  bz_auth_state_clear (auth_state);

  bz_window_add_toast (
      BZ_WINDOW (window),
      adw_toast_new (_ ("Logged Out Successfully!")));
}

static void
bz_application_flathub_favorites_action (GSimpleAction *action,
                                         GVariant      *parameter,
                                         gpointer       user_data)
{
  BzApplication     *self           = user_data;
  GtkWindow         *window         = NULL;
  AdwNavigationPage *favorites_page = NULL;

  g_assert (BZ_IS_APPLICATION (self));

  window         = get_or_create_window (self);
  favorites_page = ADW_NAVIGATION_PAGE (bz_favorites_page_new (self->state));

  bz_window_push_page (BZ_WINDOW (window), favorites_page);
}

static void
bz_application_quit_action (GSimpleAction *action,
                            GVariant      *parameter,
                            gpointer       user_data)
{
  BzApplication *self              = user_data;
  g_autoptr (GDBusConnection) conn = NULL;
  g_autoptr (GError) error         = NULL;

  conn = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, &error);
  if (conn != NULL)
    g_dbus_connection_call_sync (
        conn, "io.github.kolunmi.Bazaar.SearchProvider",
        "/io/github/kolunmi/Bazaar/SearchProvider",
        "io.github.kolunmi.Bazaar.Daemon",
        "Quit", NULL, NULL, G_DBUS_CALL_FLAGS_NONE,
        -1, NULL, &error);

  if (error != NULL)
    g_warning ("Failed to quit daemon: %s", error->message);

  g_application_quit (G_APPLICATION (self));
}

static const GActionEntry app_actions[] = {
  {     "flathub-login",     bz_application_flathub_login_action, NULL },
  {    "flathub-logout",    bz_application_flathub_logout_action, NULL },
  { "flathub-favorites", bz_application_flathub_favorites_action, NULL },
  {              "quit",              bz_application_quit_action, NULL },
  {       "preferences",       bz_application_preferences_action, NULL },
  {             "about",             bz_application_about_action, NULL },
  {      "sync-remotes",      bz_application_sync_remotes_action, NULL },
  {            "search",            bz_application_search_action,  "s" },
  {       "show-app-id",       bz_application_show_app_id_action,  "s" },
  {            "donate",            bz_application_donate_action, NULL },
  {  "bazaar-inspector",  bz_application_bazaar_inspector_action, NULL },
  { "toggle-debug-mode", bz_application_toggle_debug_mode_action, NULL },
};

static void
bz_application_init (BzApplication *self)
{
  self->running = FALSE;
  g_weak_ref_init (&self->main_window, NULL);

  g_action_map_add_action_entries (
      G_ACTION_MAP (self),
      app_actions,
      G_N_ELEMENTS (app_actions),
      self);
  gtk_application_set_accels_for_action (
      GTK_APPLICATION (self),
      "app.quit",
      (const char *[]){ "<primary>q", NULL });
  gtk_application_set_accels_for_action (
      GTK_APPLICATION (self),
      "app.preferences",
      (const char *[]){ "<primary>comma", NULL });
  gtk_application_set_accels_for_action (
      GTK_APPLICATION (self),
      "app.sync-remotes",
      (const char *[]){ "<primary>r", NULL });
  gtk_application_set_accels_for_action (
      GTK_APPLICATION (self),
      "app.search('')",
      (const char *[]){ "<primary>f", NULL });
  gtk_application_set_accels_for_action (
      GTK_APPLICATION (self),
      "app.bazaar-inspector",
      (const char *[]){ "<primary><alt><shift>i", NULL });
  gtk_application_set_accels_for_action (
      GTK_APPLICATION (self),
      "app.toggle-debug-mode",
      (const char *[]){ "<primary><alt>d", NULL });
}

BzStateInfo *
bz_state_info_get_default (void)
{
  GApplication  *app  = NULL;
  BzApplication *self = NULL;

  app = g_application_get_default ();
  if G_UNLIKELY (app == NULL)
    {
      static BzStateInfo *fallback_state = NULL;

      if (g_once_init_enter_pointer (&fallback_state))
        {
          g_autoptr (BzStateInfo) tmp = NULL;

          tmp = bz_state_info_new ();
          // state_info_set_icon_themes (tmp);

          g_once_init_leave_pointer (
              &fallback_state,
              g_steal_pointer (&tmp));
        }

      return fallback_state;
    }

  self = (BzApplication *) app;
  g_assert (BZ_IS_APPLICATION (self));

  return self->state;
}

static DexFuture *
init_fiber (GWeakRef *wr)
{
  g_autoptr (BzApplication) self        = NULL;
  g_autoptr (GError) local_error        = NULL;
  g_autofree char *root_cache_dir       = NULL;
  g_autoptr (GFile) root_cache_dir_file = NULL;
  g_autoptr (GListModel) repos          = NULL;
  gboolean has_flathub                  = FALSE;
  gboolean cache_has_flathub            = FALSE;
  gboolean result                       = FALSE;
  g_autoptr (BzAuthState) auth_state    = NULL;
  g_autofree char *flathub_cache        = NULL;
  g_autoptr (GFile) flathub_cache_file  = NULL;
  g_autofree char *cache_version_path   = NULL;
  g_autoptr (GFile) cache_version_file  = NULL;

  bz_weak_get_or_return_reject (self, wr);

/* Here until Ubuntu fixes its apparmor crap. */
#ifdef SANDBOXED_LIBFLATPAK
  if (g_settings_get_boolean (self->settings, "check-for-ubuntu"))
    {
      g_autoptr (GSubprocessLauncher) os_check_launcher = NULL;
      g_autoptr (GSubprocess) os_check                  = NULL;
      g_autoptr (GError) os_check_error                 = NULL;
      gboolean      os_check_result                     = FALSE;
      GInputStream *os_check_stdout_pipe                = NULL;
      g_autoptr (GBytes) os_out                         = NULL;

      os_check_launcher = g_subprocess_launcher_new (G_SUBPROCESS_FLAGS_STDOUT_PIPE);
      os_check          = g_subprocess_launcher_spawn (os_check_launcher, &os_check_error,
                                                       "flatpak-spawn", "--host", "cat", "/usr/lib/os-release", NULL);

      if (os_check != NULL)
        {
          os_check_result = dex_await (dex_subprocess_wait_check (os_check), &os_check_error);
          if (os_check_result)
            {
              os_check_stdout_pipe = g_subprocess_get_stdout_pipe (os_check);
              os_out               = g_input_stream_read_bytes (os_check_stdout_pipe, 4096, NULL, &os_check_error);
            }
          else
            g_warning ("Ubuntu check subprocess failed: %s", os_check_error->message);
        }
      else
        g_warning ("Failed to spawn ubuntu check subprocess: %s", os_check_error->message);

      if (os_out != NULL &&
          strstr (g_bytes_get_data (os_out, NULL), "ID=ubuntu") != NULL)
        {
          GtkWindow       *window   = NULL;
          AdwDialog       *alert    = NULL;
          g_autofree char *response = NULL;

          dex_await (dex_ref (DEX_FUTURE (self->first_window_opened)), NULL);

          window = gtk_application_get_active_window (GTK_APPLICATION (self));
          if (window == NULL)
            window = new_window (self);

          alert = adw_alert_dialog_new (NULL, NULL);
          adw_alert_dialog_format_heading (ADW_ALERT_DIALOG (alert), _ ("Ubuntu Troubles!"));
          adw_alert_dialog_format_body_markup (
              ADW_ALERT_DIALOG (alert),
              _ ("Ubuntu 25.10 and newer have messed up default security rules, "
                 "making it <b>impossible to install apps</b> from the Flatpak version "
                 "of Bazaar, please just use the normal package for now by using\n\n"
                 "<tt>sudo apt install bazaar</tt>\n\n"
                 "You can then remove this Flatpak version with\n\n"
                 "<tt>flatpak uninstall io.github.kolunmi.Bazaar</tt>"));
          adw_alert_dialog_add_response (ADW_ALERT_DIALOG (alert), "ok", _ ("Continue Anyway"));
          adw_alert_dialog_set_default_response (ADW_ALERT_DIALOG (alert), "ok");
          adw_alert_dialog_set_close_response (ADW_ALERT_DIALOG (alert), "ok");

          adw_dialog_present (alert, GTK_WIDGET (window));
          response = dex_await_string (bz_make_alert_dialog_future (ADW_ALERT_DIALOG (alert)), NULL);

          g_settings_set_boolean (self->settings, "check-for-ubuntu", FALSE);
        }
      else
        g_settings_set_boolean (self->settings, "check-for-ubuntu", FALSE);
    }
#endif

  bz_state_info_set_online (self->state, TRUE);
  bz_state_info_set_busy (self->state, TRUE);
  bz_state_info_set_background_task_label (self->state, _ ("Performing setup…"));

  root_cache_dir      = bz_dup_root_cache_dir ();
  root_cache_dir_file = g_file_new_for_path (root_cache_dir);
  cache_version_path  = g_build_filename (root_cache_dir, "cache-version", NULL);
  cache_version_file  = g_file_new_for_path (cache_version_path);

  if (dex_await (dex_file_query_exists (root_cache_dir_file), NULL))
    {
      gboolean wipe_cache = TRUE;

      if (dex_await (dex_file_query_exists (cache_version_file), NULL))
        {
          g_autoptr (GBytes) bytes = NULL;

          bytes = dex_await_boxed (dex_file_load_contents_bytes (cache_version_file), NULL);
          if (bytes != NULL)
            {
              g_autoptr (GVariant) variant = NULL;

              variant = g_variant_new_from_bytes (G_VARIANT_TYPE_STRING, bytes, FALSE);
              if (variant != NULL)
                {
                  const char *version = NULL;

                  version    = g_variant_get_string (variant, NULL);
                  wipe_cache = g_strcmp0 (version, CACHE_VERSION) != 0;
                }
            }
        }

      if (wipe_cache)
        {
          bz_state_info_set_donation_prompt_dismissed (self->state, FALSE);
          g_settings_set_int64 (self->settings, "last-refresh-time", 0);

          g_info ("Version incompatibility detected: clearing cache");
          dex_await (bz_reap_file_dex (root_cache_dir_file), NULL);
        }
    }
  else
    bz_state_info_set_donation_prompt_dismissed (self->state, TRUE);

  {
    g_autoptr (GError) mkdir_error = NULL;

    dex_await (dex_file_make_directory_with_parents (root_cache_dir_file), &mkdir_error);
    if (mkdir_error == NULL || mkdir_error->code == G_IO_ERROR_EXISTS)
      {
        g_autoptr (GVariant) variant = NULL;
        g_autoptr (GBytes) bytes     = NULL;

        variant = g_variant_new_string (CACHE_VERSION);
        bytes   = g_variant_get_data_as_bytes (variant);
        dex_await (dex_file_replace_contents_bytes (
                       cache_version_file, bytes, NULL, FALSE,
                       G_FILE_CREATE_REPLACE_DESTINATION),
                   NULL);
      }
    else
      g_warning ("Unable to ensure cache directory: %s", mkdir_error->message);
  }

  g_clear_object (&self->flatpak);
  self->flatpak = dex_await_object (bz_flatpak_instance_new (), &local_error);
  if (self->flatpak == NULL)
    return dex_future_new_for_error (g_steal_pointer (&local_error));
  bz_transaction_manager_set_backend (self->transactions, BZ_BACKEND (self->flatpak));
  bz_state_info_set_backend (self->state, BZ_BACKEND (self->flatpak));

  has_flathub = dex_await_boolean (
      bz_flatpak_instance_has_flathub (self->flatpak, NULL),
      &local_error);
  if (local_error != NULL)
    return dex_future_new_for_error (g_steal_pointer (&local_error));

  if (!has_flathub)
    {
      GtkWindow       *window   = NULL;
      g_autofree char *response = NULL;
      AdwDialog       *alert    = NULL;

      dex_await (dex_ref (DEX_FUTURE (self->first_window_opened)), NULL);

      window = gtk_application_get_active_window (GTK_APPLICATION (self));
      if (window == NULL)
        window = new_window (self);

      alert = adw_alert_dialog_new (NULL, NULL);

#ifdef SANDBOXED_LIBFLATPAK
      adw_alert_dialog_format_heading (
          ADW_ALERT_DIALOG (alert),
          _ ("Set Up System Flathub?"));
      adw_alert_dialog_format_body (
          ADW_ALERT_DIALOG (alert),
          _ ("The system Flathub remote is not set up. Bazaar requires "
             "Flathub to be configured on the system Flatpak installation "
             "to browse and install applications.\n\n"
             "You can still use Bazaar to browse and remove already installed apps."));
#else
      adw_alert_dialog_format_heading (
          ADW_ALERT_DIALOG (alert),
          _ ("Set Up Flathub?"));
      adw_alert_dialog_format_body (
          ADW_ALERT_DIALOG (alert),
          _ ("Flathub is not set up on this system. "
             "You will not be able to browse and install applications in Bazaar if its unavailable.\n\n"
             "You can still use Bazaar to browse and remove already installed apps."));
#endif
      adw_alert_dialog_add_responses (
          ADW_ALERT_DIALOG (alert),
          "later", _ ("Later"),
          "add", _ ("Set Up Flathub"),
          NULL);
      adw_alert_dialog_set_response_appearance (
          ADW_ALERT_DIALOG (alert), "add", ADW_RESPONSE_SUGGESTED);
      adw_alert_dialog_set_default_response (ADW_ALERT_DIALOG (alert), "add");
      adw_alert_dialog_set_close_response (ADW_ALERT_DIALOG (alert), "later");

      adw_dialog_present (alert, GTK_WIDGET (window));
      response = dex_await_string (
          bz_make_alert_dialog_future (ADW_ALERT_DIALOG (alert)),
          NULL);

      if (response != NULL &&
          g_strcmp0 (response, "add") == 0)
        {
          result = dex_await (
              bz_flatpak_instance_ensure_has_flathub (self->flatpak, NULL),
              &local_error);
          if (result)
            has_flathub = TRUE;
          else
            {
              g_warning ("Failed to install flathub: %s",
                         local_error->message);
              g_clear_error (&local_error);
            }
        }
    }
  bz_state_info_set_has_flathub (self->state, has_flathub);

  self->installed_set = dex_await_boxed (
      bz_backend_retrieve_install_ids (
          BZ_BACKEND (self->flatpak), NULL),
      &local_error);
  if (self->installed_set == NULL)
    {
      g_warning ("Unable to enumerate installed entries from flatpak backend; "
                 "no entries will appear to be installed: %s",
                 local_error->message);
      g_clear_error (&local_error);

      self->installed_set = g_hash_table_new_full (
          g_str_hash, g_str_equal, g_free, g_free);
    }

  repos = dex_await_object (
      bz_backend_list_repositories (BZ_BACKEND (self->flatpak), NULL),
      &local_error);

  if (repos != NULL)
    bz_state_info_set_repositories (self->state, repos);
  else
    {
      g_warning ("Failed to enumerate repositories: %s", local_error->message);
      g_clear_error (&local_error);
    }

  /* Revive old cache from previous Bazaar process */
  cache_has_flathub = dex_await_boolean (
      dex_scheduler_spawn (
          dex_scheduler_get_default (),
          bz_get_dex_stack_size (),
          (DexFiberFunc) enumerate_disk_groups_fiber,
          bz_track_weak (self),
          bz_weak_release),
      NULL);

  self->had_cache_on_init = g_list_model_get_n_items (G_LIST_MODEL (self->groups)) > 0;

  flathub_cache_file = fiber_dup_cache_file ("flathub-cache", &flathub_cache, &local_error);
  if (flathub_cache_file != NULL)
    {
      if (dex_await (dex_file_query_exists (flathub_cache_file), NULL))
        {
          g_autoptr (GBytes) bytes = NULL;

          bytes = dex_await_boxed (
              dex_file_load_contents_bytes (flathub_cache_file),
              &local_error);
          if (bytes != NULL)
            {
              g_autoptr (GVariant) variant       = NULL;
              g_autoptr (BzFlathubState) flathub = NULL;

              variant = g_variant_new_from_bytes (G_VARIANT_TYPE_VARDICT, bytes, FALSE);
              if (variant == NULL)
                {
                  g_warning ("Failed to interpret cached flathub state from %s: %s",
                             flathub_cache, local_error->message);
                  g_clear_error (&local_error);
                }

              flathub = bz_flathub_state_new ();
              result  = bz_serializable_deserialize (
                  BZ_SERIALIZABLE (flathub), variant, &local_error);
              if (result)
                {
                  self->flathub = g_steal_pointer (&flathub);
                  bz_flathub_state_set_map_factory (self->flathub, self->application_factory);
                  bz_state_info_set_flathub (self->state, self->flathub);

                  if (cache_has_flathub)
                    {
                      dex_promise_resolve_boolean (self->ready_to_open_files, TRUE);
                      bz_state_info_set_busy (self->state, FALSE);
                    }
                }
              else
                {
                  g_warning ("Failed to deserialize cached flathub state from %s: %s",
                             flathub_cache, local_error->message);
                  g_clear_error (&local_error);
                }
            }
          else
            {
              g_warning ("Failed to decache cache flathub state from %s: %s",
                         flathub_cache, local_error->message);
              g_clear_error (&local_error);
            }
        }
    }
  else
    {
      g_warning ("Unable to ensure cache directory: %s", local_error->message);
      g_clear_error (&local_error);
    }

  auth_state = bz_auth_state_new ();
  bz_state_info_set_auth_state (self->state, auth_state);

  return dex_future_new_true ();
}

static DexFuture *
enumerate_disk_groups_fiber (GWeakRef *wr)
{
  g_autoptr (BzApplication) self      = NULL;
  g_autoptr (GError) local_error      = NULL;
  g_autofree char *groups_cache       = NULL;
  g_autoptr (GFile) groups_cache_file = NULL;
  g_autoptr (GBytes) bytes            = NULL;
  g_autoptr (GVariant) variant        = NULL;
  g_autoptr (GVariantIter) iter       = NULL;
  gboolean has_flathub_group          = FALSE;

  bz_weak_get_or_return_reject (self, wr);

  groups_cache_file = fiber_dup_cache_file ("groups-cache", &groups_cache, &local_error);
  if (groups_cache_file == NULL)
    {
      g_warning ("Unable to ensure groups cache directory: %s", local_error->message);
      return dex_future_new_for_boolean (FALSE);
    }

  if (!dex_await (dex_file_query_exists (groups_cache_file), NULL))
    return dex_future_new_for_boolean (FALSE);

  bytes = dex_await_boxed (
      dex_file_load_contents_bytes (groups_cache_file),
      &local_error);
  if (bytes == NULL)
    {
      g_warning ("Failed to load groups cache from %s: %s",
                 groups_cache, local_error->message);
      return dex_future_new_for_boolean (FALSE);
    }

  variant = g_variant_new_from_bytes (G_VARIANT_TYPE ("aa{sv}"), bytes, FALSE);
  if (variant == NULL)
    {
      g_warning ("Failed to parse groups cache from %s", groups_cache);
      return dex_future_new_for_boolean (FALSE);
    }

  iter = g_variant_iter_new (variant);
  for (;;)
    {
      g_autoptr (GVariant) group_variant = NULL;
      g_autoptr (BzEntryGroup) group     = NULL;

      group_variant = g_variant_iter_next_value (iter);
      if (group_variant == NULL)
        break;

      group = bz_entry_group_new (self->entry_factory);
      if (bz_entry_group_deserialize (group, group_variant))
        {
          const char *id           = NULL;
          gboolean    is_installed = FALSE;

          is_installed = bz_entry_group_reconcile_with_installed_set (group, self->installed_set);

          if (!has_flathub_group &&
              bz_entry_group_get_is_flathub (group))
            has_flathub_group = TRUE;

          g_list_store_append (self->groups, group);

          id = bz_entry_group_get_id (group);
          if (id != NULL)
            g_hash_table_replace (
                self->ids_to_groups,
                g_strdup (id),
                g_object_ref (group));

          if (is_installed)
            g_list_store_insert_sorted (
                self->installed_apps, group,
                (GCompareDataFunc) cmp_group, NULL);
        }
    }

  gtk_filter_changed (GTK_FILTER (self->group_filter), GTK_FILTER_CHANGE_LESS_STRICT);
  gtk_filter_changed (GTK_FILTER (self->appid_filter), GTK_FILTER_CHANGE_LESS_STRICT);

  dex_future_disown (dex_scheduler_spawn (
      dex_scheduler_get_default (),
      bz_get_dex_stack_size (),
      (DexFiberFunc) check_for_updates_fiber,
      bz_track_weak (self),
      bz_weak_release));

  return dex_future_new_for_boolean (has_flathub_group);
}

static DexFuture *
enumerate_disk_io_fiber (EnumerateDiskIoData *data)
{
  g_autoptr (GError) local_error    = NULL;
  g_autoptr (GHashTable) cached_set = NULL;
  g_autoptr (GPtrArray) entries     = NULL;
  GHashTableIter iter               = { 0 };

  cached_set = dex_await_boxed (
      bz_entry_cache_manager_enumerate_disk (data->cache),
      &local_error);
  if (cached_set == NULL)
    return dex_future_new_for_error (g_steal_pointer (&local_error));

  entries = g_ptr_array_new_with_free_func (g_object_unref);

  g_hash_table_iter_init (&iter, cached_set);
  for (;;)
    {
      g_autoptr (GPtrArray) batch = NULL;
      char *checksum              = NULL;

      batch = g_ptr_array_new_with_free_func (dex_unref);

      while (batch->len < CACHE_ENUM_BATCH_SIZE &&
             g_hash_table_iter_next (&iter, (gpointer *) &checksum, NULL))
        g_ptr_array_add (
            batch,
            bz_entry_cache_manager_get_by_checksum (
                data->cache, checksum));

      if (batch->len > 0)
        dex_await (dex_future_allv (
                       (DexFuture *const *) batch->pdata,
                       batch->len),
                   NULL);

      for (guint i = 0; i < batch->len; i++)
        {
          DexFuture    *future = NULL;
          const GValue *value  = NULL;

          future = g_ptr_array_index (batch, i);
          value  = dex_future_get_value (future, &local_error);
          if (value != NULL)
            g_ptr_array_add (entries, g_value_dup_object (value));
          else
            {
              g_warning ("Unable to retrieve cached entry: %s", local_error->message);
              g_clear_error (&local_error);
            }
        }

      if (batch->len < CACHE_ENUM_BATCH_SIZE)
        break;
    }

  return dex_future_new_take_boxed (G_TYPE_PTR_ARRAY, g_steal_pointer (&entries));
}

static DexFuture *
enumerate_disk_entries_fiber (GWeakRef *wr)
{
  g_autoptr (BzApplication) self          = NULL;
  g_autoptr (GError) local_error          = NULL;
  g_autoptr (GPtrArray) entries           = NULL;
  g_autoptr (EnumerateDiskIoData) io_data = NULL;
  gboolean has_flathub_entry              = FALSE;

  bz_weak_get_or_return_reject (self, wr);

  io_data        = enumerate_disk_io_data_new ();
  io_data->cache = g_object_ref (self->cache);

  entries = dex_await_boxed (
      dex_scheduler_spawn (
          bz_get_io_scheduler (),
          bz_get_dex_stack_size (),
          (DexFiberFunc) enumerate_disk_io_fiber,
          enumerate_disk_io_data_ref (io_data),
          enumerate_disk_io_data_unref),
      &local_error);

  if (entries == NULL)
    {
      g_warning ("Unable to enumerate cached entries: %s", local_error->message);
      return dex_future_new_for_error (g_steal_pointer (&local_error));
    }

  for (guint i = 0; i < entries->len; i++)
    {
      BzEntry *entry = NULL;

      entry = g_ptr_array_index (entries, i);

      if (BZ_IS_FLATPAK_ENTRY (entry) &&
          bz_flatpak_entry_get_bundle_path (BZ_FLATPAK_ENTRY (entry)) != NULL)
        /* refrain from restoring bundle entries */
        continue;

      if (!has_flathub_entry &&
          BZ_IS_FLATPAK_ENTRY (entry) &&
          g_strcmp0 (bz_entry_get_remote_repo_name (entry), "flathub") == 0)
        has_flathub_entry = TRUE;

      fiber_replace_entry (self, entry);
    }

  gtk_filter_changed (GTK_FILTER (self->group_filter), GTK_FILTER_CHANGE_LESS_STRICT);
  gtk_filter_changed (GTK_FILTER (self->appid_filter), GTK_FILTER_CHANGE_LESS_STRICT);

  dex_future_disown (dex_scheduler_spawn (
      dex_scheduler_get_default (),
      bz_get_dex_stack_size (),
      (DexFiberFunc) check_for_updates_fiber,
      bz_track_weak (self),
      bz_weak_release));

  return dex_future_new_for_boolean (has_flathub_entry);
}

static DexFuture *
check_for_updates_fiber (GWeakRef *wr)
{
  g_autoptr (BzApplication) self = NULL;

  bz_weak_get_or_return_reject (self, wr);

  fiber_check_for_updates (self);
  finish_with_background_task_label (self);

  return dex_future_new_true ();
}

static DexFuture *
cache_flathub_fiber (GWeakRef *wr)
{
  g_autoptr (BzApplication) self       = NULL;
  g_autoptr (GError) local_error       = NULL;
  gboolean         result              = FALSE;
  g_autofree char *flathub_cache       = NULL;
  g_autoptr (GFile) flathub_cache_file = NULL;

  bz_weak_get_or_return_reject (self, wr);

  flathub_cache_file = fiber_dup_cache_file ("flathub-cache", &flathub_cache, &local_error);
  if (flathub_cache_file != NULL)
    {
      g_autoptr (GVariantBuilder) builder = NULL;
      g_autoptr (GVariant) variant        = NULL;
      g_autoptr (GBytes) bytes            = NULL;

      builder = g_variant_builder_new (G_VARIANT_TYPE_VARDICT);
      bz_serializable_serialize (BZ_SERIALIZABLE (self->flathub), builder);
      variant = g_variant_builder_end (builder);
      bytes   = g_variant_get_data_as_bytes (variant);

      result = dex_await (
          dex_file_replace_contents_bytes (
              flathub_cache_file, bytes,
              NULL, FALSE,
              G_FILE_CREATE_REPLACE_DESTINATION),
          &local_error);
      if (!result)
        {
          g_warning ("Failed to cache flathub state to %s: %s",
                     flathub_cache, local_error->message);
          g_clear_error (&local_error);
        }
    }
  else
    {
      g_warning ("Unable to ensure cache directory: %s", local_error->message);
      g_clear_error (&local_error);
    }

  return dex_future_new_true ();
}

static DexFuture *
cache_groups_fiber (GWeakRef *wr)
{
  g_autoptr (BzApplication) self      = NULL;
  g_autoptr (GError) local_error      = NULL;
  gboolean         result             = FALSE;
  g_autofree char *groups_cache       = NULL;
  g_autoptr (GFile) groups_cache_file = NULL;
  g_autoptr (GVariantBuilder) builder = NULL;
  g_autoptr (GVariant) variant        = NULL;
  g_autoptr (GBytes) bytes            = NULL;
  guint n_groups                      = 0;
  g_autoptr (GError) index_error      = NULL;
  g_autofree char *module_dir         = NULL;
  g_autofree char *index_path         = NULL;

  bz_weak_get_or_return_reject (self, wr);

  groups_cache_file = fiber_dup_cache_file ("groups-cache", &groups_cache, &local_error);
  if (groups_cache_file == NULL)
    {
      g_warning ("Unable to ensure groups cache directory: %s", local_error->message);
      return dex_future_new_true ();
    }

  builder  = g_variant_builder_new (G_VARIANT_TYPE ("aa{sv}"));
  n_groups = g_list_model_get_n_items (G_LIST_MODEL (self->groups));

  for (guint i = 0; i < n_groups; i++)
    {
      g_autoptr (BzEntryGroup) group = NULL;
      g_autoptr (GVariantBuilder) gb = NULL;

      group = g_list_model_get_item (G_LIST_MODEL (self->groups), i);
      gb    = g_variant_builder_new (G_VARIANT_TYPE ("a{sv}"));

      bz_entry_group_serialize (group, gb);
      g_variant_builder_add (builder, "@a{sv}", g_variant_builder_end (gb));
    }

  variant = g_variant_builder_end (builder);
  bytes   = g_variant_get_data_as_bytes (variant);

  result = dex_await (
      dex_file_replace_contents_bytes (
          groups_cache_file, bytes,
          NULL, FALSE,
          G_FILE_CREATE_REPLACE_DESTINATION),
      &local_error);
  if (!result)
    g_warning ("Failed to cache groups to %s: %s", groups_cache, local_error->message);

  module_dir = bz_dup_module_dir ();
  index_path = g_build_filename (module_dir, "search-index", NULL);

  if (!bz_write_search_index (G_LIST_MODEL (self->groups), index_path, &index_error))
    g_warning ("Failed to write search index to %s: %s", index_path, index_error->message);

  return dex_future_new_true ();
}

static DexFuture *
respond_to_flatpak_fiber (RespondToFlatpakData *data)
{
  g_autoptr (BzApplication) self            = NULL;
  BzBackendNotification *notif              = data->notif;
  g_autoptr (GError) local_error            = NULL;
  g_autoptr (GPtrArray) build_futures       = NULL;
  g_autoptr (GPtrArray) build_notify_groups = NULL;
  g_autoptr (DexFuture) read_future         = NULL;
  g_autoptr (DexFuture) reread_timeout      = NULL;
  gboolean update_labels                    = FALSE;
  gboolean update_filters                   = FALSE;

  bz_weak_get_or_return_reject (self, data->self);

  build_futures       = g_ptr_array_new_with_free_func (dex_unref);
  build_notify_groups = g_ptr_array_new_with_free_func (g_object_unref);

  read_future = dex_future_new_for_object (notif);

  /* `reread_timeout` defines how long we are allowed to spend adding to
     `build-futures` before we update the UI later */
  reread_timeout = dex_timeout_new_msec (100);
  for (;;)
    {
      BzBackendNotificationKind kind = 0;

      if (!dex_future_is_resolved (read_future))
        {
          g_autoptr (DexFuture) future = NULL;

          future = dex_future_all_race (
              dex_ref (reread_timeout),
              dex_ref (read_future),
              NULL);
          dex_await (g_steal_pointer (&future), NULL);
          if (!dex_future_is_pending (reread_timeout))
            break;
        }

      notif = g_value_get_object (dex_future_get_value (read_future, NULL));
      kind  = bz_backend_notification_get_kind (notif);
      switch (kind)
        {
        case BZ_BACKEND_NOTIFICATION_KIND_PRESENT_ID:
          {
            const char *id = NULL;

            id = bz_backend_notification_get_generic_id (notif);
            if (id == NULL)
              break;

            open_generic_id (self, id);
          }
          break;
        case BZ_BACKEND_NOTIFICATION_KIND_ERROR:
          {
            const char *error  = NULL;
            GtkWindow  *window = NULL;

            error = bz_backend_notification_get_error (notif);
            if (error == NULL)
              break;

            g_warning ("Received an error from the flatpak backend: %s", error);

            window = gtk_application_get_active_window (GTK_APPLICATION (self));
            if (window != NULL)
              bz_show_error_for_widget (GTK_WIDGET (window), _ ("A backend error occurred"), error);
          }
          break;
        case BZ_BACKEND_NOTIFICATION_KIND_TELL_INCOMING:
          {
            int n_incoming = 0;

            n_incoming = bz_backend_notification_get_n_incoming (notif);
            self->n_entries_incoming += n_incoming;

            update_labels = TRUE;
          }
          break;
        case BZ_BACKEND_NOTIFICATION_KIND_REPLACE_ENTRY:
          {
            BzEntry *entry = NULL;

            entry = bz_backend_notification_get_entry (notif);
            fiber_replace_entry (self, entry);

            g_ptr_array_add (build_futures, bz_entry_cache_manager_add (self->cache, entry));
            if (bz_entry_is_of_kinds (entry, BZ_ENTRY_KIND_APPLICATION))
              {
                const char   *id    = NULL;
                BzEntryGroup *group = NULL;

                update_filters = TRUE;

                id    = bz_entry_get_id (entry);
                group = g_hash_table_lookup (self->ids_to_groups, id);
                if (group != NULL)
                  g_ptr_array_add (build_notify_groups, g_object_ref (group));
              }

            self->n_entries_incoming--;
            update_labels = TRUE;
          }
          break;
        case BZ_BACKEND_NOTIFICATION_KIND_REMOTE_SYNC_START:
          {
            const char *remote_name = NULL;

            remote_name = bz_backend_notification_get_remote_name (notif);

            if (bz_state_info_get_has_flathub (self->state))
              /* We only count instances of the "flathub" remote if we have
                 flathub, since that's all we care about for the UI */
              {
                if (g_strcmp0 (remote_name, "flathub") == 0)
                  self->n_remotes_syncing++;
              }
            else
              self->n_remotes_syncing++;

            g_debug ("remote '%s' has begun synchronization; "
                     "now currently syncing %u remote(s)",
                     remote_name, self->n_remotes_syncing);
          }
          break;
        case BZ_BACKEND_NOTIFICATION_KIND_REMOTE_SYNC_FINISH:
          {
            const char *remote_name = NULL;

            remote_name = bz_backend_notification_get_remote_name (notif);

            if (bz_state_info_get_has_flathub (self->state))
              {
                if (g_strcmp0 (remote_name, "flathub") == 0)
                  {
                    self->n_remotes_syncing--;
                    if (self->n_remotes_syncing == 0)
                      self->flathub_remote_initialized = TRUE;
                  }
              }
            else
              {
                self->n_remotes_syncing--;
                if (self->n_remotes_syncing == 0)
                  bz_state_info_set_busy (self->state, FALSE);
              }

            g_debug ("remote '%s' has finished synchronization; "
                     "now currently syncing %u remote(s)",
                     remote_name, self->n_remotes_syncing);
          }
          break;
        case BZ_BACKEND_NOTIFICATION_KIND_INSTALL_DONE:
        case BZ_BACKEND_NOTIFICATION_KIND_UPDATE_DONE:
        case BZ_BACKEND_NOTIFICATION_KIND_REMOVE_DONE:
          {
            const char *unique_id     = NULL;
            g_autoptr (BzEntry) entry = NULL;

            unique_id = bz_backend_notification_get_unique_id (notif);
            entry     = dex_await_object (
                bz_entry_cache_manager_get (self->cache, unique_id),
                &local_error);
            if (entry == NULL)
              {
                g_warning ("Backend notification references an entry "
                           "which couldn't be decached: %s",
                           local_error->message);
                break;
              }

            switch (kind)
              {
              case BZ_BACKEND_NOTIFICATION_KIND_INSTALL_DONE:
                {
                  const char *version = NULL;

                  version = bz_backend_notification_get_version (notif);

                  g_hash_table_replace (self->installed_set, g_strdup (unique_id), g_strdup (version));
                  bz_entry_set_installed_version (entry, version);
                  bz_entry_set_installed (entry, TRUE);

                  if (bz_entry_is_of_kinds (entry, BZ_ENTRY_KIND_APPLICATION))
                    {
                      BzEntryGroup *group = NULL;

                      group = g_hash_table_lookup (self->ids_to_groups, bz_entry_get_id (entry));
                      if (group != NULL)
                        {
                          gboolean found    = FALSE;
                          guint    position = 0;

                          found = g_list_store_find (self->installed_apps, group, &position);
                          if (!found)
                            g_list_store_insert_sorted (self->installed_apps, group, (GCompareDataFunc) cmp_group, NULL);
                        }
                    }
                }
                break;
              case BZ_BACKEND_NOTIFICATION_KIND_UPDATE_DONE:
              case BZ_BACKEND_NOTIFICATION_KIND_REMOVE_DONE:
                {
                  gboolean was_rebased = FALSE;

                  was_rebased = bz_backend_notification_get_was_rebased (notif);
                  if (kind == BZ_BACKEND_NOTIFICATION_KIND_UPDATE_DONE)
                    {
                      const char *version = NULL;

                      version = bz_backend_notification_get_version (notif);
                      g_hash_table_replace (self->installed_set, g_strdup (unique_id), g_strdup (version));
                    }

                  if (kind == BZ_BACKEND_NOTIFICATION_KIND_REMOVE_DONE || was_rebased)
                    {
                      bz_entry_set_installed_version (entry, NULL);
                      bz_entry_set_installed (entry, FALSE);
                      g_hash_table_remove (self->installed_set, unique_id);

                      if (bz_entry_is_of_kinds (entry, BZ_ENTRY_KIND_APPLICATION))
                        {
                          BzEntryGroup *group = NULL;

                          group = g_hash_table_lookup (self->ids_to_groups, bz_entry_get_id (entry));
                          if (group != NULL &&
                              (was_rebased || !bz_entry_group_get_removable (group)))
                            {
                              gboolean found    = FALSE;
                              guint    position = 0;

                              found = g_list_store_find (self->installed_apps, group, &position);
                              if (found)
                                g_list_store_remove (self->installed_apps, position);
                            }
                        }
                    }
                }
                break;
              case BZ_BACKEND_NOTIFICATION_KIND_ERROR:
              case BZ_BACKEND_NOTIFICATION_KIND_EXTERNAL_CHANGE:
              case BZ_BACKEND_NOTIFICATION_KIND_INVALIDATE_REMOTES:
              case BZ_BACKEND_NOTIFICATION_KIND_PRESENT_ID:
              case BZ_BACKEND_NOTIFICATION_KIND_REMOTE_SYNC_FINISH:
              case BZ_BACKEND_NOTIFICATION_KIND_REMOTE_SYNC_START:
              case BZ_BACKEND_NOTIFICATION_KIND_REPLACE_ENTRY:
              case BZ_BACKEND_NOTIFICATION_KIND_TELL_INCOMING:
              default:
                g_assert_not_reached ();
              };

            g_ptr_array_add (build_futures, bz_entry_cache_manager_add (self->cache, entry));
            if (bz_entry_is_of_kinds (entry, BZ_ENTRY_KIND_APPLICATION))
              {
                const char   *id    = NULL;
                BzEntryGroup *group = NULL;

                id    = bz_entry_get_id (entry);
                group = g_hash_table_lookup (self->ids_to_groups, id);
                if (group != NULL)
                  g_ptr_array_add (build_notify_groups, g_object_ref (group));
              }
          }
          break;
        case BZ_BACKEND_NOTIFICATION_KIND_INVALIDATE_REMOTES:
        case BZ_BACKEND_NOTIFICATION_KIND_EXTERNAL_CHANGE:
          {
            g_autoptr (GListModel) repos         = NULL;
            g_autoptr (GHashTable) installed_set = NULL;
            g_autoptr (GPtrArray) diff_reads     = NULL;
            GHashTableIter old_iter              = { 0 };
            GHashTableIter new_iter              = { 0 };
            g_autoptr (GPtrArray) diff_writes    = NULL;

            bz_state_info_set_background_task_label (self->state, _ ("Refreshing…"));

            repos = dex_await_object (
                bz_backend_list_repositories (BZ_BACKEND (self->flatpak), NULL),
                &local_error);

            if (repos != NULL)
              bz_state_info_set_repositories (self->state, repos);
            else
              {
                g_warning ("Failed to enumerate repositories: %s", local_error->message);
                g_clear_error (&local_error);
              }

            installed_set = dex_await_boxed (
                bz_backend_retrieve_install_ids (
                    BZ_BACKEND (self->flatpak), NULL),
                &local_error);
            if (installed_set == NULL)
              {
                g_warning ("Failed to enumerate installed entries: %s", local_error->message);
                finish_with_background_task_label (self);
                break;
              }

            diff_reads = g_ptr_array_new_with_free_func (dex_unref);

            g_hash_table_iter_init (&old_iter, self->installed_set);
            for (;;)
              {
                char *unique_id = NULL;

                if (!g_hash_table_iter_next (
                        &old_iter, (gpointer *) &unique_id, NULL))
                  break;

                if (!g_hash_table_contains (installed_set, unique_id))
                  g_ptr_array_add (
                      diff_reads,
                      bz_entry_cache_manager_get (self->cache, unique_id));
              }

            g_hash_table_iter_init (&new_iter, installed_set);
            for (;;)
              {
                char *unique_id = NULL;

                if (!g_hash_table_iter_next (
                        &new_iter, (gpointer *) &unique_id, NULL))
                  break;

                if (!g_hash_table_contains (self->installed_set, unique_id))
                  g_ptr_array_add (
                      diff_reads,
                      bz_entry_cache_manager_get (self->cache, unique_id));
              }

            if (diff_reads->len > 0)
              {
                dex_await (dex_future_allv (
                               (DexFuture *const *) diff_reads->pdata,
                               diff_reads->len),
                           NULL);

                diff_writes = g_ptr_array_new_with_free_func (dex_unref);
                for (guint i = 0; i < diff_reads->len; i++)
                  {
                    DexFuture *future = NULL;

                    future = g_ptr_array_index (diff_reads, i);
                    if (dex_future_is_resolved (future))
                      {
                        BzEntry      *entry     = NULL;
                        const char   *id        = NULL;
                        const char   *unique_id = NULL;
                        BzEntryGroup *group     = NULL;
                        gboolean      installed = FALSE;
                        const char   *version   = NULL;

                        entry = g_value_get_object (dex_future_get_value (future, NULL));
                        id    = bz_entry_get_id (entry);
                        group = g_hash_table_lookup (self->ids_to_groups, id);
                        if (group != NULL)
                          bz_entry_group_connect_living (group, entry);

                        unique_id = bz_entry_get_unique_id (entry);
                        installed = g_hash_table_contains (installed_set, unique_id);

                        version = g_hash_table_lookup (installed_set, unique_id);
                        if (installed && version != NULL && *version != '\0')
                          bz_entry_set_installed_version (entry, version);

                        bz_entry_set_installed (entry, installed);

                        if (group != NULL)
                          {
                            gboolean found    = FALSE;
                            guint    position = 0;

                            found = g_list_store_find (self->installed_apps, group, &position);
                            if (installed && !found)
                              g_list_store_insert_sorted (
                                  self->installed_apps, group,
                                  (GCompareDataFunc) cmp_group, NULL);
                            else if (!installed && found &&
                                     bz_entry_group_get_removable (group) == 0)
                              g_list_store_remove (self->installed_apps, position);
                          }

                        g_ptr_array_add (
                            diff_writes,
                            bz_entry_cache_manager_add (self->cache, entry));
                      }
                  }

                dex_await (dex_future_allv (
                               (DexFuture *const *) diff_writes->pdata,
                               diff_writes->len),
                           NULL);
              }
            g_clear_pointer (&self->installed_set, g_hash_table_unref);
            self->installed_set = g_steal_pointer (&installed_set);

            fiber_check_for_updates (self);
            finish_with_background_task_label (self);
          }
          break;
        default:
          g_assert_not_reached ();
        }

      dex_clear (&read_future);
      read_future = dex_channel_receive (self->flatpak_notifs);

      if (!dex_future_is_pending (reread_timeout))
        break;
    }

  if (build_futures->len > 0)
    {
      g_autoptr (DexFuture) future                   = NULL;
      g_autoptr (CacheWriteBackData) write_back_data = NULL;

      future = dex_future_allv (
          (DexFuture *const *) build_futures->pdata,
          build_futures->len);

      write_back_data                 = cache_write_back_data_new ();
      write_back_data->self           = bz_track_weak (self);
      write_back_data->notify_groups  = g_ptr_array_ref (build_notify_groups);
      write_back_data->update_filters = update_filters;

      future = dex_future_finally (
          future,
          (DexFutureCallback) cache_write_back_finally,
          cache_write_back_data_ref (write_back_data),
          cache_write_back_data_unref);
      dex_future_disown (g_steal_pointer (&future));
    }

  if (update_labels)
    {
      if (self->n_entries_incoming > 0)
        bz_state_info_set_background_task_label_take_printf (
            self->state, _ ("Loading %d apps…"), self->n_entries_incoming);
      else
        {
          fiber_check_for_updates (self);
          finish_with_background_task_label (self);
        }
    }

  if (self->n_entries_incoming == 0 &&
      self->flathub_remote_initialized)
    {
      bz_state_info_set_busy (self->state, FALSE);
      finish_with_background_task_label (self);
    }

  return g_steal_pointer (&read_future);
}

static DexFuture *
open_appstream_fiber (OpenAppstreamData *data)
{
  g_autoptr (BzApplication) self = NULL;
  char *id                       = data->id;

  bz_weak_get_or_return_reject (self, data->self);
  dex_await (dex_ref (self->ready_to_open_files), NULL);

  open_generic_id (self, id);
  return dex_future_new_true ();
}

static DexFuture *
open_flatpakref_fiber (OpenFlatpakrefData *data)
{
  g_autoptr (BzApplication) self = NULL;
  GFile *file                    = data->file;
  g_autoptr (GError) local_error = NULL;
  g_autoptr (DexFuture) future   = NULL;
  const GValue *value            = NULL;

  bz_weak_get_or_return_reject (self, data->self);
  dex_await (dex_ref (self->ready_to_open_files), NULL);

  future = bz_backend_load_local_package (BZ_BACKEND (self->flatpak), file, NULL);
  dex_await (dex_ref (future), NULL);

  value = dex_future_get_value (future, &local_error);
  if (value == NULL)
    {
      GtkWindow *window = get_or_create_window (self);

      bz_show_error_for_widget (
          GTK_WIDGET (window),
          _ ("Failed to open file"),
          local_error->message);

      return dex_future_new_for_error (g_steal_pointer (&local_error));
    }

  if (G_VALUE_HOLDS_OBJECT (value))
    {
      GtkWindow             *window        = NULL;
      BzEntry               *entry         = NULL;
      BzFlatpakRepo         *repo          = NULL;
      BzBundleInstallDialog *dialog        = NULL;
      BzFlatpakBundleResult *bundle_result = NULL;
      const char            *id            = NULL;

      window = get_or_create_window (self);

      if (adw_application_window_get_visible_dialog (ADW_APPLICATION_WINDOW (window)) != NULL)
        window = new_window (self);

      bundle_result = BZ_FLATPAK_BUNDLE_RESULT (g_value_get_object (value));
      entry         = BZ_ENTRY (bz_flatpak_bundle_result_get_entry (bundle_result));
      repo          = bz_flatpak_bundle_result_get_runtime_repo (bundle_result);

      id = bz_entry_get_id (entry);

      if (id != NULL)
        {
          BzEntryGroup *group = NULL;

          group = g_hash_table_lookup (self->ids_to_groups, id);
          if (group != NULL && bz_entry_group_get_removable (group) > 0)
            bz_entry_set_installed (entry, TRUE);
        }

      dialog = g_object_new (
          BZ_TYPE_BUNDLE_INSTALL_DIALOG,
          "state", self->state,
          "entry", entry,
          "runtime-repo", repo,
          NULL);
      adw_dialog_present (ADW_DIALOG (dialog), GTK_WIDGET (window));
    }

  return dex_future_new_true ();
}

static DexFuture *
open_metainfo_fiber (OpenMetainfoData *data)
{
  g_autoptr (BzApplication) self    = NULL;
  GFile *file                       = data->file;
  g_autoptr (GError) local_error    = NULL;
  g_autoptr (DexFuture) pick_future = NULL;
  const GValue         *value       = NULL;
  BzMetainfoPickResult *result      = NULL;
  g_autoptr (BzEntry) entry         = NULL;
  GtkWindow *window                 = NULL;

  bz_weak_get_or_return_reject (self, data->self);
  dex_await (dex_ref (self->ready_to_open_files), NULL);

  window = get_or_create_window (self);
  if (adw_application_window_get_visible_dialog (ADW_APPLICATION_WINDOW (window)) != NULL)
    window = new_window (self);

  pick_future = bz_metainfo_preview_open_file (file, window);
  dex_await (dex_ref (pick_future), NULL);

  value = dex_future_get_value (pick_future, &local_error);
  if (value == NULL)
    {
      bz_show_error_for_widget (
          GTK_WIDGET (window),
          _ ("Failed to open file"),
          local_error->message);

      return dex_future_new_for_error (g_steal_pointer (&local_error));
    }

  result = g_value_get_boxed (value);

  entry = bz_appstream_parser_entry_from_metainfo (
      result->metainfo_file, result->icon_file, &local_error);
  if (entry == NULL)
    {
      bz_show_error_for_widget (
          GTK_WIDGET (window),
          _ ("Failed to load metainfo"),
          local_error->message);

      return dex_future_new_for_error (g_steal_pointer (&local_error));
    }

  g_object_set (entry, "id", "preview", NULL);
  bz_window_show_entry (BZ_WINDOW (window), entry);

  return dex_future_new_true ();
}

static DexFuture *
init_fiber_finally (DexFuture *future,
                    GWeakRef  *wr)
{
  g_autoptr (BzApplication) self      = NULL;
  g_autoptr (GError) local_error      = NULL;
  const GValue *value                 = NULL;
  gint64        last_refresh          = 0;
  gint64        now                   = 0;
  gint64        seconds_since_refresh = 0;

  bz_weak_get_or_return_reject (self, wr);

  value = dex_future_get_value (future, &local_error);
  if (value != NULL)
    {
      self->flatpak_notifs = bz_backend_create_notification_channel (
          BZ_BACKEND (self->flatpak));
      self->notif_watch = dex_future_then_loop (
          dex_channel_receive (self->flatpak_notifs),
          (DexFutureCallback) watch_backend_notifs_then_loop_cb,
          bz_track_weak (self),
          bz_weak_release);

      last_refresh          = g_settings_get_int64 (self->settings, "last-refresh-time");
      now                   = g_get_real_time () / G_USEC_PER_SEC;
      seconds_since_refresh = now - last_refresh;

      if ((!bz_state_info_get_metered_connection (self->state) || !self->had_cache_on_init) &&
          seconds_since_refresh >= MIN_STARTUP_REFRESH_INTERVAL_SECONDS)
        {
          g_autoptr (DexFuture) sync_future = NULL;

          sync_future = make_sync_future (self);
          sync_future = dex_future_finally (
              sync_future,
              (DexFutureCallback) init_sync_finally,
              bz_track_weak (self),
              bz_weak_release);
          self->sync = g_steal_pointer (&sync_future);
        }
      else
        {
          bz_state_info_set_allow_manual_sync (self->state, TRUE);
          bz_state_info_set_busy (self->state, FALSE);
          bz_state_info_set_syncing (self->state, FALSE);
          dex_promise_resolve_boolean (self->ready_to_open_files, TRUE);

          // Only check for updates if we skipped a full sync.
          dex_future_disown (dex_scheduler_spawn (
              dex_scheduler_get_default (),
              bz_get_dex_stack_size (),
              (DexFiberFunc) check_for_updates_fiber,
              bz_track_weak (self),
              bz_weak_release));
        }

      self->periodic_timeout_source = g_timeout_add_seconds (
          /* Check every day */
          60 * 60 * 24, (GSourceFunc) periodic_timeout_cb, self);

      bz_malcontent_service_start (self->malcontent);

      g_object_bind_property (
          bz_state_info_get_auth_state (self->state), "authenticated",
          g_action_map_lookup_action (G_ACTION_MAP (self), "flathub-login"), "enabled",
          G_BINDING_SYNC_CREATE | G_BINDING_INVERT_BOOLEAN);
    }
  else
    {
      GtkWindow *window = NULL;

      bz_state_info_set_online (self->state, FALSE);
      bz_state_info_set_busy (self->state, FALSE);
      window = gtk_application_get_active_window (GTK_APPLICATION (self));
      if (window != NULL)
        {
          g_autofree char *error_string = NULL;

          error_string = g_strdup_printf (
              "Could not initialize: %s",
              local_error->message);
          bz_show_error_for_widget (GTK_WIDGET (window), _ ("An initialization error occurred"), error_string);
        }
    }

  return dex_future_new_true ();
}

static DexFuture *
init_sync_finally (DexFuture *future,
                   GWeakRef  *wr)
{
  g_autoptr (BzApplication) self = NULL;
  g_autoptr (GError) local_error = NULL;

  bz_weak_get_or_return_reject (self, wr);

  /* Do nothing */

  return dex_future_new_true ();
}

static DexFuture *
backend_sync_finally (DexFuture *future,
                      GWeakRef  *wr)
{
  g_autoptr (BzApplication) self = NULL;

  bz_weak_get_or_return_reject (self, wr);

  if (dex_future_is_resolved (future))
    {
      g_autoptr (DexFuture) enum_future = NULL;

      enum_future = dex_scheduler_spawn (
          dex_scheduler_get_default (),
          bz_get_dex_stack_size (),
          (DexFiberFunc) enumerate_disk_entries_fiber,
          bz_track_weak (self),
          bz_weak_release);

      enum_future = dex_future_finally (
          enum_future,
          (DexFutureCallback) backend_sync_save_groups_finally,
          bz_track_weak (self),
          bz_weak_release);

      return g_steal_pointer (&enum_future);
    }
  else
    return dex_ref (future);
}

static DexFuture *
backend_sync_save_groups_finally (DexFuture *future,
                                  GWeakRef  *wr)
{
  g_autoptr (BzApplication) self = NULL;

  bz_weak_get_or_return_reject (self, wr);

  return dex_scheduler_spawn (
      dex_scheduler_get_default (),
      bz_get_dex_stack_size (),
      (DexFiberFunc) cache_groups_fiber,
      bz_track_weak (self),
      bz_weak_release);
}

static DexFuture *
flathub_update_finally (DexFuture *future,
                        GWeakRef  *wr)
{
  g_autoptr (BzApplication) self = NULL;

  bz_weak_get_or_return_reject (self, wr);

  if (dex_future_is_resolved (future))
    {
      g_clear_object (&self->flathub);
      g_assert (self->tmp_flathub != NULL);
      self->flathub = g_steal_pointer (&self->tmp_flathub);
      bz_flathub_state_set_map_factory (self->flathub, self->application_factory);
      bz_state_info_set_flathub (self->state, self->flathub);

      return dex_scheduler_spawn (
          dex_scheduler_get_default (),
          bz_get_dex_stack_size (),
          (DexFiberFunc) cache_flathub_fiber,
          bz_track_weak (self), bz_weak_release);
    }
  else
    {
      g_clear_object (&self->tmp_flathub);
      return dex_ref (future);
    }
}

static DexFuture *
cache_write_back_finally (DexFuture          *future,
                          CacheWriteBackData *data)
{
  g_autoptr (BzApplication) self = NULL;
  GPtrArray *notify_groups       = data->notify_groups;
  gboolean   update_filters      = data->update_filters;

  bz_weak_get_or_return_reject (self, data->self);

  for (guint i = 0; i < notify_groups->len; i++)
    {
      BzEntryGroup *group = NULL;

      group = g_ptr_array_index (notify_groups, i);
      g_object_notify (G_OBJECT (group), "ui-entry");
    }

  if (update_filters)
    {
      gtk_filter_changed (GTK_FILTER (self->group_filter), GTK_FILTER_CHANGE_LESS_STRICT);
      gtk_filter_changed (GTK_FILTER (self->appid_filter), GTK_FILTER_CHANGE_LESS_STRICT);
    }

  return dex_future_new_true ();
}

static DexFuture *
sync_finally (DexFuture *future,
              GWeakRef  *wr)
{
  g_autoptr (BzApplication) self = NULL;

  bz_weak_get_or_return_reject (self, wr);

  bz_state_info_set_online (self->state, dex_future_is_resolved (future));
  bz_state_info_set_allow_manual_sync (self->state, TRUE);
  bz_state_info_set_busy (self->state, FALSE);
  bz_state_info_set_syncing (self->state, FALSE);
  finish_with_background_task_label (self);

  if (dex_future_is_resolved (future))
    g_settings_set_int64 (
        self->settings, "last-refresh-time",
        g_get_real_time () / G_USEC_PER_SEC);

  dex_promise_resolve_boolean (self->ready_to_open_files, TRUE);

  return dex_future_new_true ();
}

static DexFuture *
watch_backend_notifs_then_loop_cb (DexFuture *future,
                                   GWeakRef  *wr)
{
  g_autoptr (BzApplication) self        = NULL;
  g_autoptr (GError) local_error        = NULL;
  const GValue          *value          = NULL;
  BzBackendNotification *notif          = NULL;
  g_autoptr (RespondToFlatpakData) data = NULL;
  g_autoptr (DexFuture) ret_future      = NULL;

  bz_weak_get_or_return_reject (self, wr);

  value = dex_future_get_value (future, NULL);
  g_assert (value != NULL);
  notif = g_value_get_object (value);

  data        = respond_to_flatpak_data_new ();
  data->self  = bz_track_weak (self);
  data->notif = g_object_ref (notif);

  ret_future = dex_scheduler_spawn (
      dex_scheduler_get_default (),
      bz_get_dex_stack_size (),
      (DexFiberFunc) respond_to_flatpak_fiber,
      respond_to_flatpak_data_ref (data),
      respond_to_flatpak_data_unref);
  return g_steal_pointer (&ret_future);
}

static BzEntryGroup *
ensure_group_and_add (BzApplication *self,
                      const char    *id,
                      BzEntry       *entry,
                      BzEntry       *eol_runtime,
                      gboolean       ignore_eol,
                      gboolean       installed)
{
  BzEntryGroup *group = NULL;

  group = g_hash_table_lookup (self->ids_to_groups, id);
  if (group != NULL)
    bz_entry_group_add (group, entry, eol_runtime, ignore_eol);
  else
    {
      g_autoptr (BzEntryGroup) new_group = NULL;

      g_debug ("Creating new application group for id %s", id);
      new_group = bz_entry_group_new (self->entry_factory);
      bz_entry_group_add (new_group, entry, eol_runtime, ignore_eol);

      g_list_store_append (self->groups, new_group);
      g_hash_table_replace (self->ids_to_groups, g_strdup (id), g_object_ref (new_group));

      group = new_group;
    }

  if (installed && !g_list_store_find (self->installed_apps, group, NULL))
    g_list_store_insert_sorted (
        self->installed_apps, group,
        (GCompareDataFunc) cmp_group, NULL);

  return group;
}

static void
fiber_replace_entry (BzApplication *self,
                     BzEntry       *entry)
{
  const char *id                 = NULL;
  const char *unique_id          = NULL;
  const char *unique_id_checksum = NULL;
  gboolean    user               = FALSE;
  gboolean    installed          = FALSE;
  const char *flatpak_id         = NULL;
  const char *version            = NULL;
  GHashTable *name_to_addons     = NULL;

  id                 = bz_entry_get_id (entry);
  unique_id          = bz_entry_get_unique_id (entry);
  unique_id_checksum = bz_entry_get_unique_id_checksum (entry);
  if (id == NULL ||
      unique_id == NULL ||
      unique_id_checksum == NULL)
    return;

  user           = bz_flatpak_entry_is_user (BZ_FLATPAK_ENTRY (entry));
  name_to_addons = user ? self->usr_name_to_addons : self->sys_name_to_addons;

  installed = g_hash_table_contains (self->installed_set, unique_id);
  bz_entry_set_installed (entry, installed);

  version = g_hash_table_lookup (self->installed_set, unique_id);
  if (version != NULL && *version != '\0')
    bz_entry_set_installed_version (entry, version);

  flatpak_id = bz_flatpak_entry_get_flatpak_id (BZ_FLATPAK_ENTRY (entry));
  if (flatpak_id != NULL)
    {
      GPtrArray *addons = NULL;

      addons = g_hash_table_lookup (name_to_addons, flatpak_id);
      if (addons != NULL)
        {
          g_debug ("Appending %d addons to %s", addons->len, unique_id);
          for (guint i = 0; i < addons->len; i++)
            {
              const char *addon_id = NULL;

              addon_id = g_ptr_array_index (addons, i);
              bz_entry_append_addon (entry, addon_id);
            }
          g_hash_table_remove (name_to_addons, flatpak_id);
          addons = NULL;
        }
    }

  if (bz_entry_is_of_kinds (entry, BZ_ENTRY_KIND_APPLICATION))
    {
      gboolean    ignore_eol               = FALSE;
      const char *runtime_name             = NULL;
      g_autoptr (BzEntry) eol_runtime      = NULL;
      BzEntryGroup *group                  = NULL;
      GHashTable   *ref_to_addon_group_ids = NULL;
      GPtrArray    *pending                = NULL;

      if (self->ignore_eol_set != NULL)
        ignore_eol = g_hash_table_contains (self->ignore_eol_set, id);

      runtime_name = bz_flatpak_entry_get_application_runtime (BZ_FLATPAK_ENTRY (entry));
      if (!ignore_eol &&
          runtime_name != NULL)
        {
          char *runtime_checksum = NULL;

          runtime_checksum = g_hash_table_lookup (self->eol_runtimes, runtime_name);
          if (runtime_checksum != NULL)
            eol_runtime = dex_await_object (
                bz_entry_cache_manager_get_by_checksum (
                    self->cache,
                    runtime_checksum),
                NULL);
        }

      group = ensure_group_and_add (self, id, entry, eol_runtime, ignore_eol, installed);

      ref_to_addon_group_ids =
          user
              ? self->usr_ref_to_addon_group_ids
              : self->sys_ref_to_addon_group_ids;
      pending = g_hash_table_lookup (ref_to_addon_group_ids, id);
      if (pending != NULL)
        {
          for (guint i = 0; i < pending->len; i++)
            bz_entry_group_append_addon_group_id (group, g_ptr_array_index (pending, i));
          g_hash_table_remove (ref_to_addon_group_ids, id);
        }
    }

  if (flatpak_id != NULL &&
      bz_entry_is_of_kinds (entry, BZ_ENTRY_KIND_RUNTIME) &&
      g_str_has_prefix (flatpak_id, "runtime/"))
    {
      const char *stripped = NULL;
      const char *eol      = NULL;

      stripped = flatpak_id + strlen ("runtime/");

      eol = bz_entry_get_eol (entry);
      if (eol != NULL)
        g_hash_table_replace (
            self->eol_runtimes,
            g_strdup (stripped),
            g_strdup (unique_id_checksum));
      else
        g_hash_table_remove (self->eol_runtimes, stripped);
    }

  if (bz_entry_is_of_kinds (entry, BZ_ENTRY_KIND_ADDON) &&
      strstr (id, ".Debug") == NULL &&
      strstr (id, ".Locale") == NULL)
    {
      const char *extension_of_what = NULL;

      extension_of_what = bz_flatpak_entry_get_addon_extension_of_ref (
          BZ_FLATPAK_ENTRY (entry));

      if (extension_of_what != NULL &&
          g_str_has_prefix (extension_of_what, "app/"))
        {
          g_auto (GStrv) parts    = NULL;
          BzEntryGroup *app_group = NULL;

          ensure_group_and_add (self, id, entry, NULL, FALSE, installed);

          parts = g_strsplit (extension_of_what, "/", -1);
          if (parts != NULL && parts[1] != NULL)
            {
              app_group = g_hash_table_lookup (self->ids_to_groups, parts[1]);
              if (app_group != NULL)
                bz_entry_group_append_addon_group_id (app_group, id);
              else
                {
                  GHashTable *ref_to_addon_group_ids =
                      user
                          ? self->usr_ref_to_addon_group_ids
                          : self->sys_ref_to_addon_group_ids;
                  GPtrArray *pending = NULL;

                  pending = g_hash_table_lookup (ref_to_addon_group_ids, parts[1]);
                  if (pending == NULL)
                    {
                      pending = g_ptr_array_new_with_free_func (g_free);
                      g_hash_table_replace (ref_to_addon_group_ids,
                                            g_strdup (parts[1]), pending);
                    }
                  g_ptr_array_add (pending, g_strdup (id));
                }
            }
        }

      if (extension_of_what != NULL)
        {
          GPtrArray *addons = NULL;

          /* BzFlatpakInstance ensures addons come before applications */
          addons = g_hash_table_lookup (name_to_addons, extension_of_what);
          if (addons == NULL)
            {
              addons = g_ptr_array_new_with_free_func (g_free);
              g_hash_table_replace (
                  name_to_addons,
                  g_strdup (extension_of_what), addons);
            }
          g_ptr_array_add (addons, g_strdup (unique_id));
        }
      else
        g_warning ("Entry with unique id %s is an addon but "
                   "does not seem to extend anything",
                   unique_id);
    }
}

static void
fiber_check_for_updates (BzApplication *self)
{
  g_autoptr (GError) local_error   = NULL;
  g_autoptr (GPtrArray) update_ids = NULL;
  GtkWindow *window                = NULL;

  g_debug ("Checking for updates...");
  bz_state_info_set_background_task_label (self->state, _ ("Checking for updates…"));
  bz_state_info_set_checking_for_updates (self->state, TRUE);

  update_ids = dex_await_boxed (
      bz_backend_retrieve_update_ids (BZ_BACKEND (self->flatpak), NULL),
      &local_error);
  window = gtk_application_get_active_window (GTK_APPLICATION (self));
  if (update_ids != NULL &&
      update_ids->len > 0)
    {
      g_autoptr (GPtrArray) futures = NULL;
      g_autoptr (GListStore) store  = NULL;

      futures = g_ptr_array_new_with_free_func (dex_unref);
      for (guint i = 0; i < update_ids->len; i++)
        {
          const char *unique_id = NULL;

          unique_id = g_ptr_array_index (update_ids, i);
          g_ptr_array_add (futures, bz_entry_cache_manager_get (self->cache, unique_id));
        }

      dex_await (
          dex_future_allv ((DexFuture *const *) futures->pdata, futures->len),
          NULL);

      store = g_list_store_new (BZ_TYPE_ENTRY);
      for (guint i = 0; i < futures->len; i++)
        {
          DexFuture    *future = NULL;
          const GValue *value  = NULL;

          future = g_ptr_array_index (futures, i);
          value  = dex_future_get_value (future, &local_error);

          if (value != NULL)
            g_list_store_append (store, g_value_get_object (value));
          else
            {
              const char *unique_id = NULL;

              unique_id = g_ptr_array_index (update_ids, i);
              g_warning ("%s could not be resolved for the update list and thus will not be included: %s",
                         unique_id, local_error->message);
              g_clear_pointer (&local_error, g_error_free);
            }
        }

      if (g_list_model_get_n_items (G_LIST_MODEL (store)) > 0)
        bz_state_info_set_available_updates (self->state, G_LIST_MODEL (store));
    }
  else if (local_error != NULL)
    {
      g_warning ("Failed to check for updates: %s", local_error->message);

      if (window != NULL)
        bz_show_error_for_widget (GTK_WIDGET (window), _ ("Failed to check for updates"), local_error->message);
    }

  bz_state_info_set_checking_for_updates (self->state, FALSE);
}

static GFile *
fiber_dup_cache_file (const char *name,
                      char      **path_out,
                      GError    **error)
{
  gboolean         result           = FALSE;
  g_autofree char *module_dir       = NULL;
  g_autoptr (GFile) module_dir_file = NULL;
  g_autofree char *path             = NULL;
  g_autoptr (GFile) file            = NULL;

  module_dir      = bz_dup_module_dir ();
  module_dir_file = g_file_new_for_path (module_dir);
  result          = dex_await (
      dex_file_make_directory_with_parents (module_dir_file),
      error);
  if (!result)
    return NULL;

  path = g_build_filename (module_dir, name, NULL);
  file = g_file_new_for_path (path);

  if (path_out != NULL)
    *path_out = g_steal_pointer (&path);
  return g_steal_pointer (&file);
}

static gboolean
periodic_timeout_cb (BzApplication *self)
{
  gboolean have_connection    = FALSE;
  gboolean metered_connection = FALSE;

  if (self->sync != NULL &&
      dex_future_is_pending (self->sync))
    /* If for some reason the last update check is still happening, let it
       finish */
    goto done;

  dex_clear (&self->sync);

  have_connection    = bz_state_info_get_have_connection (self->state);
  metered_connection = bz_state_info_get_metered_connection (self->state);
  if (have_connection && !metered_connection)
    /* Do not do periodic sync on metered connections. The user will have to
       manually refresh instead. */
    self->sync = make_sync_future (self);
  else
    bz_state_info_set_recently_synced (self->state, FALSE);

done:
  return G_SOURCE_CONTINUE;
}

static gboolean
scheduled_timeout_cb (GWeakRef *wr)
{
  g_autoptr (BzApplication) self = NULL;
  gboolean have_connection       = FALSE;

  /* Use weak ref here since the source tag of this callback won't be tracked by
     the main application obj */
  self = g_weak_ref_get (wr);
  if (self == NULL)
    goto done;

  dex_clear (&self->sync);
  have_connection = bz_state_info_get_have_connection (self->state);
  if (have_connection)
    self->sync = make_sync_future (self);

done:
  return G_SOURCE_REMOVE;
}

static void
network_status_changed (BzApplication   *self,
                        GParamSpec      *pspec,
                        GNetworkMonitor *network)
{
  gboolean             was_connected   = FALSE;
  gboolean             was_metered     = FALSE;
  GNetworkConnectivity connectivity    = 0;
  gboolean             have_connection = FALSE;
  gboolean             is_metered      = FALSE;

  was_connected = bz_state_info_get_have_connection (self->state);
  was_metered   = bz_state_info_get_metered_connection (self->state);

  connectivity    = g_network_monitor_get_connectivity (network);
  have_connection = connectivity == G_NETWORK_CONNECTIVITY_FULL;
  is_metered      = g_network_monitor_get_network_metered (network);

  if (!bz_state_info_get_busy (self->state) &&
      ((!was_connected &&
        have_connection &&
        !is_metered) ||
       (was_metered &&
        !is_metered)))
    /* Wait a bit to prevent flakiness */
    g_timeout_add_full (
        G_PRIORITY_DEFAULT,
        500, (GSourceFunc) scheduled_timeout_cb,
        bz_track_weak (self), bz_weak_release);

  bz_state_info_set_have_connection (self->state, have_connection);
  bz_state_info_set_metered_connection (self->state, is_metered);
}

static void
disable_blocklists_changed (BzApplication *self,
                            GParamSpec    *pspec,
                            BzStateInfo   *state)
{
  gtk_filter_changed (GTK_FILTER (self->group_filter), GTK_FILTER_CHANGE_DIFFERENT);
  gtk_filter_changed (GTK_FILTER (self->appid_filter), GTK_FILTER_CHANGE_DIFFERENT);
}

static void
show_hide_app_setting_changed (BzApplication *self,
                               const char    *key,
                               GSettings     *settings)
{
  g_object_freeze_notify (G_OBJECT (self->state));

  bz_state_info_set_hide_eol (self->state, g_settings_get_boolean (self->settings, "hide-eol"));
  bz_state_info_set_show_only_foss (self->state, g_settings_get_boolean (self->settings, "show-only-foss"));
  bz_state_info_set_show_only_flathub (self->state, g_settings_get_boolean (self->settings, "show-only-flathub"));
  bz_state_info_set_show_only_verified (self->state, g_settings_get_boolean (self->settings, "show-only-verified"));

  gtk_filter_changed (GTK_FILTER (self->group_filter), GTK_FILTER_CHANGE_DIFFERENT);
  gtk_filter_changed (GTK_FILTER (self->appid_filter), GTK_FILTER_CHANGE_DIFFERENT);

  g_object_thaw_notify (G_OBJECT (self->state));
}

static gboolean
window_close_request (BzApplication *self,
                      GtkWidget     *window)
{
  int      width             = 0;
  int      height            = 0;
  GList   *remaining_windows = NULL;
  gboolean reap_dl_workers   = TRUE;

  width  = gtk_widget_get_width (window);
  height = gtk_widget_get_height (window);

  g_settings_set (self->settings, "window-dimensions",
                  "(ii)", width, height);

  remaining_windows = gtk_application_get_windows (
      GTK_APPLICATION (self));
  for (GList *l = remaining_windows; l != NULL; l = l->next)
    {
      if (l->data != window)
        {
          reap_dl_workers = FALSE;
          break;
        }
    }
  if (reap_dl_workers)
    /* If no windows are left, kill the dl-worker subprocesses to minimize idle
       memory usage */
    bz_reap_default_download_workers ();

  /* Do not stop other handlers from being invoked for the signal */
  return FALSE;
}

static void
blocklists_changed (BzApplication *self,
                    guint          position,
                    guint          removed,
                    guint          added,
                    GListModel    *model)
{
  g_autoptr (GError) local_error = NULL;

  if (removed > 0)
    g_ptr_array_remove_range (self->blocklist_regexes, position, removed);

  for (guint i = 0; i < added; i++)
    {
      g_autoptr (BzRootBlocklist) root  = NULL;
      g_autoptr (GPtrArray) regex_datas = NULL;
      GListModel *blocklists            = NULL;

      root        = g_list_model_get_item (model, position + i);
      regex_datas = g_ptr_array_new_with_free_func (blocklist_regex_data_unref);

      blocklists = bz_root_blocklist_get_blocklists (root);
      if (blocklists != NULL)
        {
          guint n_blocklists = 0;

          n_blocklists = g_list_model_get_n_items (blocklists);
          for (guint j = 0; j < n_blocklists; j++)
            {
              g_autoptr (BzBlocklist) blocklist   = NULL;
              GListModel *conditions              = NULL;
              GListModel *allow                   = NULL;
              GListModel *allow_regex             = NULL;
              GListModel *block                   = NULL;
              GListModel *block_regex             = NULL;
              g_autoptr (BlocklistRegexData) data = NULL;

              blocklist   = g_list_model_get_item (blocklists, j);
              allow       = bz_blocklist_get_allow (blocklist);
              allow_regex = bz_blocklist_get_allow_regex (blocklist);
              block       = bz_blocklist_get_block (blocklist);
              block_regex = bz_blocklist_get_block_regex (blocklist);

              if (allow == NULL &&
                  allow_regex == NULL &&
                  block == NULL &&
                  block_regex == NULL)
                {
                  g_warning ("Blocklist file has an empty blocklist, ignoring");
                  continue;
                }

              conditions = bz_blocklist_get_conditions (blocklist);
              if (conditions != NULL)
                {
                  guint    n_conditions = 0;
                  gboolean ignore       = FALSE;

                  n_conditions = g_list_model_get_n_items (conditions);
                  for (guint k = 0; k < n_conditions; k++)
                    {
                      gboolean condition_result                        = FALSE;
                      g_autoptr (BzBlocklistCondition) condition       = NULL;
                      BzBlocklistConditionMatchEnvvar    *match_envvar = NULL;
                      BzBlocklistConditionMatchLocale    *match_locale = NULL;
                      BzBlocklistConditionPostProcessKind postprocess  = BZ_BLOCKLIST_CONDITION_POST_PROCESS_KIND_IDENTITY;

                      condition    = g_list_model_get_item (conditions, k);
                      match_envvar = bz_blocklist_condition_get_match_envvar (condition);
                      match_locale = bz_blocklist_condition_get_match_locale (condition);
                      postprocess  = bz_blocklist_condition_get_post_process (condition);

                      if (match_envvar == NULL &&
                          match_locale == NULL)
                        {
                          g_warning ("Blocklist file has an empty condition");
                          continue;
                        }

                      if (!condition_result &&
                          match_envvar != NULL)
                        {
                          const char *var   = NULL;
                          const char *regex = NULL;

                          var   = bz_blocklist_condition_match_envvar_get_var (match_envvar);
                          regex = bz_blocklist_condition_match_envvar_get_regex (match_envvar);

                          if (var != NULL &&
                              regex != NULL)
                            {
                              g_autoptr (GRegex) compiled = NULL;
                              const char *value           = NULL;

                              compiled = g_regex_new (
                                  regex,
                                  G_REGEX_ANCHORED,
                                  G_REGEX_MATCH_ANCHORED,
                                  &local_error);
                              if (compiled == NULL)
                                {
                                  g_warning ("Blocklist condition contains invalid regex: %s",
                                             local_error->message);
                                  g_clear_error (&local_error);
                                  continue;
                                }

                              value = g_getenv (var);
                              if (value != NULL &&
                                  g_regex_match (
                                      compiled, value,
                                      G_REGEX_MATCH_ANCHORED, NULL))
                                condition_result = TRUE;
                              if (postprocess == BZ_BLOCKLIST_CONDITION_POST_PROCESS_KIND_INVERT)
                                condition_result = !condition_result;
                            }
                          else
                            g_warning ("Blocklist file has a envvar condition "
                                       "missing a var and/or a regex pattern");
                        }

                      if (!condition_result &&
                          match_locale != NULL)
                        {
                          const char *regex = NULL;

                          regex = bz_blocklist_condition_match_locale_get_regex (match_locale);

                          if (regex != NULL)
                            {
                              g_autoptr (GRegex) compiled = NULL;
                              const char *const *locales  = NULL;

                              compiled = g_regex_new (
                                  regex,
                                  G_REGEX_ANCHORED,
                                  G_REGEX_MATCH_ANCHORED,
                                  &local_error);
                              if (compiled == NULL)
                                {
                                  g_warning ("Blocklist condition contains invalid regex: %s",
                                             local_error->message);
                                  g_clear_error (&local_error);
                                  continue;
                                }

                              locales = g_get_language_names ();
                              for (guint l = 0; locales[l] != NULL; l++)
                                {
                                  if (g_regex_match (
                                          compiled, locales[i],
                                          G_REGEX_MATCH_ANCHORED, NULL))
                                    condition_result = TRUE;
                                  if (condition_result)
                                    break;
                                }
                              if (postprocess == BZ_BLOCKLIST_CONDITION_POST_PROCESS_KIND_INVERT)
                                condition_result = !condition_result;
                            }
                          else
                            g_warning ("Blocklist file has a match-locale "
                                       "condition missing a regex pattern");
                        }

                      if (!condition_result)
                        {
                          ignore = TRUE;
                          break;
                        }
                    }

                  if (ignore)
                    continue;
                }

              data           = blocklist_regex_data_new ();
              data->priority = bz_blocklist_get_priority (blocklist);

#define BUILD_REGEX(_name, _builder)                                                             \
  if (_name != NULL)                                                                             \
    {                                                                                            \
      guint _n_strings = 0;                                                                      \
                                                                                                 \
      _n_strings = g_list_model_get_n_items (_name);                                             \
      for (guint _i = 0; _i < _n_strings; _i++)                                                  \
        {                                                                                        \
          g_autoptr (GtkStringObject) _object = NULL;                                            \
          const char *_string                 = NULL;                                            \
          g_autoptr (GRegex) _regex           = NULL;                                            \
                                                                                                 \
          _object = g_list_model_get_item (_name, _i);                                           \
          _string = gtk_string_object_get_string (_object);                                      \
          _regex  = g_regex_new (_string, G_REGEX_DEFAULT, G_REGEX_MATCH_DEFAULT, &local_error); \
                                                                                                 \
          if (_regex != NULL)                                                                    \
            g_strv_builder_add (_builder, _string);                                              \
          else                                                                                   \
            {                                                                                    \
              g_warning ("Blocklist file has an invalid "                                        \
                         "regular expression '%s': %s",                                          \
                         _string, local_error->message);                                         \
              g_clear_error (&local_error);                                                      \
            }                                                                                    \
        }                                                                                        \
    }

#define BUILD_REGEX_ESCAPED(_name, _builder)                                   \
  if (_name != NULL)                                                           \
    {                                                                          \
      guint _n_strings = 0;                                                    \
                                                                               \
      _n_strings = g_list_model_get_n_items (_name);                           \
      for (guint _i = 0; _i < _n_strings; _i++)                                \
        {                                                                      \
          g_autoptr (GtkStringObject) _object = NULL;                          \
          const char *_string                 = NULL;                          \
                                                                               \
          _object = g_list_model_get_item (_name, _i);                         \
          _string = gtk_string_object_get_string (_object);                    \
                                                                               \
          g_strv_builder_take (_builder, g_regex_escape_string (_string, -1)); \
        }                                                                      \
    }

#define GATHER(name)                                                    \
  if (name != NULL ||                                                   \
      name##_regex != NULL)                                             \
    {                                                                   \
      g_autoptr (GStrvBuilder) _builder = NULL;                         \
      g_auto (GStrv) _patterns          = NULL;                         \
                                                                        \
      _builder = g_strv_builder_new ();                                 \
                                                                        \
      BUILD_REGEX_ESCAPED (name, _builder)                              \
      BUILD_REGEX (name##_regex, _builder)                              \
                                                                        \
      _patterns = g_strv_builder_end (_builder);                        \
      if (_patterns != NULL)                                            \
        {                                                               \
          g_autofree char *_joined       = NULL;                        \
          g_autofree char *_regex_string = NULL;                        \
                                                                        \
          _joined       = g_strjoinv ("|", _patterns);                  \
          _regex_string = g_strdup_printf ("^(%s)$", _joined);          \
          data->name    = g_regex_new (_regex_string, G_REGEX_OPTIMIZE, \
                                       G_REGEX_MATCH_DEFAULT, NULL);    \
        }                                                               \
    }

              GATHER (allow);
              GATHER (block);

#undef GATHER
#undef BUILD_REGEX_ESCAPED
#undef BUILD_REGEX

              if (data->allow != NULL || data->block != NULL)
                g_ptr_array_add (regex_datas, g_steal_pointer (&data));
            }
        }

      g_ptr_array_insert (self->blocklist_regexes,
                          position + i,
                          g_steal_pointer (&regex_datas));
    }

  gtk_filter_changed (GTK_FILTER (self->group_filter), GTK_FILTER_CHANGE_DIFFERENT);
  gtk_filter_changed (GTK_FILTER (self->appid_filter), GTK_FILTER_CHANGE_DIFFERENT);
}

static void
txt_blocklists_changed (BzApplication *self,
                        guint          position,
                        guint          removed,
                        guint          added,
                        GListModel    *model)
{
  if (removed > 0)
    g_ptr_array_remove_range (self->txt_blocked_id_sets, position, removed);

  for (guint i = 0; i < added; i++)
    {
      g_autoptr (BzHashTableObject) obj = NULL;
      GHashTable *set                   = NULL;

      obj = g_list_model_get_item (model, position + i);
      set = bz_hash_table_object_get_hash_table (obj);

      g_ptr_array_insert (self->txt_blocked_id_sets,
                          position + i,
                          g_hash_table_ref (set));
    }

  gtk_filter_changed (GTK_FILTER (self->group_filter), GTK_FILTER_CHANGE_DIFFERENT);
  gtk_filter_changed (GTK_FILTER (self->appid_filter), GTK_FILTER_CHANGE_DIFFERENT);
}

static void
init_service_struct (BzApplication *self,
                     GtkStringList *blocklists,
                     GtkStringList *txt_blocklists,
                     GtkStringList *curated_configs)
{
  g_autoptr (GError) local_error                       = NULL;
  g_autoptr (GBytes) internal_config_bytes             = NULL;
  g_autoptr (BzYamlParser) internal_config_parser      = NULL;
  g_autoptr (GHashTable) internal_config_parse_results = NULL;
  const char *app_id                                   = NULL;
#ifdef HARDCODED_MAIN_CONFIG
  g_autoptr (GFile) config_file   = NULL;
  g_autoptr (GBytes) config_bytes = NULL;
#endif
  GtkCustomFilter *filter  = NULL;
  GNetworkMonitor *network = NULL;

  g_type_ensure (BZ_TYPE_INTERNAL_CONFIG);
  internal_config_bytes = g_resources_lookup_data (
      "/io/github/kolunmi/Bazaar/internal-config.yaml",
      G_RESOURCE_LOOKUP_FLAGS_NONE,
      NULL);
  g_assert (internal_config_bytes != NULL);
  internal_config_parser = bz_yaml_parser_new_for_resource_schema (
      "/io/github/kolunmi/Bazaar/internal-config-schema.xml");
  g_assert (internal_config_parser != NULL);
  internal_config_parse_results = bz_parser_process_bytes (
      BZ_PARSER (internal_config_parser), internal_config_bytes, &local_error);
  if (internal_config_parse_results == NULL)
    g_critical ("FATAL: unable to parse internal config resource: %s",
                local_error->message);
  g_assert (internal_config_parse_results != NULL);
  self->internal_config = g_value_dup_object (g_hash_table_lookup (internal_config_parse_results, "/"));

  g_type_ensure (BZ_TYPE_MAIN_CONFIG);
#ifdef HARDCODED_MAIN_CONFIG
  config_file  = g_file_new_for_path (HARDCODED_MAIN_CONFIG);
  config_bytes = g_file_load_bytes (config_file, NULL, NULL, &local_error);
  if (config_bytes != NULL)
    {
      g_autoptr (BzYamlParser) parser      = NULL;
      g_autoptr (GHashTable) parse_results = NULL;

      parser = bz_yaml_parser_new_for_resource_schema (
          "/io/github/kolunmi/Bazaar/main-config-schema.xml");

      parse_results = bz_parser_process_bytes (
          BZ_PARSER (parser), config_bytes, &local_error);
      if (parse_results != NULL)
        {
          GListModel *override_eol_markings = NULL;

          self->config = g_value_dup_object (g_hash_table_lookup (parse_results, "/"));

          override_eol_markings = bz_main_config_get_override_eol_markings (self->config);
          if (override_eol_markings != NULL)
            {
              guint n_appids = 0;

              self->ignore_eol_set = g_hash_table_new_full (
                  g_str_hash, g_str_equal, g_free, g_free);

              n_appids = g_list_model_get_n_items (override_eol_markings);
              for (guint i = 0; i < n_appids; i++)
                {
                  g_autoptr (GtkStringObject) string = NULL;
                  const char *value                  = NULL;

                  string = g_list_model_get_item (override_eol_markings, i);
                  value  = gtk_string_object_get_string (string);
                  g_hash_table_replace (self->ignore_eol_set, g_strdup (value), NULL);
                }
            }
        }
      else
        {
          g_warning ("Could not load main config at %s: %s",
                     HARDCODED_MAIN_CONFIG, local_error->message);
          g_clear_error (&local_error);
        }
    }
  g_clear_error (&local_error);
#endif

  self->init_timer          = g_timer_new ();
  self->ready_to_open_files = dex_promise_new ();
  self->first_window_opened = dex_promise_new ();

  if (self->config != NULL &&
      bz_main_config_get_yaml_blocklist_paths (self->config) != NULL)
    {
      GListModel *paths   = NULL;
      guint       n_paths = 0;

      paths   = bz_main_config_get_yaml_blocklist_paths (self->config);
      n_paths = g_list_model_get_n_items (paths);
      for (guint i = 0; i < n_paths; i++)
        {
          g_autoptr (GtkStringObject) string = NULL;

          string = g_list_model_get_item (paths, i);
          gtk_string_list_append (blocklists, gtk_string_object_get_string (string));
        }
    }
  self->blocklists          = g_object_ref (blocklists);
  self->blocklists_to_files = gtk_map_list_model_new (
      NULL, (GtkMapListModelMapFunc) map_strings_to_files, NULL, NULL);
  gtk_map_list_model_set_model (
      self->blocklists_to_files,
      G_LIST_MODEL (self->blocklists));

  if (self->config != NULL &&
      bz_main_config_get_txt_blocklist_paths (self->config) != NULL)
    {
      GListModel *paths   = NULL;
      guint       n_paths = 0;

      paths   = bz_main_config_get_txt_blocklist_paths (self->config);
      n_paths = g_list_model_get_n_items (paths);
      for (guint i = 0; i < n_paths; i++)
        {
          g_autoptr (GtkStringObject) string = NULL;

          string = g_list_model_get_item (paths, i);
          gtk_string_list_append (txt_blocklists, gtk_string_object_get_string (string));
        }
    }
  self->txt_blocklists          = g_object_ref (txt_blocklists);
  self->txt_blocklists_to_files = gtk_map_list_model_new (
      NULL, (GtkMapListModelMapFunc) map_strings_to_files, NULL, NULL);
  gtk_map_list_model_set_model (
      self->txt_blocklists_to_files,
      G_LIST_MODEL (self->txt_blocklists));

  if (self->config != NULL &&
      bz_main_config_get_curated_config_paths (self->config) != NULL)
    {
      GListModel *paths   = NULL;
      guint       n_paths = 0;

      paths   = bz_main_config_get_curated_config_paths (self->config);
      n_paths = g_list_model_get_n_items (paths);
      for (guint i = 0; i < n_paths; i++)
        {
          g_autoptr (GtkStringObject) string = NULL;

          string = g_list_model_get_item (paths, i);
          gtk_string_list_append (curated_configs, gtk_string_object_get_string (string));
        }
    }

#ifdef DEVELOPMENT_BUILD
  if (g_list_model_get_n_items (G_LIST_MODEL (curated_configs)) == 0)
    {
      g_autofree char *dest_path = NULL;
      g_autoptr (GFile) src      = NULL;
      g_autoptr (GFile) dest     = NULL;

      dest_path = g_build_filename (g_get_user_data_dir (), "example.yaml", NULL);
      src       = g_file_new_for_path (DEVELOPMENT_EXAMPLE_YAML);
      dest      = g_file_new_for_path (dest_path);
      g_file_copy (src, dest, G_FILE_COPY_OVERWRITE, NULL, NULL, NULL, NULL);

      gtk_string_list_append (curated_configs, dest_path);
    }
#endif

  self->curated_configs          = g_object_ref (curated_configs);
  self->curated_configs_to_files = gtk_map_list_model_new (
      NULL, (GtkMapListModelMapFunc) map_strings_to_files, NULL, NULL);
  gtk_map_list_model_set_model (
      self->curated_configs_to_files,
      G_LIST_MODEL (self->curated_configs));

  self->search_biases         = gtk_flatten_list_model_new (NULL);
  self->search_biases_backing = g_list_store_new (G_TYPE_LIST_MODEL);
  {
    GListModel *main_config_search_biases     = NULL;
    GListModel *internal_config_search_biases = NULL;

    if (self->config != NULL)
      main_config_search_biases = bz_main_config_get_search_biases (self->config);

    internal_config_search_biases = bz_internal_config_get_search_biases (self->internal_config);

    /* Main config biases take precedence over the hardcoded ones */
    if (main_config_search_biases != NULL)
      g_list_store_append (self->search_biases_backing, main_config_search_biases);
    if (internal_config_search_biases != NULL)
      g_list_store_append (self->search_biases_backing, internal_config_search_biases);
  }
  gtk_flatten_list_model_set_model (
      self->search_biases,
      G_LIST_MODEL (self->search_biases_backing));

  g_type_ensure (BZ_TYPE_ROOT_BLOCKLIST);
  g_type_ensure (BZ_TYPE_BLOCKLIST);
  g_type_ensure (BZ_TYPE_BLOCKLIST_CONDITION);
  self->blocklist_parser = bz_yaml_parser_new_for_resource_schema (
      "/io/github/kolunmi/Bazaar/blocklist-schema.xml");

  self->txt_blocklist_parser = bz_newline_parser_new (
      TRUE, MAX_IDS_PER_BLOCKLIST);

  g_type_ensure (BZ_TYPE_ROOT_CURATED_CONFIG);
  g_type_ensure (BZ_TYPE_CURATED_ROW);
  g_type_ensure (BZ_TYPE_CURATED_SECTION);
  g_type_ensure (BZ_TYPE_CURATED_ARTICLE);
  self->curated_parser = bz_yaml_parser_new_for_resource_schema (
      "/io/github/kolunmi/Bazaar/curated-config-schema.xml");

  self->cache = bz_entry_cache_manager_new ();

  self->state = bz_state_info_new ();
  bz_state_info_set_busy (self->state, TRUE);
  bz_state_info_set_donation_prompt_dismissed (self->state, TRUE);
  bz_state_info_set_parental_age_rating (self->state, -1);

  state_info_set_icon_themes (self->state);

  {
    g_autoptr (GError) bus_error        = NULL;
    g_autoptr (GDBusConnection) sys_bus = NULL;

    sys_bus = g_bus_get_sync (G_BUS_TYPE_SYSTEM, NULL, &bus_error);
    if (sys_bus != NULL)
      self->malcontent = bz_malcontent_service_new (sys_bus, self->state);
    else
      g_warning ("Failed to connect to system bus for malcontent: %s", bus_error->message);

    if (self->malcontent != NULL)
      g_signal_connect_swapped (
          self->state,
          "notify::parental-age-rating",
          G_CALLBACK (show_hide_app_setting_changed),
          self);
  }

  g_signal_connect_swapped (
      self->state,
      "notify::disable-blocklists",
      G_CALLBACK (disable_blocklists_changed),
      self);

  network = g_network_monitor_get_default ();
  if (network != NULL)
    {
      GNetworkConnectivity connectivity = 0;
      gboolean             metered      = FALSE;

      connectivity = g_network_monitor_get_connectivity (network);
      bz_state_info_set_have_connection (self->state, connectivity == G_NETWORK_CONNECTIVITY_FULL);

      metered = g_network_monitor_get_network_metered (network);
      bz_state_info_set_metered_connection (self->state, metered);

      g_signal_connect_swapped (network, "notify", G_CALLBACK (network_status_changed), self);
    }
  else
    g_warning ("Unable to detect networking device! Continuing anyway...");

  app_id = g_application_get_application_id (G_APPLICATION (self));
  g_assert (app_id != NULL);
  g_debug ("Constructing gsettings for %s ...", app_id);
  self->settings = g_settings_new (app_id);

  if (g_settings_get_boolean (self->settings, "force-adwaita-icons"))
    {
      GtkSettings *gtk_settings = NULL;

      gtk_settings = gtk_settings_get_default ();
      g_object_set (
          gtk_settings,
          "gtk-icon-theme-name", "Adwaita",
          NULL);
    }

  bz_state_info_set_hide_eol (
      self->state,
      g_settings_get_boolean (self->settings, "hide-eol"));
  g_signal_connect_swapped (
      self->settings,
      "changed::hide-eol",
      G_CALLBACK (show_hide_app_setting_changed),
      self);

  bz_state_info_set_show_only_foss (
      self->state,
      g_settings_get_boolean (self->settings, "show-only-foss"));
  g_signal_connect_swapped (
      self->settings,
      "changed::show-only-foss",
      G_CALLBACK (show_hide_app_setting_changed),
      self);

  bz_state_info_set_show_only_flathub (
      self->state,
      g_settings_get_boolean (self->settings, "show-only-flathub"));
  g_signal_connect_swapped (
      self->settings,
      "changed::show-only-flathub",
      G_CALLBACK (show_hide_app_setting_changed),
      self);

  bz_state_info_set_show_only_verified (
      self->state,
      g_settings_get_boolean (self->settings, "show-only-verified"));
  g_signal_connect_swapped (
      self->settings,
      "changed::show-only-verified",
      G_CALLBACK (show_hide_app_setting_changed),
      self);

  self->blocklist_regexes = g_ptr_array_new_with_free_func (
      (GDestroyNotify) g_ptr_array_unref);
  self->blocklists_provider = bz_content_provider_new ();
  bz_content_provider_set_parser (self->blocklists_provider, BZ_PARSER (self->blocklist_parser));
  bz_content_provider_set_input_files (
      self->blocklists_provider, G_LIST_MODEL (self->blocklists_to_files));
  g_signal_connect_swapped (self->blocklists_provider, "items-changed", G_CALLBACK (blocklists_changed), self);

  self->txt_blocked_id_sets = g_ptr_array_new_with_free_func (
      (GDestroyNotify) g_hash_table_unref);
  self->txt_blocklists_provider = bz_content_provider_new ();
  bz_content_provider_set_parser (self->txt_blocklists_provider, BZ_PARSER (self->txt_blocklist_parser));
  bz_content_provider_set_input_files (
      self->txt_blocklists_provider, G_LIST_MODEL (self->txt_blocklists_to_files));
  g_signal_connect_swapped (self->txt_blocklists_provider, "items-changed", G_CALLBACK (txt_blocklists_changed), self);

  self->groups         = g_list_store_new (BZ_TYPE_ENTRY_GROUP);
  self->installed_apps = g_list_store_new (BZ_TYPE_ENTRY_GROUP);
  self->ids_to_groups  = g_hash_table_new_full (
      g_str_hash, g_str_equal, g_free, g_object_unref);
  self->eol_runtimes = g_hash_table_new_full (
      g_str_hash, g_str_equal, g_free, g_free);
  self->sys_name_to_addons = g_hash_table_new_full (
      g_str_hash, g_str_equal, g_free, (GDestroyNotify) g_ptr_array_unref);
  self->usr_name_to_addons = g_hash_table_new_full (
      g_str_hash, g_str_equal, g_free, (GDestroyNotify) g_ptr_array_unref);
  self->sys_ref_to_addon_group_ids = g_hash_table_new_full (
      g_str_hash, g_str_equal, g_free, (GDestroyNotify) g_ptr_array_unref);
  self->usr_ref_to_addon_group_ids = g_hash_table_new_full (
      g_str_hash, g_str_equal, g_free, (GDestroyNotify) g_ptr_array_unref);

  self->entry_factory = bz_application_map_factory_new (
      (GtkMapListModelMapFunc) map_ids_to_entries,
      self, NULL, NULL, NULL);

  filter = gtk_custom_filter_new (
      (GtkCustomFilterFunc) filter_application_ids, self, NULL);
  self->appid_filter        = g_object_ref_sink (g_steal_pointer (&filter));
  self->application_factory = bz_application_map_factory_new (
      (GtkMapListModelMapFunc) map_generic_ids_to_groups,
      self, NULL, NULL, GTK_FILTER (self->appid_filter));

  filter = gtk_custom_filter_new (
      (GtkCustomFilterFunc) filter_entry_groups, self, NULL);
  self->group_filter       = g_object_ref_sink (g_steal_pointer (&filter));
  self->group_filter_model = gtk_filter_list_model_new (
      g_object_ref (G_LIST_MODEL (self->groups)),
      g_object_ref (GTK_FILTER (self->group_filter)));

  self->search_engine = bz_search_engine_new ();
  bz_search_engine_set_model (self->search_engine, G_LIST_MODEL (self->group_filter_model));
  bz_search_engine_set_biases (self->search_engine, G_LIST_MODEL (self->search_biases));

  self->curated_provider = bz_content_provider_new ();
  bz_content_provider_set_input_files (
      self->curated_provider, G_LIST_MODEL (self->curated_configs_to_files));
  bz_content_provider_set_parser (self->curated_provider, BZ_PARSER (self->curated_parser));

  self->transactions = bz_transaction_manager_new ();
  bz_transaction_manager_set_config (self->transactions, self->config);

  bz_state_info_set_all_entry_groups (self->state, G_LIST_MODEL (self->groups));
  bz_state_info_set_all_installed_entry_groups (self->state, G_LIST_MODEL (self->installed_apps));
  bz_state_info_set_filtered_entry_groups (self->state, G_LIST_MODEL (self->group_filter_model));
  bz_state_info_set_application_factory (self->state, self->application_factory);
  bz_state_info_set_blocklists (self->state, G_LIST_MODEL (self->blocklists));
  bz_state_info_set_blocklists_provider (self->state, self->blocklists_provider);
  bz_state_info_set_curated_configs (self->state, G_LIST_MODEL (self->curated_configs));
  bz_state_info_set_curated_provider (self->state, self->curated_provider);
  bz_state_info_set_entry_factory (self->state, self->entry_factory);
  bz_state_info_set_main_config (self->state, self->config);
  bz_state_info_set_search_engine (self->state, self->search_engine);
  bz_state_info_set_settings (self->state, self->settings);
  bz_state_info_set_transaction_manager (self->state, self->transactions);
  bz_state_info_set_txt_blocklists (self->state, G_LIST_MODEL (self->txt_blocklists));
  bz_state_info_set_txt_blocklists_provider (self->state, self->txt_blocklists_provider);
  bz_state_info_set_cache_manager (self->state, self->cache);

  g_object_bind_property (
      self->state, "allow-manual-sync",
      g_action_map_lookup_action (G_ACTION_MAP (self), "sync-remotes"), "enabled",
      G_BINDING_SYNC_CREATE);

  gtk_style_context_add_provider_for_display (
      gdk_display_get_default (),
      bz_get_pride_style_provider (),
      GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
}

static GtkWindow *
get_or_create_window (BzApplication *self)
{
  GtkWindow *window  = NULL;
  GList     *windows = NULL;

  window = gtk_application_get_active_window (GTK_APPLICATION (self));
  if (BZ_IS_WINDOW (window))
    {
      gtk_window_present (window);
      return window;
    }

  windows = gtk_application_get_windows (GTK_APPLICATION (self));
  for (GList *l = windows; l != NULL; l = l->next)
    {
      if (BZ_IS_WINDOW (l->data))
        {
          gtk_window_present (GTK_WINDOW (l->data));
          return GTK_WINDOW (l->data);
        }
    }

  return new_window (self);
}

static GtkWindow *
new_window (BzApplication *self)
{
  BzWindow *window                  = NULL;
  g_autoptr (GtkWidget) main_window = NULL;
  int width                         = 0;
  int height                        = 0;

  window = bz_window_new (self->state);
  gtk_application_add_window (GTK_APPLICATION (self), GTK_WINDOW (window));

  main_window = g_weak_ref_get (&self->main_window);
  if (main_window != NULL)
    {
      width  = gtk_widget_get_width (main_window);
      height = gtk_widget_get_height (main_window);

      g_settings_set (self->settings, "window-dimensions", "(ii)", width, height);
    }
  else
    {
      g_settings_get (self->settings, "window-dimensions", "(ii)", &width, &height);

      g_signal_connect_object (
          window, "close-request",
          G_CALLBACK (window_close_request),
          self, G_CONNECT_SWAPPED);
      g_weak_ref_init (&self->main_window, window);
    }

  gtk_window_set_default_size (GTK_WINDOW (window), width, height);
  gtk_window_present (GTK_WINDOW (window));

  if (dex_future_is_pending (DEX_FUTURE (self->first_window_opened)))
    dex_promise_resolve_boolean (self->first_window_opened, TRUE);

  return GTK_WINDOW (window);
}

static void
open_appstream_take (BzApplication *self,
                     char          *appstream)
{
  const char *id                     = NULL;
  g_autoptr (OpenAppstreamData) data = NULL;

  g_info ("Loading appstream link %s...", appstream);

  if (g_str_has_prefix (appstream, "appstream://"))
    id = appstream + strlen ("appstream://");
  else
    id = appstream + strlen ("appstream:");

  data       = open_appstream_data_new ();
  data->self = bz_track_weak (self);
  data->id   = g_strdup (id);

  dex_future_disown (dex_scheduler_spawn (
      dex_scheduler_get_default (),
      bz_get_dex_stack_size (),
      (DexFiberFunc) open_appstream_fiber,
      open_appstream_data_ref (data),
      open_appstream_data_unref));
  g_free (appstream);
}

static void
open_flatpakref_take (BzApplication *self,
                      GFile         *file)
{
  g_autoptr (GError) local_error      = NULL;
  gboolean         result             = FALSE;
  g_autofree char *path               = NULL;
  g_autoptr (OpenFlatpakrefData) data = NULL;

  path = g_file_get_path (file);
  if (path != NULL)
    /* We must do this synchronously so we don't lose access to a portal file */
    {
      g_autofree char *basename   = NULL;
      g_autofree char *module_dir = NULL;
      g_autofree char *staging    = NULL;
      g_autofree char *dest       = NULL;
      g_autoptr (GFile) copied    = NULL;

      basename   = g_file_get_basename (file);
      module_dir = bz_dup_module_dir ();
      staging    = g_build_filename (module_dir, "bundle-staging", NULL);
      g_mkdir_with_parents (staging, 0755);
      dest   = g_build_filename (staging, basename, NULL);
      copied = g_file_new_for_path (dest);

      result = g_file_copy (
          file, copied,
          G_FILE_COPY_OVERWRITE | G_FILE_COPY_NOFOLLOW_SYMLINKS,
          NULL, NULL, NULL,
          &local_error);
      if (result)
        {
          g_clear_object (&file);
          file = g_steal_pointer (&copied);
        }
      else
        {
          g_warning ("Failed to copy bundle to %s : %s",
                     dest, local_error->message);
          g_clear_error (&local_error);
          g_clear_object (&copied);
        }
    }

  data       = open_flatpakref_data_new ();
  data->self = bz_track_weak (self);
  data->file = g_steal_pointer (&file);

  dex_future_disown (dex_scheduler_spawn (
      dex_scheduler_get_default (),
      bz_get_dex_stack_size (),
      (DexFiberFunc) open_flatpakref_fiber,
      open_flatpakref_data_ref (data),
      open_flatpakref_data_unref));
}

static void
open_metainfo_take (BzApplication *self,
                    GFile         *file)
{
  g_autoptr (OpenMetainfoData) data = NULL;

  data       = open_metainfo_data_new ();
  data->self = bz_track_weak (self);
  data->file = g_steal_pointer (&file);

  dex_future_disown (dex_scheduler_spawn (
      dex_scheduler_get_default (),
      bz_get_dex_stack_size (),
      (DexFiberFunc) open_metainfo_fiber,
      open_metainfo_data_ref (data),
      open_metainfo_data_unref));
}

static void
command_line_open_location (BzApplication           *self,
                            GApplicationCommandLine *cmdline,
                            const char              *location)
{
  g_autoptr (GFile) file = NULL;

  if (g_uri_is_valid (location, G_URI_FLAGS_NONE, NULL) &&
      g_str_has_prefix (location, "appstream:"))
    {
      open_appstream_take (self, g_strdup (location));
      return;
    }

  if (g_uri_is_valid (location, G_URI_FLAGS_NONE, NULL))
    file = g_file_new_for_uri (location);
  else if (g_path_is_absolute (location))
    file = g_file_new_for_path (location);
  else
    {
      const char *cwd = NULL;

      cwd  = g_application_command_line_get_cwd (cmdline);
      file = cwd != NULL
                 ? g_file_new_build_filename (cwd, location, NULL)
                 : g_file_new_for_path (location);
    }

  if (g_str_has_suffix (location, ".metainfo.xml") ||
      g_str_has_suffix (location, ".appdata.xml"))
    open_metainfo_take (self, g_steal_pointer (&file));
  else
    open_flatpakref_take (self, g_steal_pointer (&file));
}

static void
open_generic_id (BzApplication *self,
                 const char    *generic_id)
{
  BzEntryGroup    *group        = NULL;
  GtkWindow       *window       = NULL;
  g_autofree char *corrected_id = NULL;
  const char      *original_id  = generic_id;
  const char      *matched_id   = generic_id;
  gboolean         case_fixed   = FALSE;

  group = g_hash_table_lookup (self->ids_to_groups, generic_id);

  // This is needed because KDE likes to mangle IDs just for fun...
  if (group == NULL)
    {
      gsize len = 0;

      len = strlen (generic_id);

      // if it has more than 3 parts and end with ".desktop" then cut it off.
      if (len > 8 && g_str_has_suffix (generic_id, ".desktop"))
        {
          guint       n_dots = 0;
          const char *suffix = NULL;

          for (const char *p = strchr (generic_id, '.');
               p != NULL;
               p = strchr (p, '.'))
            {
              if (++n_dots >= 3)
                {
                  suffix = strstr (generic_id, ".desktop");
                  g_assert (suffix != NULL);
                  break;
                }
            }

          if (suffix != NULL)
            {
              corrected_id = g_strndup (generic_id, suffix - generic_id);
              generic_id   = corrected_id;
              matched_id   = corrected_id;
              group        = g_hash_table_lookup (self->ids_to_groups, generic_id);
            }
        }

      if (group == NULL)
        {
          GHashTableIter iter = { 0 };

          g_hash_table_iter_init (&iter, self->ids_to_groups);
          for (;;)
            /* screeching sounds */
            {
              char         *key   = NULL;
              BzEntryGroup *value = NULL;

              if (!g_hash_table_iter_next (
                      &iter, (gpointer *) &key, (gpointer *) &value))
                break;

              if (g_ascii_strcasecmp (key, generic_id) == 0)
                {
                  group      = value;
                  matched_id = key;
                  case_fixed = TRUE;
                  break;
                }
            }
        }

      if (group == NULL)
        matched_id = original_id;
    }

  window = get_or_create_window (self);

  if (adw_application_window_get_visible_dialog (ADW_APPLICATION_WINDOW (window)) != NULL)
    window = new_window (self);

  if (group != NULL)
    {
      gtk_widget_activate_action (GTK_WIDGET (window), "window.show-group", "s", matched_id);

      if (case_fixed)
        bz_show_error_for_widget (
            GTK_WIDGET (window),
            _ ("Malformed Link"),
            _ ("The link used to open this app has incorrect capitalization and may stop working in the future.\n\n"
               "This is most likely caused by KRunner sending incorrect app IDs"));
    }
  else
    {
      g_autofree char *message = NULL;

      message = g_strdup_printf ("ID '%s' was not found", original_id);
      bz_show_error_for_widget (GTK_WIDGET (window), _ ("Could not find app"), message);
    }
}

static gpointer
map_strings_to_files (GtkStringObject *string,
                      gpointer         data)
{
  const char *path   = NULL;
  GFile      *result = NULL;

  path   = gtk_string_object_get_string (string);
  result = g_file_new_for_path (path);

  g_object_unref (string);
  return result;
}

static gpointer
map_generic_ids_to_groups (GtkStringObject *string,
                           BzApplication   *self)
{
  BzEntryGroup *group = NULL;

  group = g_hash_table_lookup (
      self->ids_to_groups,
      gtk_string_object_get_string (string));

  g_object_unref (string);
  return bz_object_maybe_ref (group);
}

static gpointer
map_ids_to_entries (GtkStringObject *string,
                    BzApplication   *self)
{
  g_autoptr (GError) local_error = NULL;
  const char *id                 = NULL;
  g_autoptr (DexFuture) future   = NULL;
  g_autoptr (BzResult) result    = NULL;

  id     = gtk_string_object_get_string (string);
  future = bz_entry_cache_manager_get (self->cache, id);
  result = bz_result_new (future);

  g_object_unref (string);
  return g_steal_pointer (&result);
}

static gboolean
filter_application_ids (GtkStringObject *string,
                        BzApplication   *self)
{
  BzEntryGroup *group = NULL;

  group = g_hash_table_lookup (
      self->ids_to_groups,
      gtk_string_object_get_string (string));
  if (group != NULL)
    return validate_group_for_ui (self, group);
  else
    return FALSE;
}

static gboolean
filter_entry_groups (BzEntryGroup  *group,
                     BzApplication *self)
{
  return validate_group_for_ui (self, group);
}

static gint
cmp_group (BzEntryGroup *a,
           BzEntryGroup *b,
           gpointer      user_data)
{
  const char *title_a = NULL;
  const char *title_b = NULL;

  title_a = bz_entry_group_get_title (a);
  title_b = bz_entry_group_get_title (b);

  if (title_a == NULL)
    return 1;
  if (title_b == NULL)
    return -1;

  return strcasecmp (title_a, title_b);
}

static gboolean
validate_group_for_ui (BzApplication *self,
                       BzEntryGroup  *group)
{
  const char *id               = NULL;
  int         allowed_priority = G_MAXINT;
  int         blocked_priority = G_MAXINT;

  if (bz_state_info_get_hide_eol (self->state) &&
      bz_entry_group_get_eol (group) != NULL)
    return FALSE;
  if (bz_state_info_get_show_only_foss (self->state) &&
      !bz_entry_group_get_is_floss (group))
    return FALSE;
  if (bz_state_info_get_show_only_flathub (self->state) &&
      !bz_entry_group_get_is_flathub (group))
    return FALSE;
  if (bz_state_info_get_show_only_verified (self->state) &&
      !bz_entry_group_get_is_verified (group))
    return FALSE;

  if (self->malcontent != NULL)
    {
      int parental_age = -1;
      int app_age      = 0;

      parental_age = bz_state_info_get_parental_age_rating (self->state);
      app_age      = bz_entry_group_get_content_age_rating (group);

      if (app_age > parental_age && parental_age != -1)
        return FALSE;
    }

  if (bz_state_info_get_disable_blocklists (self->state))
    return TRUE;

  id = bz_entry_group_get_id (group);
  for (guint i = 0; i < self->txt_blocked_id_sets->len; i++)
    {
      GHashTable *set = NULL;

      set = g_ptr_array_index (self->txt_blocked_id_sets, i);
      if (g_hash_table_contains (set, id))
        return FALSE;
    }

  for (guint i = 0; i < self->blocklist_regexes->len; i++)
    {
      GPtrArray *regex_datas = NULL;

      regex_datas = g_ptr_array_index (self->blocklist_regexes, i);
      for (guint j = 0; j < regex_datas->len; j++)
        {
          BlocklistRegexData *data = NULL;

          data = g_ptr_array_index (regex_datas, j);

          if (data->allow != NULL &&
              data->priority < allowed_priority &&
              g_regex_match (data->allow, id, G_REGEX_MATCH_DEFAULT, NULL))
            allowed_priority = data->priority;
          if (data->block != NULL &&
              data->priority < blocked_priority &&
              g_regex_match (data->block, id, G_REGEX_MATCH_DEFAULT, NULL))
            blocked_priority = data->priority;
        }
    }
  return allowed_priority <= blocked_priority;
}

static DexFuture *
make_sync_future (BzApplication *self)
{
  g_autoptr (GError) local_error         = NULL;
  g_autoptr (GSubprocess) refresh_worker = NULL;
  g_autoptr (DexFuture) backend_future   = NULL;
  g_autoptr (DexFuture) flathub_future   = NULL;
  g_autoptr (DexFuture) ret_future       = NULL;

  bz_state_info_set_allow_manual_sync (self->state, FALSE);

  bz_state_info_set_syncing (self->state, TRUE);
  bz_state_info_set_recently_synced (self->state, TRUE);
  finish_with_background_task_label (self);

  refresh_worker = g_subprocess_new (
      G_SUBPROCESS_FLAGS_NONE,
      &local_error,
      BAZAAR_BIN_NAME,
      REFRESH_WORKER_CLI_OPTION,
      NULL);
  if (refresh_worker == NULL)
    g_critical ("FATAL!!! The refresh worker could not be spawned: %s",
                local_error->message);
  g_assert (refresh_worker != NULL);

  backend_future = dex_subprocess_wait_check (refresh_worker);
  backend_future = dex_future_finally (
      backend_future,
      (DexFutureCallback) backend_sync_finally,
      bz_track_weak (self), bz_weak_release);

  g_clear_object (&self->tmp_flathub);
  self->tmp_flathub = bz_flathub_state_new ();
  flathub_future    = bz_flathub_state_update_to_today (self->tmp_flathub);
  flathub_future    = dex_future_finally (
      flathub_future,
      (DexFutureCallback) flathub_update_finally,
      bz_track_weak (self), bz_weak_release);

  ret_future = dex_future_all (
      dex_ref (backend_future),
      dex_ref (flathub_future),
      NULL);
  ret_future = dex_future_finally (
      ret_future,
      (DexFutureCallback) sync_finally,
      bz_track_weak (self), bz_weak_release);
  return g_steal_pointer (&ret_future);
}

static void
finish_with_background_task_label (BzApplication *self)
{
  if (self->n_entries_incoming > 0)
    bz_state_info_set_background_task_label_take_printf (
        self->state,
        ngettext ("Loading %d app…", "Loading %d apps…", self->n_entries_incoming),
        self->n_entries_incoming);
  else if (bz_state_info_get_syncing (self->state))
    bz_state_info_set_background_task_label (self->state, _ ("Refreshing…"));
  else if (bz_state_info_get_busy (self->state))
    bz_state_info_set_background_task_label (self->state, _ ("Writing to cache…"));
  else
    bz_state_info_set_background_task_label (self->state, NULL);
}

static void
state_info_set_icon_themes (BzStateInfo *state)
{
  g_autoptr (GtkIconTheme) user_theme   = NULL;
  g_autoptr (GtkIconTheme) system_theme = NULL;
  g_autofree char *user_export_dir      = NULL;

  user_theme      = gtk_icon_theme_new ();
  user_export_dir = g_build_filename (g_get_home_dir (), ".local/share/flatpak/exports/share/icons", NULL);
  gtk_icon_theme_add_search_path (user_theme, user_export_dir);
  bz_state_info_set_user_icon_theme (state, user_theme);

  system_theme = gtk_icon_theme_new ();
  gtk_icon_theme_add_search_path (system_theme, "/var/lib/flatpak/exports/share/icons");
  bz_state_info_set_system_icon_theme (state, system_theme);
}
