/*
 * Copyright (c) 2026 Phosh.mobi e.V.
 *
 * SPDX-License-Identifier: LGPL-2.1-or-later
 *
 * Inspired by GNOME Software's gs-plugin-systemd-sysupdate.c
 *
 * Author: Guido Günther <agx@sigxcpu.org>
 */

#define G_LOG_DOMAIN "ms-systemd-sysupdate"

#include "mobile-settings-config.h"

#include "ms-os-update-priv.h"
#include "ms-os-updater-priv.h"
#include "ms-dbus-systemd-sysupdated.h"
#include "ms-systemd-sysupdate.h"

#include <json-glib/json-glib.h>

#include <gio/gio.h>

/**
 * MsSystemdSysupdate:
 *
 * Perform host updates via systemd's sysupdated sysupdate1 DBus
 * interface
 */

/* We can use the session bus for easy mocking */
#if 0
# define MS_DBUS_BUS (G_BUS_TYPE_SESSION)
#else
# define MS_DBUS_BUS (G_BUS_TYPE_SYSTEM)
#endif


struct _MsSystemdSysupdate {
  MsOsUpdater             parent;

  MsDBusSysupdateManager *manager;
  GDBusConnection        *system_bus;
  GCancellable           *cancel;
  GPtrArray *targets;
};
G_DEFINE_TYPE (MsSystemdSysupdate, ms_systemd_sysupdate, MS_TYPE_OS_UPDATER)


typedef struct _Target {
  MsDBusSysupdateTarget *target;
  MsSystemdSysupdate    *sysupdate; /* unowned */
  GCancellable          *cancel;

  char      *class;
  char      *name;
  GPtrArray *versions;
} Target;


typedef struct _Version {
  char       *version;
  Target     *target;
  gboolean    newest;
  gboolean    available;
  gboolean    installed;
  gboolean    obsolete;

  MsDBusSysupdateJob *job_proxy;
  guint64     job_id;
  gulong      progress_id;

  /* The task associated with the current job */
  GTask      *task;
  /* The task for cancelling the current job */
  GTask      *cancel_task;
  MsOsUpdate *os_update;
} Version;


static void version_destroy (Version *version);

static Target *
target_new (const char *name, const char *class, MsSystemdSysupdate *sysupdate)
{
  Target *target = g_new0 (Target, 1);

  target->sysupdate = sysupdate;
  target->cancel = g_cancellable_new ();
  target->name = g_strdup (name);
  target->class = g_strdup (class);
  target->versions = g_ptr_array_new_with_free_func ((GDestroyNotify)version_destroy);

  return target;
}


static void
target_destroy (Target *target)
{
  g_cancellable_cancel (target->cancel);
  g_clear_object (&target->cancel);

  g_clear_object (&target->target);

  g_clear_pointer (&target->name, g_free);
  g_clear_pointer (&target->class, g_free);
  g_clear_pointer (&target->versions, g_ptr_array_unref);

  g_free (target);
}


static Version *
version_new (const char *ver, Target *target)
{
  Version *version = g_new0 (Version, 1);

  version->version = g_strdup (ver);
  version->target = target;

  return version;
}


static void
clear_job_state (Version *version)
{
  g_clear_signal_handler (&version->progress_id, version->job_proxy);
  g_clear_object (&version->job_proxy);
  version->job_id = 0;
}


static void
version_destroy (Version *version)
{
  g_clear_pointer (&version->version, g_free);

  clear_job_state (version);

  g_clear_object (&version->os_update);
  g_clear_object (&version->task);

  g_free (version);
}


static void
on_job_progress (Version *version, GParamSpec *pspec, MsDBusSysupdateJob *job_proxy)
{
  guint progress = ms_dbus_sysupdate_job_get_progress (job_proxy);

  g_assert (MS_IS_OS_UPDATE (version->os_update));

  g_debug ("Job %" G_GUINT64_FORMAT " progress: %u", version->job_id, progress);
  ms_os_update_set_progress (version->os_update, progress);
}


