/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
 *
 * Copyright 2025 GNOME Foundation, Inc.
 *
 * SPDX-License-Identifier: LGPL-2.1-or-later
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *
 * Authors:
 *  - Philip Withnall <pwithnall@gnome.org>
 */

#include "config.h"

#include <act/act.h>
#include <glib.h>
#include <glib-object.h>
#include <glib/gi18n-lib.h>
#include <gio/gio.h>
#include <libmalcontent/user-manager.h>

#include "user-private.h"


/**
 * MctUserManager:
 *
 * Gives access to the system user database.
 *
 * Before it can be used, the user data needs to be loaded by calling
 * [method@Malcontent.UserManager.load_async]. After then, changes to the user
 * database are signalled using [signal@Malcontent.UserManager::user-added] and
 * [signal@Malcontent.UserManager::user-removed]. Changes to the properties of
 * individual users are signalled using [signal@GObject.Object::notify] on the
 * user objects.
 *
 * As well as exposing the (non-system) users in the database, the user manager
 * can describe family relationships between the users. Currently, there is only
 * one family supported on the system, containing all the normal and
 * administrator user accounts.
 * [In the future](https://gitlab.gnome.org/Teams/Design/app-mockups/-/issues/118),
 * this may extend to supporting more families, with more roles for members
 * within a family.
 *
 * Since: 0.14.0
 */
struct _MctUserManager
{
  GObject parent_instance;

  GDBusConnection *connection;  /* (owned) */

  ActUserManager *user_manager;  /* (owned) (nullable) */
  unsigned long user_manager_notify_is_loaded_id;
  unsigned long user_added_id;
  unsigned long user_removed_id;
  gboolean is_loaded;
};

G_DEFINE_TYPE (MctUserManager, mct_user_manager, G_TYPE_OBJECT)

typedef enum
{
  PROP_CONNECTION = 1,
  PROP_IS_LOADED,
} MctUserManagerProperty;

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

typedef enum
{
  SIGNAL_USER_ADDED,
  SIGNAL_USER_REMOVED,
} MctUserManagerSignal;

static unsigned int signals[SIGNAL_USER_REMOVED + 1] = { 0, };

static void
mct_user_manager_init (MctUserManager *self)
{
  /* Nothing to do here. */
}

static void
mct_user_manager_get_property (GObject    *object,
                               guint       property_id,
                               GValue     *value,
                               GParamSpec *spec)
{
  MctUserManager *self = MCT_USER_MANAGER (object);

  switch ((MctUserManagerProperty) property_id)
    {
    case PROP_CONNECTION:
      g_value_set_object (value, self->connection);
      break;
    case PROP_IS_LOADED:
      g_value_set_boolean (value, self->is_loaded);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, spec);
      break;
    }
}

static void
mct_user_manager_set_property (GObject      *object,
                               guint         property_id,
                               const GValue *value,
                               GParamSpec   *spec)
{
  MctUserManager *self = MCT_USER_MANAGER (object);

  switch ((MctUserManagerProperty) property_id)
    {
    case PROP_CONNECTION:
      /* Construct-only. May not be %NULL. */
      g_assert (self->connection == NULL);
      self->connection = g_value_dup_object (value);
      g_assert (self->connection != NULL);
      break;
    case PROP_IS_LOADED:
      /* Read only. */
      g_assert_not_reached ();
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, spec);
      break;
    }
}

static void
mct_user_manager_dispose (GObject *object)
{
  MctUserManager *self = MCT_USER_MANAGER (object);

  /* MctUserManager can’t be disposed while load_async() is still in flight */
  g_assert (self->user_manager_notify_is_loaded_id == 0);

  g_clear_signal_handler (&self->user_added_id, self->user_manager);
  g_clear_signal_handler (&self->user_removed_id, self->user_manager);
  g_clear_object (&self->user_manager);

  g_clear_object (&self->connection);

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

static void
mct_user_manager_class_init (MctUserManagerClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->dispose = mct_user_manager_dispose;
  object_class->get_property = mct_user_manager_get_property;
  object_class->set_property = mct_user_manager_set_property;

  /**
   * MctUserManager:connection: (not nullable)
   *
   * A connection to the system bus, where accounts-service runs.
   *
   * It’s provided mostly for testing purposes, or to allow an existing
   * connection to be re-used.
   *
   * Since: 0.14.0
   */
  props[PROP_CONNECTION] = g_param_spec_object ("connection",
                                                NULL, NULL,
                                                G_TYPE_DBUS_CONNECTION,
                                                G_PARAM_READWRITE |
                                                G_PARAM_CONSTRUCT_ONLY |
                                                G_PARAM_STATIC_STRINGS);

  /**
   * MctUserManager:is-loaded:
   *
   * Whether the user manager has finished loading yet.
   *
   * Since: 0.14.0
   */
  props[PROP_IS_LOADED] = g_param_spec_boolean ("is-loaded",
                                                NULL, NULL,
                                                FALSE,
                                                G_PARAM_READABLE |
                                                G_PARAM_STATIC_STRINGS |
                                                G_PARAM_EXPLICIT_NOTIFY);

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

  /**
   * MctUserManager::user-added:
   * @self: a user manager
   * @user: (not nullable): the new user
   *
   * Emitted when a new user is added on the system.
   *
   * Since: 0.14.0
   */
  signals[SIGNAL_USER_ADDED] = g_signal_new ("user-added",
                                             G_TYPE_FROM_CLASS (klass),
                                             G_SIGNAL_RUN_LAST,
                                             0, NULL, NULL, NULL,
                                             G_TYPE_NONE, 1,
                                             MCT_TYPE_USER);

  /**
   * MctUserManager::user-removed:
   * @self: a user manager
   * @user: (not nullable): the old user
   *
   * Emitted when a user is removed from the system.
   *
   * Since: 0.14.0
   */
  signals[SIGNAL_USER_REMOVED] = g_signal_new ("user-removed",
                                               G_TYPE_FROM_CLASS (klass),
                                               G_SIGNAL_RUN_LAST,
                                               0, NULL, NULL, NULL,
                                               G_TYPE_NONE, 1,
                                               MCT_TYPE_USER);
}

/**
 * mct_user_manager_new:
 * @connection: (transfer none): a D-Bus system bus connection to use
 *
 * Create a new user manager.
 *
 * Returns: (transfer full): a new user manager
 * Since: 0.14.0
 */
MctUserManager *
mct_user_manager_new (GDBusConnection *connection)
{
  g_return_val_if_fail (G_IS_DBUS_CONNECTION (connection), NULL);

  return g_object_new (MCT_TYPE_USER_MANAGER,
                       "connection", connection,
                       NULL);
}

static void
user_added_cb (ActUserManager *user_manager,
               ActUser        *act_user,
               void           *user_data)
{
  MctUserManager *self = MCT_USER_MANAGER (user_data);
  g_autoptr(MctUser) user = mct_user_new_from_act_user (act_user);

  g_signal_emit (self, signals[SIGNAL_USER_ADDED], 0, user);
}

static void
user_removed_cb (ActUserManager *user_manager,
                 ActUser        *act_user,
                 void           *user_data)
{
  MctUserManager *self = MCT_USER_MANAGER (user_data);
  g_autoptr(MctUser) user = mct_user_new_from_act_user (act_user);

  g_signal_emit (self, signals[SIGNAL_USER_REMOVED], 0, user);
}

static void user_manager_notify_is_loaded_cb (GObject    *object,
                                              GParamSpec *pspec,
                                              void       *user_data);

static void
object_unref_closure (void     *data,
                      GClosure *closure)
{
  g_object_unref (data);
}

/**
 * mct_user_manager_load_async:
 * @self: a user manager
 * @cancellable: a [class@Gio.Cancellable], or `NULL`,
 * @callback: callback for when the operation is complete
 * @user_data: data to pass to @callback
 *
 * Load the user data from the system.
 *
 * This must be called before the [class@Malcontent.UserManager] can be used.
 *
 * It does not currently support being called more than once.
 *
 * Since: 0.14.0
 */
void
mct_user_manager_load_async (MctUserManager      *self,
                             GCancellable        *cancellable,
                             GAsyncReadyCallback  callback,
                             void                *user_data)
{
  g_autoptr(GTask) task = NULL;
  gboolean is_loaded = FALSE;

  g_return_if_fail (MCT_IS_USER_MANAGER (self));
  g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable));

  /* Doesn’t currently support being called in parallel */
  g_return_if_fail (self->user_manager == NULL);

  task = g_task_new (self, cancellable, callback, user_data);
  g_task_set_source_tag (task, mct_user_manager_load_async);

  /* FIXME: Ideally we would not be using a singleton ActUserManager, and it
   * would use the GDBusConnection in ->connection. Then this class becomes
   * testable by pointing it to a private GDBusConnection. However,
   * libaccountsservice doesn’t currently support that. */
  self->user_manager = g_object_ref (act_user_manager_get_default ());
  self->user_manager_notify_is_loaded_id =
      g_signal_connect_data (self->user_manager, "notify::is-loaded",
                             G_CALLBACK (user_manager_notify_is_loaded_cb),
                             g_object_ref (task), object_unref_closure,
                             G_CONNECT_DEFAULT);

  g_object_get (self->user_manager, "is-loaded", &is_loaded, NULL);
  if (is_loaded)
    user_manager_notify_is_loaded_cb (G_OBJECT (self->user_manager), NULL, task);
  /* otherwise wait for the notify::is-loaded signal */
}