static void
on_job_proxy_ready (GObject *source_object, GAsyncResult *res, gpointer user_data)
{
  MsDBusSysupdateJob *job_proxy;
  g_autoptr (GError) err = NULL;
  Version *version = user_data;

  job_proxy = ms_dbus_sysupdate_job_proxy_new_finish (res, &err);
  if (!job_proxy) {
    if (!g_error_matches (err, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
      g_autoptr (GTask) task = g_steal_pointer (&version->task);

      g_warning ("Failed to get job proxy for update to %s: %s", version->version, err->message);
      g_task_return_error (task, g_steal_pointer (&err));
    }
    return;
  }

  g_debug ("Got job proxy for update to %s", version->version);
  version->job_proxy = job_proxy;
  version->progress_id = g_signal_connect_swapped (version->job_proxy,
                                                   "notify::progress",
                                                   G_CALLBACK (on_job_progress),
                                                   version);
}


static void on_job_cancel_ready (GObject *source_object, GAsyncResult *res, gpointer user_data);


static void
ms_systemd_sysupdate_cancel_job (MsOsUpdater        *updater,
                                 MsOsUpdate         *update,
                                 MsOsUpdateFlags     flags,
                                 MsOsUpdateState     state,
                                 GCancellable       *cancel,
                                 GAsyncReadyCallback callback,
                                 gpointer            user_data)
{
  MsSystemdSysupdate *self = MS_SYSTEMD_SYSUPDATE (updater);
  Version *version = g_object_get_data (G_OBJECT (update), "sysupdate-version");
  g_autoptr (GTask) task = NULL;

  g_return_if_fail (version);
  g_return_if_fail (version->job_id > 0);
  g_return_if_fail (MS_DBUS_SYSUPDATE_IS_JOB_PROXY (version->job_proxy));

  task = g_task_new (self, cancel, callback, user_data);
  g_task_set_source_tag (task, ms_systemd_sysupdate_cancel_job);
  version->cancel_task = g_steal_pointer (&task);

  if (ms_os_update_get_state (update) != state) {
    GError *err;

    err = g_error_new_literal (G_IO_ERROR,
                               G_IO_ERROR_INVALID_ARGUMENT,
                               "Update not in cancellable state");
    g_task_return_error (task, g_steal_pointer (&err));
    return;
  }

  ms_dbus_sysupdate_job_call_cancel (version->job_proxy,
                                     G_DBUS_CALL_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION,
                                     -1,
                                     version->target->cancel,
                                     on_job_cancel_ready,
                                     version);
}


static void
on_job_cancel_ready (GObject *source_object, GAsyncResult *res, gpointer user_data)
{
  g_autoptr (GError) err = NULL;
  gboolean success;
  Version *version = user_data;
  g_autoptr (GTask) task = NULL;

  success = ms_dbus_sysupdate_job_call_cancel_finish (MS_DBUS_SYSUPDATE_JOB (source_object),
                                                      res,
                                                      &err);
  if (!success) {
    if (!g_error_matches (err, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
      task = g_steal_pointer (&version->cancel_task);

      g_warning ("Failed to cancel job for %s: %s", version->version, err->message);
      /* Job status will set in `job-removed` */
      g_task_return_error (task, g_steal_pointer (&err));
    }
    return;
  }

  ms_os_update_set_state (version->os_update, MS_OS_UPDATE_STATE_READY);
  /* Job state itself will be cleared when handling `job-removed` */
  task = g_steal_pointer (&version->cancel_task);
  g_assert (g_task_get_source_tag (task) == ms_systemd_sysupdate_cancel_job);
  g_task_return_boolean (task, TRUE);
}


static void
ms_systemd_sysupdate_cancel_install_update (MsOsUpdater        *updater,
                                            MsOsUpdate         *update,
                                            MsOsUpdateFlags     flags,
                                            GCancellable       *cancel,
                                            GAsyncReadyCallback callback,
                                            gpointer            user_data)
{
  ms_systemd_sysupdate_cancel_job (updater,
                                   update,
                                   flags,
                                   MS_OS_UPDATE_STATE_INSTALLING,
                                   cancel,
                                   callback,
                                   user_data);
}


static void on_target_install_update_ready (GObject *source_object, GAsyncResult *res,
                                            gpointer user_data);


static void
ms_systemd_sysupdate_install_update (MsOsUpdater        *updater,
                                     MsOsUpdate         *update,
                                     MsOsUpdateFlags     flags,
                                     GCancellable       *cancel,
                                     GAsyncReadyCallback callback,
                                     gpointer            user_data)
{
  MsSystemdSysupdate *self = MS_SYSTEMD_SYSUPDATE (updater);
  Version *version = g_object_get_data (G_OBJECT (update), "sysupdate-version");
  const char *new_version = "";
  GTask *task;

  g_return_if_fail (version);
  g_return_if_fail (version->job_id == 0);

  task = g_task_new (self, cancel, callback, user_data);
  g_task_set_source_tag (task, ms_systemd_sysupdate_install_update);
  version->task = g_steal_pointer (&task);

  /* sysupdate1 allows the user to install the latest version, other versions need creds */
  if (!version->newest)
    new_version = version->version;

  ms_dbus_sysupdate_target_call_install (version->target->target,
                                         new_version,
                                         0,
                                         G_DBUS_CALL_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION,
                                         -1,
                                         version->target->cancel,
                                         on_target_install_update_ready,
                                         version);
}


static void
on_target_install_update_ready (GObject *source_object, GAsyncResult *res, gpointer user_data)
{
  g_autofree char *new_version = NULL;
  g_autofree char *job_path = NULL;
  g_autoptr (GError) err = NULL;
  guint64 job_id;
  gboolean success;
  Version *version = user_data;

  success = ms_dbus_sysupdate_target_call_install_finish (MS_DBUS_SYSUPDATE_TARGET (source_object),
                                                          &new_version,
                                                          &job_id,
                                                          &job_path,
                                                          res,
                                                          &err);
  if (!success) {
    if (g_error_matches (err, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
      g_debug ("Canceled install");
    } else {
      g_autoptr (GTask) task = g_steal_pointer (&version->task);

      g_debug ("Can't start install job: %d", err->code);
      if (g_dbus_error_is_remote_error (err) &&
          g_strcmp0 (g_dbus_error_get_remote_error (err),
                     "org.freedesktop.sysupdate1.NoCandidate") == 0) {
        /* When writing to a partition install is a noop */
        g_debug ("Version %s already installed", version->version);
        ms_os_update_set_state (version->os_update, MS_OS_UPDATE_STATE_INSTALLED);
        g_task_return_boolean (task, TRUE);
        return;
      }

      g_warning ("Failed to trigger install job for %s: %s", version->version, err->message);
      ms_os_update_set_state (version->os_update, MS_OS_UPDATE_STATE_FAILED);
      g_task_return_error (task, g_steal_pointer (&err));
    }
    return;
  }

  g_assert (g_task_get_source_tag (version->task) == ms_systemd_sysupdate_install_update);
  ms_os_update_set_state (version->os_update, MS_OS_UPDATE_STATE_INSTALLING);
  g_debug ("Getting proxy for %" G_GUINT64_FORMAT " %s", job_id, job_path);
  version->job_id = job_id;
  ms_dbus_sysupdate_job_proxy_new (g_dbus_proxy_get_connection (G_DBUS_PROXY (source_object)),
                                   G_DBUS_PROXY_FLAGS_NONE,
                                   "org.freedesktop.sysupdate1",
                                   job_path,
                                   version->target->cancel,
                                   on_job_proxy_ready,
                                   version);
}


static void
ms_systemd_sysupdate_cancel_fetch_update (MsOsUpdater        *updater,
                                          MsOsUpdate         *update,
                                          MsOsUpdateFlags     flags,
                                          GCancellable       *cancel,
                                          GAsyncReadyCallback callback,
                                          gpointer            user_data)
{
  ms_systemd_sysupdate_cancel_job (updater,
                                   update,
                                   flags,
                                   MS_OS_UPDATE_STATE_FETCHING,
                                   cancel,
                                   callback,
                                   user_data);
}


static void
on_target_acquire_update_ready (GObject *source_object, GAsyncResult *res, gpointer user_data)
{
  g_autofree char *new_version = NULL;
  g_autofree char *job_path = NULL;
  g_autoptr (GError) err = NULL;
  guint64 job_id;
  gboolean success;
  Version *version = user_data;

  success = ms_dbus_sysupdate_target_call_acquire_finish (MS_DBUS_SYSUPDATE_TARGET (source_object),
                                                          &new_version,
                                                          &job_id,
                                                          &job_path,
                                                          res,
                                                          &err);
  if (!success) {
    if (g_error_matches (err, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
      g_debug ("Canceled acquire");
    } else {
      g_autoptr (GTask) task = g_steal_pointer (&version->task);

      g_debug ("Can't start acquire job: %d", err->code);
      if (g_dbus_error_is_remote_error (err) &&
          g_strcmp0 (g_dbus_error_get_remote_error (err),
                     "org.freedesktop.sysupdate1.NoCandidate") == 0) {
        g_debug ("Version %s already fetched", version->version);
        ms_os_update_set_state (version->os_update, MS_OS_UPDATE_STATE_FETCHED);
        g_task_return_boolean (task, TRUE);
        return;
      }

      g_warning ("Failed to trigger install job for %s: %s", version->version, err->message);
      ms_os_update_set_state (version->os_update, MS_OS_UPDATE_STATE_FAILED);
      g_task_return_error (task, g_steal_pointer (&err));
    }
    return;
  }

  ms_os_update_set_state (version->os_update, MS_OS_UPDATE_STATE_FETCHING);
  g_debug ("Getting proxy for %" G_GUINT64_FORMAT " %s", job_id, job_path);
  version->job_id = job_id;
  ms_dbus_sysupdate_job_proxy_new (g_dbus_proxy_get_connection (G_DBUS_PROXY (source_object)),
                                   G_DBUS_PROXY_FLAGS_NONE,
                                   "org.freedesktop.sysupdate1",
                                   job_path,
                                   version->target->cancel,
                                   on_job_proxy_ready,
                                   version);
}


static void
ms_systemd_sysupdate_fetch_update (MsOsUpdater        *updater,
                                   MsOsUpdate         *update,
                                   MsOsUpdateFlags     flags,
                                   GCancellable       *cancel,
                                   GAsyncReadyCallback callback,
                                   gpointer            user_data)
{
  MsSystemdSysupdate *self = MS_SYSTEMD_SYSUPDATE (updater);
  Version *version = g_object_get_data (G_OBJECT (update), "sysupdate-version");
  const char *new_version = "";
  GTask *task;

  g_return_if_fail (version);
  g_return_if_fail (version->job_id == 0);

  task = g_task_new (self, cancel, callback, user_data);
  g_task_set_source_tag (task, ms_systemd_sysupdate_install_update);
  version->task = g_steal_pointer (&task);

  /* sysupdate1 allows the user to install the latest version, other versions need creds */
  if (!version->newest)
    new_version = version->version;

  ms_dbus_sysupdate_target_call_acquire (version->target->target,
                                         new_version,
                                         0,
                                         G_DBUS_CALL_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION,
                                         -1,
                                         version->target->cancel,
                                         on_target_acquire_update_ready,
                                         version);
}


static void
on_target_describe_ready (GObject *source_object, GAsyncResult *res, gpointer user_data)
{
  gboolean success;
  g_autoptr (JsonParser) parser = json_parser_new ();
  g_autofree char *json = NULL;
  g_autoptr (GError) err = NULL;
  JsonNode *node;
  JsonObject *obj;
  Version *version = user_data;

  success = ms_dbus_sysupdate_target_call_describe_finish (MS_DBUS_SYSUPDATE_TARGET (source_object),
                                                           &json,
                                                           res,
                                                           &err);
  if (!success) {
    if (!g_error_matches (err, G_IO_ERROR, G_IO_ERROR_CANCELLED))
      g_warning ("Failed to get describe target version %s: %s", version->version, err->message);
    return;
  }

  success = json_parser_load_from_data (parser, json, -1, &err);
  if (!success) {
    g_warning ("Failed to parse json %s: %s", json, err->message);
    return;
  }

  node = json_parser_get_root (parser);
  obj = json_node_get_object (node);

  version->newest = json_object_get_boolean_member (obj, "newest");
  version->available = json_object_get_boolean_member (obj, "available");
  version->installed = json_object_get_boolean_member (obj, "installed");
  version->obsolete = json_object_get_boolean_member (obj, "obsolete");

  g_debug ("Parsed %s: newest: %d", version->version, version->newest);
  if (version->newest && version->available && !version->installed) {
    MsOsUpdate *os_update = ms_os_update_new (version->version);

    g_debug ("Update %s is usable", version->version);
    /* TODO: we should use a derived class but this updater will be replaced with systemd 262 */
    g_object_set_data (G_OBJECT (os_update), "sysupdate-version", version);
    version->os_update = g_object_ref (os_update);
    ms_os_update_set_state (version->os_update, MS_OS_UPDATE_STATE_READY);

    ms_os_updater_set_latest_update (MS_OS_UPDATER (version->target->sysupdate), os_update);
  }
}


static void
on_target_list_ready (GObject *source_object, GAsyncResult *res, gpointer user_data)
{
  gboolean success;
  Target *target;
  g_auto (GStrv) versions = NULL;
  g_autoptr (GError) err = NULL;
  target = user_data;

  success = ms_dbus_sysupdate_target_call_list_finish (MS_DBUS_SYSUPDATE_TARGET (source_object),
                                                       &versions,
                                                       res,
                                                       &err);
  if (!success) {
    if (!g_error_matches (err, G_IO_ERROR, G_IO_ERROR_CANCELLED))
      g_warning ("Failed to get versions for target %s: %s", target->name, err->message);
    return;
  }

  for (int i = 0; versions[i]; i++) {
    Version *version = version_new (versions[i], target);

    g_debug ("Found Version: %s", version->version);

    g_ptr_array_add (target->versions, version);

    ms_dbus_sysupdate_target_call_describe (target->target,
                                            version->version,
                                            0, /* flags */
                                            G_DBUS_CALL_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION,
                                            10000,
                                            target->cancel,
                                            on_target_describe_ready,
                                            version);
  }
}


static void
on_target_proxy_ready (GObject *source_object, GAsyncResult *res, gpointer user_data)
{
  Target *target;
  MsDBusSysupdateTarget *target_proxy;
  g_autoptr (GError) err = NULL;

  target_proxy = ms_dbus_sysupdate_target_proxy_new_finish (res, &err);
  if (!target_proxy) {
    g_warning ("Failed to get target proxy: %s", err->message);
    return;
  }

  target = user_data;
  target->target = target_proxy;

  ms_dbus_sysupdate_target_call_list (target->target,
                                      0, /* flags */
                                      G_DBUS_CALL_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION,
                                      10000,
                                      target->cancel,
                                      on_target_list_ready,
                                      target);
}


static void
on_list_targets_ready (GObject *source_object, GAsyncResult *res, gpointer user_data)
{
  MsSystemdSysupdate *self;
  MsDBusSysupdateManager *manager;
  const char *class = NULL;
  const char *name = NULL;
  const char *object_path = NULL;
  g_autoptr (GVariant) ret_targets = NULL;
  g_autoptr (GVariantIter) variant_iter = NULL;
  g_autoptr (GError) err = NULL;
  gboolean success;

  manager = MS_DBUS_SYSUPDATE_MANAGER (source_object);
  success = ms_dbus_sysupdate_manager_call_list_targets_finish (manager,
                                                                &ret_targets,
                                                                res,
                                                                &err);
  if (!success) {
    g_warning ("Failed to list sysupdate targets: %s", err->message);
    return;
  }

  self = MS_SYSTEMD_SYSUPDATE (user_data);

  g_variant_get (ret_targets, "a(sso)", &variant_iter);
  /* Get info about all the targets */
  while (g_variant_iter_loop (variant_iter, "(&s&s&o)", &class, &name, &object_path)) {
    Target *target;

    g_debug ("Target: %s, class: %s, path: %s", name, class, object_path);

    /* We're only interested in host updates */
    if (g_strcmp0 (name, "host") || g_strcmp0 (class, "host")) {
      g_debug ("Skipping %s/%s", name, class);
      continue;
    }

    /* Since we can update the host, lets signal that: */
    ms_os_updater_set_supported (MS_OS_UPDATER (self), TRUE);
    /* TODO: do we need more than one? Isn't there only host? */
    target = target_new (name, class, self);
    g_ptr_array_add (self->targets, target);

    ms_dbus_sysupdate_target_proxy_new (g_dbus_proxy_get_connection (G_DBUS_PROXY (manager)),
                                        G_DBUS_PROXY_FLAGS_NONE,
                                        "org.freedesktop.sysupdate1",
                                        object_path,
                                        target->cancel,
                                        on_target_proxy_ready,
                                        target);
  }
}


static void
on_job_removed (MsSystemdSysupdate *self, guint64 job_id, const char *job_path, int status)
{
  g_debug ("Job %" G_GUINT64_FORMAT " at %s finished: %d", job_id, job_path, status);

  g_return_if_fail (MS_IS_SYSTEMD_SYSUPDATE (self));

  for (guint i = 0; i < self->targets->len; i++) {
    Target *target = g_ptr_array_index (self->targets, i);

    for (guint j = 0; j < target->versions->len; j++) {
      Version *version = g_ptr_array_index (target->versions, j);
      MsOsUpdateState state;
      g_autoptr (GTask) task = NULL;

      if (version->job_proxy == NULL)
        continue;

      if (version->job_id != job_id)
        continue;

      if (g_strcmp0 (g_dbus_proxy_get_object_path (G_DBUS_PROXY (version->job_proxy)), job_path))
        continue;

      g_debug ("Found removed job %" G_GUINT64_FORMAT, job_id);

      g_return_if_fail (G_IS_TASK (version->task));
      task = g_steal_pointer (&version->task);

      /* Clear progress, etc */
      clear_job_state (version);

      state = ms_os_update_get_state (version->os_update);
      if (status == 0) {
        state = ms_os_update_get_state (version->os_update);
        if (state == MS_OS_UPDATE_STATE_FETCHING)
          state = MS_OS_UPDATE_STATE_FETCHED;
        else if (state == MS_OS_UPDATE_STATE_INSTALLING)
          state = MS_OS_UPDATE_STATE_INSTALLED;

        ms_os_update_set_state (version->os_update, state);
        g_task_return_boolean (task, TRUE);
      } else {
        GError *err;
        GIOErrorEnum code;

        g_debug ("Job in state %d exited with %d", state, status);
        if (status < 0)
          code = g_io_error_from_errno (-status);
        else
          code = G_IO_ERROR_FAILED;

        if (state == MS_OS_UPDATE_STATE_FETCHING)
          err = g_error_new_literal (G_IO_ERROR, code, "Fetch step failed");
        else if (state == MS_OS_UPDATE_STATE_INSTALLING)
          err = g_error_new_literal (G_IO_ERROR, code, "Install step failed");
        else
          err = g_error_new_literal (G_IO_ERROR, code, "Job failed");

        ms_os_update_set_state (version->os_update, MS_OS_UPDATE_STATE_FAILED);
        g_task_return_error (task, err);
      }
      return;
    }
  }
  g_debug ("No matching job for %" G_GUINT64_FORMAT " at %s found.", job_id, job_path);
}


static void
on_sysupdate_proxy_ready (GObject *source_object, GAsyncResult *res, gpointer user_data)
{
  MsSystemdSysupdate *self = MS_SYSTEMD_SYSUPDATE (user_data);
  g_autoptr (GError) err = NULL;
  MsDBusSysupdateManager *manager;

  manager = ms_dbus_sysupdate_manager_proxy_new_finish (res, &err);
  if (manager == NULL) {
    if (g_error_matches (err, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
      g_debug ("Cancelled getting sysupdate manager");
      return;
    }

    g_warning ("Failed to get sysupdate manager: %s", err->message);
    ms_os_updater_set_supported (MS_OS_UPDATER (self), FALSE);
    return;
  }

  self->manager = manager;

  g_signal_connect_object (self->manager,
                           "job-removed",
                           G_CALLBACK (on_job_removed),
                           self,
                           G_CONNECT_SWAPPED);

  ms_dbus_sysupdate_manager_call_list_targets (self->manager,
                                               G_DBUS_CALL_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION,
                                               10000,
                                               self->cancel,
                                               on_list_targets_ready,
                                               self);
}


static void
on_list_activatable_names_ready (GObject *source_object, GAsyncResult *res, gpointer user_data)
{
  MsSystemdSysupdate *self;
  g_autoptr (GError) err = NULL;
  g_autoptr (GVariant) ret = NULL;
  const char *activatable_names;

  ret = g_dbus_connection_call_finish (G_DBUS_CONNECTION (source_object), res, &err);
  if (ret == NULL) {
    if (g_error_matches (err, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
      g_debug ("Cancelled listing activatable names");
      return;
    }

    self = MS_SYSTEMD_SYSUPDATE (user_data);
    g_warning ("Failed to list activatable names: %s", err->message);
    ms_os_updater_set_supported (MS_OS_UPDATER (self), FALSE);
    return;
  }

  self = MS_SYSTEMD_SYSUPDATE (user_data);
  if (MS_DBUS_BUS == G_BUS_TYPE_SYSTEM) {
    g_variant_get (ret, "(^a&s)", &activatable_names);
    if (!g_strv_contains ((const char *const *) activatable_names, "org.freedesktop.sysupdate1")) {
      g_debug ("systemd-sysupdate service not available");
      ms_os_updater_set_supported (MS_OS_UPDATER (self), FALSE);
      return;
    }
  }

  ms_dbus_sysupdate_manager_proxy_new (self->system_bus,
                                       G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START_AT_CONSTRUCTION,
                                       "org.freedesktop.sysupdate1",
                                       "/org/freedesktop/sysupdate1",
                                       self->cancel,
                                       on_sysupdate_proxy_ready,
                                       self);
}


static void
ms_systemd_sysupdate_dispose (GObject *object)
{
  MsSystemdSysupdate *self = MS_SYSTEMD_SYSUPDATE (object);

  ms_os_updater_set_latest_update (MS_OS_UPDATER (self), NULL);

  g_cancellable_cancel (self->cancel);
  g_clear_object (&self->cancel);

  g_clear_object (&self->system_bus);
  g_clear_pointer (&self->targets, g_ptr_array_unref);

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


static void
ms_systemd_sysupdate_class_init (MsSystemdSysupdateClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);
  MsOsUpdaterClass *os_updater_class = MS_OS_UPDATER_CLASS (klass);

  object_class->dispose = ms_systemd_sysupdate_dispose;

  os_updater_class->fetch_update = ms_systemd_sysupdate_fetch_update;
  os_updater_class->cancel_fetch_update = ms_systemd_sysupdate_cancel_fetch_update;
  os_updater_class->install_update = ms_systemd_sysupdate_install_update;
  os_updater_class->cancel_install_update = ms_systemd_sysupdate_cancel_install_update;
}


static void
ms_systemd_sysupdate_init (MsSystemdSysupdate *self)
{
  g_autoptr (GError) err = NULL;
  self->cancel = g_cancellable_new ();
  self->system_bus = g_bus_get_sync (MS_DBUS_BUS, NULL, &err);
  self->targets = g_ptr_array_new_with_free_func ((GDestroyNotify)target_destroy);

  if (self->system_bus) {
    g_dbus_connection_call (self->system_bus,
                            "org.freedesktop.DBus",
                            "/org/freedesktop/DBus",
                            "org.freedesktop.DBus",
                            "ListActivatableNames",
                            NULL,
                            (const GVariantType *) "(as)",
                            G_DBUS_CALL_FLAGS_NONE,
                            200, /* ms */
                            self->cancel,
                            on_list_activatable_names_ready,
                            self);
  } else {
    g_debug ("Failed to get system bus: %s", err->message);
  }
}


MsSystemdSysupdate *
ms_systemd_sysupdate_new (void)
{
  return g_object_new (MS_TYPE_SYSTEMD_SYSUPDATE, NULL);
}