static void
user_manager_notify_is_loaded_cb (GObject    *object,
                                  GParamSpec *pspec,
                                  void       *user_data)
{
  g_autoptr(GTask) task = g_object_ref (G_TASK (user_data));
  MctUserManager *self = g_task_get_source_object (task);
  gboolean is_loaded = FALSE;

  g_object_get (self->user_manager, "is-loaded", &is_loaded, NULL);
  if (!is_loaded)
    return;

  g_clear_signal_handler (&self->user_manager_notify_is_loaded_id, self->user_manager);

  /* Connect to notifications from AccountsService. */
  self->user_added_id = g_signal_connect (self->user_manager, "user-added",
                                          G_CALLBACK (user_added_cb),
                                          self);
  self->user_removed_id = g_signal_connect (self->user_manager, "user-removed",
                                            G_CALLBACK (user_removed_cb),
                                            self);

  self->is_loaded = TRUE;
  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_IS_LOADED]);

  /* Is the service available? */
  if (act_user_manager_no_service (self->user_manager))
    g_task_return_new_error_literal (task, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED,
                                     _("Failed to load user data from the system"));
  else
    g_task_return_boolean (task, TRUE);
}

typedef struct
{
  GPtrArray *users;  /* (owned) (element-type MctUser) */
  unsigned long *is_loaded_ids;  /* (owned) (array length=users->len) */
  size_t n_remaining_not_loaded;

  GCancellable *cancellable;  /* (nullable) (owned) */
  unsigned long cancelled_id;
} EnsureUsersLoadedData;

static void
ensure_users_loaded_data_clear (EnsureUsersLoadedData *data)
{
  g_clear_signal_handler (&data->cancelled_id, data->cancellable);
  g_clear_object (&data->cancellable);

  for (unsigned int i = 0; data->users != NULL && i < data->users->len; i++)
    g_clear_signal_handler (&data->is_loaded_ids[i], data->users->pdata[i]);

  g_clear_pointer (&data->users, g_ptr_array_unref);
  g_clear_pointer (&data->is_loaded_ids, g_free);
}

static void
ensure_users_loaded_data_free (EnsureUsersLoadedData *data)
{
  ensure_users_loaded_data_clear (data);
  g_free (data);
}

G_DEFINE_AUTOPTR_CLEANUP_FUNC (EnsureUsersLoadedData, ensure_users_loaded_data_free)

static void
ensure_users_loaded_maybe_finish (GTask *task)
{
  EnsureUsersLoadedData *data = g_task_get_task_data (task);
  g_autoptr(GError) local_error = NULL;

  if (g_cancellable_set_error_if_cancelled (data->cancellable, &local_error))
    {
      g_task_return_error (task, g_steal_pointer (&local_error));
      ensure_users_loaded_data_clear (data);
    }
  else if (data->n_remaining_not_loaded == 0)
    {
      g_task_return_pointer (task, g_ptr_array_ref (data->users), (GDestroyNotify) g_ptr_array_unref);
      ensure_users_loaded_data_clear (data);
    }
}

static void ensure_users_loaded_cb (GObject    *object,
                                    GParamSpec *pspec,
                                    void       *user_data);
static void ensure_users_loaded_cancelled_cb (GCancellable *cancellable,
                                              void         *user_data);

static void
ensure_users_loaded_async (GPtrArray           *users  /* (element-type MctUser) */,
                           GCancellable        *cancellable,
                           GAsyncReadyCallback  callback,
                           void                *user_data)
{
  g_autoptr(GTask) task = NULL;
  g_autoptr(EnsureUsersLoadedData) data_owned = NULL;
  EnsureUsersLoadedData *data;

  task = g_task_new (NULL, cancellable, callback, user_data);
  g_task_set_source_tag (task, ensure_users_loaded_async);

  data = data_owned = g_new0 (EnsureUsersLoadedData, 1);
  data->users = g_ptr_array_ref (users);
  data->is_loaded_ids = g_new0 (unsigned long, users->len);
  data->cancellable = (cancellable != NULL) ? g_object_ref (cancellable) : NULL;
  data->cancelled_id = g_cancellable_connect (cancellable, G_CALLBACK (ensure_users_loaded_cancelled_cb),
                                              g_object_ref (task), (GDestroyNotify) g_object_unref);

  g_task_set_task_data (task, g_steal_pointer (&data_owned), (GDestroyNotify) ensure_users_loaded_data_free);

  for (unsigned int i = 0; i < users->len; i++)
    {
      MctUser *user_i = MCT_USER (users->pdata[i]);
      gboolean is_loaded = FALSE;

      g_object_get (mct_user_get_act_user (user_i), "is-loaded", &is_loaded, NULL);
      if (!is_loaded)
        {
          data->n_remaining_not_loaded++;
          data->is_loaded_ids[i] =
              g_signal_connect_data (mct_user_get_act_user (user_i), "notify::is-loaded",
                                     G_CALLBACK (ensure_users_loaded_cb),
                                     g_object_ref (task), object_unref_closure,
                                     G_CONNECT_DEFAULT);
        }
    }

  ensure_users_loaded_maybe_finish (task);
}

static gboolean
user_wrapper_equal (const void *a,
                    const void *b)
{
  MctUser *mct_user = MCT_USER ((void *) a);
  ActUser *act_user = ACT_USER ((void *) b);

  return (mct_user_get_act_user (mct_user) == act_user);
}

static void
ensure_users_loaded_cb (GObject    *object,
                        GParamSpec *pspec,
                        void       *user_data)
{
  ActUser *user = ACT_USER (object);
  g_autoptr(GTask) task = g_object_ref (G_TASK (user_data));
  EnsureUsersLoadedData *data = g_task_get_task_data (task);
  gboolean found;
  unsigned int user_index = 0;
  gboolean is_loaded = FALSE;

  g_object_get (user, "is-loaded", &is_loaded, NULL);
  if (!is_loaded)
    return;

  found = g_ptr_array_find_with_equal_func (data->users, user, user_wrapper_equal, &user_index);
  g_assert (found);
  g_clear_signal_handler (&data->is_loaded_ids[user_index], user);

  g_assert (data->n_remaining_not_loaded > 0);
  data->n_remaining_not_loaded--;

  ensure_users_loaded_maybe_finish (task);
}

static void
ensure_users_loaded_cancelled_cb (GCancellable *cancellable,
                                  void         *user_data)
{
  g_autoptr(GTask) task = g_object_ref (G_TASK (user_data));

  ensure_users_loaded_maybe_finish (task);
}

static MctUser **
ensure_users_loaded_finish (GAsyncResult  *result,
                            size_t        *out_len,
                            GError       **error)
{
  g_autoptr(GPtrArray) users = NULL;

  g_return_val_if_fail (g_task_is_valid (result, NULL), NULL);
  g_return_val_if_fail (error == NULL || *error == NULL, NULL);

  users = g_task_propagate_pointer (G_TASK (result), error);

  if (users == NULL)
    return NULL;

  if (out_len != NULL)
    *out_len = users->len;

  return (MctUser **) g_ptr_array_free (g_steal_pointer (&users), FALSE);
}

static GPtrArray *  /* (element-type MctUser) */
ensure_users_loaded_finish_array (GAsyncResult  *result,
                                  GError       **error)
{
  g_return_val_if_fail (g_task_is_valid (result, NULL), NULL);
  g_return_val_if_fail (error == NULL || *error == NULL, NULL);

  return g_task_propagate_pointer (G_TASK (result), error);
}

/**
 * mct_user_manager_load_finish:
 * @self: a user manager
 * @result: result of the asynchronous operation
 * @error: return location for a [type@GLib.Error], or `NULL`
 *
 * Finish an asynchronous load operation started with
 * [method@Malcontent.UserManager.load_async].
 *
 * Returns: true on success, false otherwise
 * Since: 0.14.0
 */
gboolean
mct_user_manager_load_finish (MctUserManager  *self,
                              GAsyncResult    *result,
                              GError         **error)
{
  g_return_val_if_fail (MCT_IS_USER_MANAGER (self), FALSE);
  g_return_val_if_fail (g_task_is_valid (result, self), FALSE);
  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

  return g_task_propagate_boolean (G_TASK (result), error);
}

/**
 * mct_user_manager_get_is_loaded:
 * @self: a user manager
 *
 * Get the value of [property@Malcontent.UserManager:is-loaded].
 *
 * It is safe to call this before [method@Malcontent.UserManager.load_async] is
 * finished.
 *
 * Returns: true if the user manager is loaded, false otherwise
 * Since: 0.14.0
 */
gboolean
mct_user_manager_get_is_loaded (MctUserManager *self)
{
  g_return_val_if_fail (MCT_IS_USER_MANAGER (self), FALSE);

  return self->is_loaded;
}

static void get_user_cb (GObject      *object,
                         GAsyncResult *result,
                         void         *user_data);

/**
 * mct_user_manager_get_user_by_uid_async:
 * @self: a user manager
 * @uid: user ID to fetch
 * @cancellable: a [class@Gio.Cancellable], or `NULL`
 * @callback: callback for when the operation is complete
 * @user_data: data to pass to @callback
 *
 * Get the user with the given @uid.
 *
 * It is not safe to call this before [method@Malcontent.UserManager.load_async]
 * is finished.
 *
 * Since: 0.14.0
 */
void
mct_user_manager_get_user_by_uid_async (MctUserManager      *self,
                                        uid_t                uid,
                                        GCancellable        *cancellable,
                                        GAsyncReadyCallback  callback,
                                        void                *user_data)
{
  g_autoptr(GTask) task = NULL;
  ActUser *user;
  g_autoptr(GPtrArray) users = NULL;

  g_return_if_fail (MCT_IS_USER_MANAGER (self));
  g_return_if_fail (uid != (uid_t) -1);
  g_return_if_fail (self->is_loaded);
  g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable));

  task = g_task_new (self, cancellable, callback, user_data);
  g_task_set_source_tag (task, mct_user_manager_get_user_by_uid_async);

  user = act_user_manager_get_user_by_id (self->user_manager, uid);
  users = g_ptr_array_new_null_terminated (1, g_object_unref, TRUE);
  g_ptr_array_add (users, mct_user_new_from_act_user (user));
  ensure_users_loaded_async (users, cancellable, get_user_cb, g_steal_pointer (&task));
}

static void
get_user_cb (GObject      *object,
             GAsyncResult *result,
             void         *user_data)
{
  g_autoptr(GTask) task = g_steal_pointer (&user_data);
  g_autoptr(GError) local_error = NULL;
  g_autoptr(MctUserArray) users = NULL;
  size_t n_users = 0;

  users = ensure_users_loaded_finish (result, &n_users, &local_error);
  g_assert (local_error != NULL || n_users == 1);

  if (local_error == NULL)
    g_task_return_pointer (task, g_object_ref (users[0]), g_object_unref);
  else
    g_task_return_error (task, g_steal_pointer (&local_error));
}

/**
 * mct_user_manager_get_user_by_uid_finish:
 * @self: a user manager
 * @result: result of the asynchronous operation
 * @error: return location for a [type@GLib.Error], or `NULL`
 *
 * Finish an asynchronous query operation started with
 * [method@Malcontent.UserManager.get_user_by_uid_async].
 *
 * Returns: (transfer full) (nullable): the user, or `NULL` if not found
 * Since: 0.14.0
 */
MctUser *
mct_user_manager_get_user_by_uid_finish (MctUserManager  *self,
                                         GAsyncResult    *result,
                                         GError         **error)
{
  g_return_val_if_fail (MCT_IS_USER_MANAGER (self), NULL);
  g_return_val_if_fail (g_task_is_valid (result, self), NULL);
  g_return_val_if_fail (error == NULL || *error == NULL, NULL);

  return g_task_propagate_pointer (G_TASK (result), error);
}

/**
 * mct_user_manager_get_user_by_username_async:
 * @self: a user manager
 * @username: username to fetch
 * @cancellable: a [class@Gio.Cancellable], or `NULL`
 * @callback: callback for when the operation is complete
 * @user_data: data to pass to @callback
 *
 * Get the user with the given @username.
 *
 * It is not safe to call this before [method@Malcontent.UserManager.load_async]
 * is finished.
 *
 * Since: 0.14.0
 */
void
mct_user_manager_get_user_by_username_async (MctUserManager      *self,
                                             const char          *username,
                                             GCancellable        *cancellable,
                                             GAsyncReadyCallback  callback,
                                             void                *user_data)
{
  g_autoptr(GTask) task = NULL;
  ActUser *user;
  g_autoptr(GPtrArray) users = NULL;

  g_return_if_fail (MCT_IS_USER_MANAGER (self));
  g_return_if_fail (username != NULL);
  g_return_if_fail (self->is_loaded);
  g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable));

  task = g_task_new (self, cancellable, callback, user_data);
  g_task_set_source_tag (task, mct_user_manager_get_user_by_username_async);

  user = act_user_manager_get_user (self->user_manager, username);
  users = g_ptr_array_new_null_terminated (1, g_object_unref, TRUE);
  g_ptr_array_add (users, mct_user_new_from_act_user (user));
  ensure_users_loaded_async (users, cancellable, get_user_cb, g_steal_pointer (&task));
}

/**
 * mct_user_manager_get_user_by_username_finish:
 * @self: a user manager
 * @result: result of the asynchronous operation
 * @error: return location for a [type@GLib.Error], or `NULL`
 *
 * Finish an asynchronous query operation started with
 * [method@Malcontent.UserManager.get_user_by_username_async].
 *
 * Returns: (transfer full) (nullable): the user, or `NULL` if not found
 * Since: 0.14.0
 */
MctUser *
mct_user_manager_get_user_by_username_finish (MctUserManager  *self,
                                              GAsyncResult    *result,
                                              GError         **error)
{
  g_return_val_if_fail (MCT_IS_USER_MANAGER (self), NULL);
  g_return_val_if_fail (g_task_is_valid (result, self), NULL);
  g_return_val_if_fail (error == NULL || *error == NULL, NULL);

  return g_task_propagate_pointer (G_TASK (result), error);
}

static void get_users_cb (GObject      *object,
                          GAsyncResult *result,
                          void         *user_data);

/**
 * mct_user_manager_get_family_members_for_user_async:
 * @self: a user manager
 * @user: (transfer none) (not nullable): user to get the family members for
 * @cancellable: a [class@Gio.Cancellable], or `NULL`
 * @callback: callback for when the operation is complete
 * @user_data: data to pass to @callback
 *
 * Get the members of the family containing @user.
 *
 * @user is returned in the result set.
 *
 * Since: 0.14.0
 */
void
mct_user_manager_get_family_members_for_user_async (MctUserManager      *self,
                                                    MctUser             *user,
                                                    GCancellable        *cancellable,
                                                    GAsyncReadyCallback  callback,
                                                    void                *user_data)
{
  g_autoptr(GSList) users = NULL;
  g_autoptr(GPtrArray) family = NULL;  /* (element-type MctUser) */
  g_autoptr(GTask) task = NULL;

  g_return_if_fail (MCT_IS_USER_MANAGER (self));
  g_return_if_fail (MCT_IS_USER (user));
  g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable));
  g_return_if_fail (self->is_loaded);

  task = g_task_new (self, cancellable, callback, user_data);
  g_task_set_source_tag (task, mct_user_manager_get_family_members_for_user_async);

  /* Build the family list */
  users = act_user_manager_list_users (self->user_manager);
  family = g_ptr_array_new_null_terminated (g_slist_length (users), g_object_unref, TRUE);

  for (GSList *l = users; l != NULL; l = l->next)
    {
      g_autoptr(MctUser) l_user = mct_user_new_from_act_user (l->data);

      if (mct_user_is_in_same_family (l_user, user))
        g_ptr_array_add (family, g_steal_pointer (&l_user));
    }

  /* Ensure they’re all loaded */
  ensure_users_loaded_async (family, cancellable, get_users_cb, g_steal_pointer (&task));
}

static void
get_users_cb (GObject      *object,
              GAsyncResult *result,
              void         *user_data)
{
  g_autoptr(GTask) task = g_steal_pointer (&user_data);
  g_autoptr(GError) local_error = NULL;
  g_autoptr(GPtrArray) users = NULL;  /* (element-type MctUser) */

  users = ensure_users_loaded_finish_array (result, &local_error);

  if (local_error == NULL)
    g_task_return_pointer (task, g_steal_pointer (&users), (GDestroyNotify) g_ptr_array_unref);
  else
    g_task_return_error (task, g_steal_pointer (&local_error));
}

/**
 * mct_user_manager_get_family_members_for_user_finish:
 * @self: a user manager
 * @result: result of the asynchronous operation
 * @out_len: (out) (optional): return location for the array length of the
 *   return value
 * @error: return location for a [type@GLib.Error], or `NULL`
 *
 * Finish an asynchronous query operation started with
 * [method@Malcontent.UserManager.get_family_members_for_user_async].
 *
 * Returns: (array length=out_len) (transfer full) (not nullable): array of
 *   family members in an undefined order, may be empty
 * Since: 0.14.0
 */
MctUser **
mct_user_manager_get_family_members_for_user_finish (MctUserManager  *self,
                                                     GAsyncResult    *result,
                                                     size_t          *out_len,
                                                     GError         **error)
{
  g_autoptr(GPtrArray) family = NULL;

  g_return_val_if_fail (MCT_IS_USER_MANAGER (self), NULL);
  g_return_val_if_fail (g_task_is_valid (result, self), NULL);
  g_return_val_if_fail (error == NULL || *error == NULL, NULL);

  family = g_task_propagate_pointer (G_TASK (result), error);

  if (family == NULL)
    return NULL;

  if (out_len != NULL)
    *out_len = family->len;

  return (MctUser **) g_ptr_array_free (g_steal_pointer (&family), FALSE);
}

/**
 * mct_user_manager_get_all_users_async:
 * @self: a user manager
 * @cancellable: a [class@Gio.Cancellable], or `NULL`
 * @callback: callback for when the operation is complete
 * @user_data: data to pass to @callback
 *
 * Get all the non-system users on the system.
 *
 * Since: 0.14.0
 */
void
mct_user_manager_get_all_users_async (MctUserManager      *self,
                                      GCancellable        *cancellable,
                                      GAsyncReadyCallback  callback,
                                      void                *user_data)
{
  g_autoptr(GSList) users = NULL;
  g_autoptr(GPtrArray) users_array = NULL;  /* (element-type MctUser) */
  g_autoptr(GTask) task = NULL;

  g_return_if_fail (MCT_IS_USER_MANAGER (self));
  g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable));
  g_return_if_fail (self->is_loaded);

  task = g_task_new (self, cancellable, callback, user_data);
  g_task_set_source_tag (task, mct_user_manager_get_all_users_async);

  /* Build the user list */
  users = act_user_manager_list_users (self->user_manager);
  users_array = g_ptr_array_new_null_terminated (g_slist_length (users), g_object_unref, TRUE);

  for (GSList *l = users; l != NULL; l = l->next)
    g_ptr_array_add (users_array, mct_user_new_from_act_user (l->data));

  /* Ensure they’re all loaded */
  ensure_users_loaded_async (users_array, cancellable, get_users_cb, g_steal_pointer (&task));
}

/**
 * mct_user_manager_get_all_users_finish:
 * @self: a user manager
 * @result: result of the asynchronous operation
 * @out_len: (out) (optional): return location for the array length of the
 *   return value
 * @error: return location for a [type@GLib.Error], or `NULL`
 *
 * Finish an asynchronous query operation started with
 * [method@Malcontent.UserManager.get_all_users_async].
 *
 * Returns: (array length=out_len) (transfer full) (not nullable): array of
 *   users in an undefined order, may be empty
 * Since: 0.14.0
 */
MctUser **
mct_user_manager_get_all_users_finish (MctUserManager  *self,
                                       GAsyncResult    *result,
                                       size_t          *out_len,
                                       GError         **error)
{
  g_autoptr(GPtrArray) users = NULL;

  g_return_val_if_fail (MCT_IS_USER_MANAGER (self), NULL);
  g_return_val_if_fail (g_task_is_valid (result, self), NULL);
  g_return_val_if_fail (error == NULL || *error == NULL, NULL);

  users = g_task_propagate_pointer (G_TASK (result), error);

  if (users == NULL)
    return NULL;

  if (out_len != NULL)
    *out_len = users->len;

  return (MctUser **) g_ptr_array_free (g_steal_pointer (&users), FALSE);
}
