/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
/*
 * Copyright © 2011 – 2017 Red Hat, Inc.
 *
 * 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 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, see <http://www.gnu.org/licenses/>.
 */

#include "config.h"

#include <errno.h>
#include <string.h>

#include <gio/gio.h>
#include <glib/gi18n.h>
#include <rest/rest-proxy.h>
#include <libsoup/soup.h>

#include "goadaemon.h"
#include "goa/goa.h"
#include "goabackend/goabackend.h"
#include "goabackend/goaprovider-priv.h"
#include "goabackend/goautils.h"

struct _GoaDaemon
{
  GObject parent_instance;

  GDBusConnection *connection;

  GFileMonitor *home_conf_file_monitor;
  GFileMonitor *template_file_monitor;
  gchar *home_conf_file_path;

  GNetworkMonitor *network_monitor;

  GDBusObjectManagerServer *object_manager;

  GoaManager *manager;

  GQueue *ensure_credentials_queue;
  gboolean ensure_credentials_running;

  guint config_timeout_id;
  guint credentials_timeout_id;
};

enum
{
  PROP_0,
  PROP_CONNECTION
};

static void on_file_monitor_changed (GFileMonitor     *monitor,
                                     GFile            *file,
                                     GFile            *other_file,
                                     GFileMonitorEvent event_type,
                                     gpointer          user_data);

static gboolean on_manager_handle_add_account (GoaManager            *object,
                                               GDBusMethodInvocation *invocation,
                                               const gchar           *provider_type,
                                               const gchar           *identity,
                                               const gchar           *presentation_identity,
                                               GVariant              *credentials,
                                               GVariant              *details,
                                               gpointer               user_data);

static gboolean on_account_handle_remove (GoaAccount            *account,
                                          GDBusMethodInvocation *invocation,
                                          gpointer               user_data);

static gboolean on_account_handle_ensure_credentials (GoaAccount            *account,
                                                      GDBusMethodInvocation *invocation,
                                                      gpointer               user_data);

static void ensure_credentials_queue_check (GoaDaemon *self);

static void goa_daemon_check_credentials (GoaDaemon *self);
static void goa_daemon_reload_configuration (GoaDaemon *self);

G_DEFINE_TYPE (GoaDaemon, goa_daemon, G_TYPE_OBJECT);

/* ---------------------------------------------------------------------------------------------------- */

typedef struct
{
  GError **error;
  GList **out_providers;
  GMainLoop *loop;
  gboolean op_res;
} GetAllSyncData;

static void
get_all_providers_sync_cb (GObject       *source_object,
                           GAsyncResult  *res,
                           gpointer       user_data)
{
  GetAllSyncData *data = (GetAllSyncData *) user_data;

  data->op_res = goa_provider_get_all_finish (data->out_providers, res, data->error);
  g_main_loop_quit (data->loop);
}

static gboolean
get_all_providers_sync (GCancellable  *cancellable,
                        GList        **out_providers,
                        GError       **error)
{
  GetAllSyncData data;

  data.error = error;
  data.out_providers = out_providers;

  /* HACK: Since telepathy-glib doesn't use the thread-default
   * GMainContext for invoking the asynchronous callbacks, we can't
   * push a new GMainContext here.
   */
  data.loop = g_main_loop_new (NULL, FALSE);

  goa_provider_get_all (get_all_providers_sync_cb, &data);
  g_main_loop_run (data.loop);
  g_main_loop_unref (data.loop);

  return data.op_res;
}

/* ---------------------------------------------------------------------------------------------------- */

static void
goa_daemon_constructed (GObject *object)
{
  GoaDaemon *self = GOA_DAEMON (object);

  G_OBJECT_CLASS (goa_daemon_parent_class)->constructed (object);

  /* prime the list of accounts */
  goa_daemon_reload_configuration (self);

  /* Export objects */
  g_dbus_object_manager_server_set_connection (self->object_manager, self->connection);
}

static void
goa_daemon_finalize (GObject *object)
{
  GoaDaemon *self = GOA_DAEMON (object);

  if (self->config_timeout_id != 0)
    {
      g_source_remove (self->config_timeout_id);
    }

  if (self->credentials_timeout_id != 0)
    {
      g_source_remove (self->credentials_timeout_id);
    }

  if (self->home_conf_file_monitor != NULL)
    {
      g_signal_handlers_disconnect_by_func (self->home_conf_file_monitor, on_file_monitor_changed, self);
      g_object_unref (self->home_conf_file_monitor);
    }

  if (self->template_file_monitor != NULL)
    {
      g_signal_handlers_disconnect_by_func (self->template_file_monitor, on_file_monitor_changed, self);
      g_object_unref (self->template_file_monitor);
    }

  g_free (self->home_conf_file_path);
  g_object_unref (self->manager);
  g_object_unref (self->object_manager);
  g_object_unref (self->connection);
  g_queue_free_full (self->ensure_credentials_queue, g_object_unref);

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

static void
goa_daemon_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
{
  GoaDaemon *self = GOA_DAEMON (object);

  switch (prop_id)
    {
    case PROP_CONNECTION:
      self->connection = G_DBUS_CONNECTION (g_value_dup_object (value));
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
    }
}

static GFileMonitor *
create_monitor (const gchar *path, gboolean is_dir)
{
  GFile *file;
  GFileMonitor *monitor;
  GError *error;

  error = NULL;
  file = g_file_new_for_path (path);
  if (is_dir)
    monitor = g_file_monitor_directory (file, G_FILE_MONITOR_NONE, NULL, &error);
  else
    monitor = g_file_monitor_file (file, G_FILE_MONITOR_NONE, NULL, &error);

  if (monitor == NULL)
    {
      g_warning ("Error monitoring %s at %s: %s (%s, %d)",
                 is_dir ? "directory" : "file",
                 path,
                 error->message, g_quark_to_string (error->domain), error->code);
      g_error_free (error);
    }
  g_object_unref (file);

  return monitor;
}

static gboolean
on_config_file_monitor_timeout (gpointer user_data)
{
  GoaDaemon *self = GOA_DAEMON (user_data);

  self->config_timeout_id = 0;
  g_debug ("Reloading configuration files");
  goa_daemon_reload_configuration (self);

  return G_SOURCE_REMOVE;
}

static void
on_file_monitor_changed (GFileMonitor      *monitor,
                         GFile             *file,
                         GFile             *other_file,
                         GFileMonitorEvent  event_type,
                         gpointer           user_data)
{
  GoaDaemon *self = GOA_DAEMON (user_data);

  if (self->config_timeout_id == 0)
    {
      self->config_timeout_id = g_timeout_add (200, on_config_file_monitor_timeout, self);
    }
}

static gboolean
on_check_credentials_timeout (gpointer user_data)
{
  GoaDaemon *self = GOA_DAEMON (user_data);

  self->credentials_timeout_id = 0;
  g_debug ("Calling EnsureCredentials due to network changes");
  goa_daemon_check_credentials (self);

  return G_SOURCE_REMOVE;
}

static void
queue_check_credentials (GoaDaemon *self)
{
  if (self->credentials_timeout_id != 0)
    {
      g_source_remove (self->credentials_timeout_id);
    }

  self->credentials_timeout_id = g_timeout_add_seconds (1, on_check_credentials_timeout, self);
}

static void
on_network_monitor_network_changed (GoaDaemon *self, gboolean available)
{
  queue_check_credentials (self);
}

static void
goa_daemon_init (GoaDaemon *self)
{
  static volatile GQuark goa_error_domain = 0;
  GError *error;
  GList *l;
  GList *providers = NULL;
  GoaObjectSkeleton *object;
  gchar *path;

  /* this will force associating errors in the GOA_ERROR error domain
   * with org.freedesktop.Goa.Error.* errors via g_dbus_error_register_error_domain().
   */
  goa_error_domain = GOA_ERROR;
  goa_error_domain; /* shut up -Wunused-but-set-variable */

  error = NULL;
  if (!get_all_providers_sync (NULL, &providers, &error))
    {
      g_warning ("Unable to get the list of providers: %s (%s, %d)",
                 error->message,
                 g_quark_to_string (error->domain),
                 error->code);
      g_error_free (error);
    }

  for (l = providers; l != NULL; l = l->next)
    {
      GoaProvider *provider = GOA_PROVIDER (l->data);
      goa_provider_initialize (provider);
    }

  /* Create object manager */
  self->object_manager = g_dbus_object_manager_server_new ("/org/gnome/OnlineAccounts");

  /* Create and export Manager */
  self->manager = goa_manager_skeleton_new ();
  g_signal_connect (self->manager,
                    "handle-add-account",
                    G_CALLBACK (on_manager_handle_add_account),
                    self);
  object = goa_object_skeleton_new ("/org/gnome/OnlineAccounts/Manager");
  goa_object_skeleton_set_manager (object, self->manager);
  g_dbus_object_manager_server_export (self->object_manager, G_DBUS_OBJECT_SKELETON (object));
  g_object_unref (object);

  self->home_conf_file_path = g_strdup_printf ("%s/goa-1.0/accounts.conf", g_get_user_config_dir ());

  /* create ~/.config/goa-1.0 directory */
  path = g_path_get_dirname (self->home_conf_file_path);
  if (g_mkdir_with_parents (path, 0755) != 0)
    {
      g_warning ("Error creating directory %s: %s", path, strerror (errno));
    }
  g_free (path);

  /* set up file monitoring */
  self->home_conf_file_monitor = create_monitor (self->home_conf_file_path, FALSE);
  if (self->home_conf_file_monitor != NULL)
    g_signal_connect (self->home_conf_file_monitor, "changed", G_CALLBACK (on_file_monitor_changed), self);

  if (GOA_TEMPLATE_FILE != NULL && GOA_TEMPLATE_FILE[0] != '\0')
    {
      self->template_file_monitor = create_monitor (GOA_TEMPLATE_FILE, FALSE);
      if (self->template_file_monitor != NULL)
        g_signal_connect (self->template_file_monitor, "changed", G_CALLBACK (on_file_monitor_changed), self);
    }

  self->network_monitor = g_network_monitor_get_default ();
  g_signal_connect_object (self->network_monitor,
                           "network-changed",
                           G_CALLBACK (on_network_monitor_network_changed),
                           self,
                           G_CONNECT_SWAPPED);

  self->ensure_credentials_queue = g_queue_new ();
  queue_check_credentials (self);

  g_list_free_full (providers, g_object_unref);
}

static void
goa_daemon_class_init (GoaDaemonClass *klass)
{
  GObjectClass *gobject_class;

  gobject_class = G_OBJECT_CLASS (klass);
  gobject_class->constructed  = goa_daemon_constructed;
  gobject_class->finalize     = goa_daemon_finalize;
  gobject_class->set_property = goa_daemon_set_property;

  g_object_class_install_property (gobject_class,
                                   PROP_CONNECTION,
                                   g_param_spec_object ("connection",
                                                        "GDBusConnection object",
                                                        "A connection to a message bus",
                                                        G_TYPE_DBUS_CONNECTION,
                                                        G_PARAM_CONSTRUCT_ONLY |
                                                        G_PARAM_STATIC_STRINGS |
                                                        G_PARAM_WRITABLE));
}

GoaDaemon *
goa_daemon_new (GDBusConnection *connection)
{
  g_return_val_if_fail (G_IS_DBUS_CONNECTION (connection), NULL);
  return GOA_DAEMON (g_object_new (GOA_TYPE_DAEMON, "connection", connection, NULL));
}


/* ---------------------------------------------------------------------------------------------------- */

static void
diff_sorted_lists (GList *list1,
                   GList *list2,
                   GCompareFunc compare,
                   GList **added,
                   GList **removed,
                   GList **unchanged)
{
  gint order;

  *added = *removed = *unchanged = NULL;

  while (list1 != NULL && list2 != NULL)
    {
      order = (*compare) (list1->data, list2->data);
      if (order < 0)
        {
          *removed = g_list_prepend (*removed, list1->data);
          list1 = list1->next;
        }
      else if (order > 0)
        {
          *added = g_list_prepend (*added, list2->data);
          list2 = list2->next;
        }
      else
        { /* same item */
          *unchanged = g_list_prepend (*unchanged, list1->data);
          list1 = list1->next;
          list2 = list2->next;
        }
    }

  while (list1 != NULL)
    {
      *removed = g_list_prepend (*removed, list1->data);
      list1 = list1->next;
    }
  while (list2 != NULL)
    {
      *added = g_list_prepend (*added, list2->data);
      list2 = list2->next;
    }
}

/* ---------------------------------------------------------------------------------------------------- */

static const gchar *
account_group_to_id (const gchar *group)
{
  g_return_val_if_fail (g_str_has_prefix (group, "Account "), NULL);
  return group + sizeof "Account " - 1;
}

static gchar *
account_object_path_to_group (const gchar *object_path)
{
  g_return_val_if_fail (g_str_has_prefix (object_path, "/org/gnome/OnlineAccounts/Accounts/"), NULL);
  return g_strdup_printf ("Account %s", object_path + sizeof "/org/gnome/OnlineAccounts/Accounts/" - 1);
}

static const gchar *
template_group_to_id (const gchar *group)
{
  g_return_val_if_fail (g_str_has_prefix (group, "Template "), NULL);
  return group + sizeof "Template " - 1;
}

/* ---------------------------------------------------------------------------------------------------- */

typedef struct
{
  GKeyFile *key_file;
  gchar *path;
} KeyFileData;

static void
key_file_data_free (KeyFileData *data)
{
  g_key_file_unref (data->key_file);
  g_free (data->path);
  g_slice_free (KeyFileData, data);
}

static KeyFileData *
key_file_data_new (GKeyFile    *key_file,
                   const gchar *path)
{
  KeyFileData *data;
  data = g_slice_new (KeyFileData);
  data->key_file = g_key_file_ref (key_file);
  data->path = g_strdup (path);
  return data;
}

/* ---------------------------------------------------------------------------------------------------- */

static void
add_config_file (GoaDaemon     *self,
                 const gchar   *path,
                 GHashTable    *group_name_to_key_file_data)
{
  GKeyFile *key_file;
  GError *error;
  gboolean needs_update = FALSE;
  gchar **groups;
  const char *guid;
  gsize num_groups;
  guint n;

  key_file = g_key_file_new ();

  error = NULL;
  if (!g_key_file_load_from_file (key_file,
                                  path,
                                  G_KEY_FILE_NONE,
                                  &error))
    {
      if (!g_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_NOENT))
        {
          g_warning ("Error loading %s: %s (%s, %d)",
                     path,
                     error->message, g_quark_to_string (error->domain), error->code);
        }
      g_error_free (error);
      goto out;
    }

  guid = g_dbus_connection_get_guid (self->connection);
  groups = g_key_file_get_groups (key_file, &num_groups);
  for (n = 0; n < num_groups; n++)
    {
      if (g_str_has_prefix (groups[n], "Account "))
        {
          gboolean is_temporary;
          char *session_id;

          is_temporary = g_key_file_get_boolean (key_file,
                                                 groups[n],
                                                 "IsTemporary",
                                                 NULL);

          if (is_temporary)
            {
              session_id = g_key_file_get_string (key_file,
                                                  groups[n],
                                                  "SessionId",
                                                  NULL);

              /* discard temporary accounts from older sessions */
              if (session_id != NULL &&
                  g_strcmp0 (session_id, guid) != 0)
                {
                  GoaProvider *provider = NULL;
                  const gchar *id;
                  gchar *provider_type = NULL;

                  g_debug ("ignoring account \"%s\" in file %s because it's stale",
                           groups[n], path);

                  id = account_group_to_id (groups[n]);
                  if (id == NULL)
                    {
                      g_warning ("Unable to get account ID from group: %s", groups[n]);
                      goto cleanup_and_continue;
                    }

                  provider_type = g_key_file_get_string (key_file, groups[n], "Provider", NULL);
                  if (provider_type != NULL)
                    provider = goa_provider_get_for_provider_type (provider_type);

                  if (provider == NULL)
                    {
                      g_warning ("Unsupported account type %s for ID %s (no provider)", provider_type, id);
                      goto cleanup_and_continue;
                    }

                  needs_update = g_key_file_remove_group (key_file, groups[n], NULL);

                  error = NULL;
                  if (!goa_utils_delete_credentials_for_id_sync (provider, id, NULL, &error))
                    {
                      g_warning ("Unable to clean-up stale keyring entries: %s", error->message);
                      g_error_free (error);
                      goto cleanup_and_continue;
                    }

                cleanup_and_continue:
                  g_clear_object (&provider);
                  g_free (groups[n]);
                  g_free (provider_type);
                  g_free (session_id);
                  continue;
                }
              g_free (session_id);
            }
          else
            {
              needs_update = g_key_file_remove_key (key_file, groups[n], "SessionId", NULL);
            }

          g_hash_table_insert (group_name_to_key_file_data,
                               groups[n], /* steals string */
                               key_file_data_new (key_file, path));
        }
      else if (g_str_has_prefix (groups[n], "Template "))
        {
          g_hash_table_insert (group_name_to_key_file_data,
                               groups[n], /* steals string */
                               key_file_data_new (key_file, path));
        }
      else
        {
          g_warning ("Unexpected group \"%s\" in file %s", groups[n], path);
          g_free (groups[n]);
        }
    }
  g_free (groups);

  if (needs_update)
    {
      error = NULL;
      if (!g_key_file_save_to_file (key_file, path, &error))
        {
          g_prefix_error (&error, "Error writing key-value-file %s: ", path);
          g_warning ("%s (%s, %d)", error->message, g_quark_to_string (error->domain), error->code);
          g_error_free (error);
        }
    }

 out:
  g_key_file_unref (key_file);
}

/* ---------------------------------------------------------------------------------------------------- */

/* returns FALSE if object is not (or no longer) valid */
static gboolean
update_account_object (GoaDaemon           *self,
                       GoaObjectSkeleton   *object,
                       const gchar         *path,
                       const gchar         *group,
                       GKeyFile            *key_file,
                       gboolean             just_added)
{
  GoaAccount *account = NULL;
  GoaProvider *provider = NULL;
  gboolean is_locked;
  gboolean is_temporary;
  gboolean ret = FALSE;
  gchar *identity = NULL;
  gchar *presentation_identity;
  gchar *type = NULL;
  gchar *name = NULL;
  GIcon *icon = NULL;
  gchar *serialized_icon = NULL;
  GError *error;

  g_return_val_if_fail (GOA_IS_DAEMON (self), FALSE);
  g_return_val_if_fail (G_IS_DBUS_OBJECT_SKELETON (object), FALSE);
  g_return_val_if_fail (group != NULL, FALSE);
  g_return_val_if_fail (key_file != NULL, FALSE);

  g_debug ("updating %s %d", g_dbus_object_get_object_path (G_DBUS_OBJECT (object)), just_added);

  type = g_key_file_get_string (key_file, group, "Provider", NULL);
  identity = g_key_file_get_string (key_file, group, "Identity", NULL);
  presentation_identity = g_key_file_get_string (key_file, group, "PresentationIdentity", NULL);
  is_locked = g_key_file_get_boolean (key_file, group, "IsLocked", NULL);
  is_temporary = g_key_file_get_boolean (key_file, group, "IsTemporary", NULL);
  if (just_added)
    {
      account = goa_account_skeleton_new ();
      goa_object_skeleton_set_account (object, account);
    }
  else
    {
      account = goa_object_get_account (GOA_OBJECT (object));
    }

  provider = goa_provider_get_for_provider_type (type);
  if (provider == NULL)
    {
      g_warning ("Unsupported account type %s for identity %s (no provider)", type, identity);
      goto out;
    }

  goa_account_set_id (account, g_strrstr (g_dbus_object_get_object_path (G_DBUS_OBJECT (object)), "/") + 1);
  goa_account_set_provider_type (account, type);
  goa_account_set_identity (account, identity);
  goa_account_set_presentation_identity (account, presentation_identity);
  goa_account_set_is_locked (account, is_locked);
  goa_account_set_is_temporary (account, is_temporary);

  error = NULL;
  if (!goa_provider_build_object (provider, object, key_file, group, self->connection, just_added, &error))
    {
      g_warning ("Error parsing account: %s (%s, %d)",
                 error->message, g_quark_to_string (error->domain), error->code);
      g_error_free (error);
      goto out;
    }

  name = goa_provider_get_provider_name (provider, GOA_OBJECT (object));
  goa_account_set_provider_name (account, name);

  icon = goa_provider_get_provider_icon (provider, GOA_OBJECT (object));
  serialized_icon = g_icon_to_string (icon);
  goa_account_set_provider_icon (account, serialized_icon);

  ret = TRUE;

 out:
  g_free (serialized_icon);
  g_clear_object (&icon);
  g_free (name);
  g_clear_object (&provider);
  g_clear_object (&account);
  g_free (type);
  g_free (identity);
  g_free (presentation_identity);
  return ret;
}

static void
process_config_entries (GoaDaemon  *self,
                        GHashTable *group_name_to_key_file_data)
{
  GHashTableIter iter;
  const gchar *group;
  KeyFileData *key_file_data;
  GList *existing_object_paths = NULL;
  GList *config_object_paths = NULL;
  GList *added;
  GList *removed;
  GList *unchanged;
  GList *l;

  {
    GList *existing_objects;
    existing_objects = g_dbus_object_manager_get_objects (G_DBUS_OBJECT_MANAGER (self->object_manager));
    for (l = existing_objects; l != NULL; l = l->next)
      {
        GoaObject *object = GOA_OBJECT (l->data);
        const gchar *object_path;
        object_path = g_dbus_object_get_object_path (G_DBUS_OBJECT (object));
        if (g_str_has_prefix (object_path, "/org/gnome/OnlineAccounts/Accounts/"))
          existing_object_paths = g_list_prepend (existing_object_paths, g_strdup (object_path));
      }
    g_list_free_full (existing_objects, g_object_unref);
  }

  g_hash_table_iter_init (&iter, group_name_to_key_file_data);
  while (g_hash_table_iter_next (&iter, (gpointer*) &group, (gpointer*) &key_file_data))
    {
      const gchar *id;
      gchar *object_path;

      if (!g_str_has_prefix (group, "Account "))
        continue;

      id = account_group_to_id (group);

      /* create and validate object path */
      object_path = g_strdup_printf ("/org/gnome/OnlineAccounts/Accounts/%s", id);
      if (strstr (id, "/") != NULL || !g_variant_is_object_path (object_path))
        {
          g_warning ("`%s' is not a valid account identifier", group);
          g_free (object_path);
          continue;
        }
      /* steals object_path variable */
      config_object_paths = g_list_prepend (config_object_paths, object_path);
    }

  existing_object_paths = g_list_sort (existing_object_paths, (GCompareFunc) g_strcmp0);
  config_object_paths = g_list_sort (config_object_paths, (GCompareFunc) g_strcmp0);
  diff_sorted_lists (existing_object_paths,
                     config_object_paths,
                     (GCompareFunc) g_strcmp0,
                     &added,
                     &removed,
                     &unchanged);

  for (l = removed; l != NULL; l = l->next)
    {
      const gchar *object_path = l->data;
      GoaObject *object;
      object = GOA_OBJECT (g_dbus_object_manager_get_object (G_DBUS_OBJECT_MANAGER (self->object_manager), object_path));
      g_warn_if_fail (object != NULL);
      g_signal_handlers_disconnect_by_func (goa_object_peek_account (object),
                                            G_CALLBACK (on_account_handle_remove),
                                            self);
      g_object_unref (object);
      g_debug ("removing %s", object_path);
      g_warn_if_fail (g_dbus_object_manager_server_unexport (self->object_manager, object_path));
    }
  for (l = added; l != NULL; l = l->next)
    {
      const gchar *object_path = l->data;
      GoaObjectSkeleton *object;
      gchar *group;

      g_debug ("adding %s", object_path);

      group = account_object_path_to_group (object_path);
      key_file_data = g_hash_table_lookup (group_name_to_key_file_data, group);
      g_warn_if_fail (key_file_data != NULL);

      object = goa_object_skeleton_new (object_path);
      if (update_account_object (self,
                                 object,
                                 key_file_data->path,
                                 group,
                                 key_file_data->key_file,
                                 TRUE))
        {
          g_dbus_object_manager_server_export (self->object_manager, G_DBUS_OBJECT_SKELETON (object));
          g_signal_connect (goa_object_peek_account (GOA_OBJECT (object)),
                            "handle-remove",
                            G_CALLBACK (on_account_handle_remove),
                            self);
          g_signal_connect (goa_object_peek_account (GOA_OBJECT (object)),
                            "handle-ensure-credentials",
                            G_CALLBACK (on_account_handle_ensure_credentials),
                            self);
        }
      g_object_unref (object);
      g_free (group);
    }
  for (l = unchanged; l != NULL; l = l->next)
    {
      const gchar *object_path = l->data;
      GoaObject *object;
      gchar *group;

      g_debug ("unchanged %s", object_path);

      group = account_object_path_to_group (object_path);
      key_file_data = g_hash_table_lookup (group_name_to_key_file_data, group);
      g_warn_if_fail (key_file_data != NULL);

      object = GOA_OBJECT (g_dbus_object_manager_get_object (G_DBUS_OBJECT_MANAGER (self->object_manager), object_path));
      g_warn_if_fail (object != NULL);
      if (!update_account_object (self,
                                  GOA_OBJECT_SKELETON (object),
                                  key_file_data->path,
                                  group,
                                  key_file_data->key_file,
                                  FALSE))
        {
          g_signal_handlers_disconnect_by_func (goa_object_peek_account (object),
                                                G_CALLBACK (on_account_handle_remove),
                                                self);
          g_signal_handlers_disconnect_by_func (goa_object_peek_account (object),
                                                G_CALLBACK (on_account_handle_ensure_credentials),
                                                self);
          g_warn_if_fail (g_dbus_object_manager_server_unexport (self->object_manager, object_path));
        }
      g_object_unref (object);
      g_free (group);
    }

  g_list_free (removed);
  g_list_free (added);
  g_list_free (unchanged);
  g_list_free_full (existing_object_paths, g_free);
  g_list_free_full (config_object_paths, g_free);
}

/* ---------------------------------------------------------------------------------------------------- */

static gint
compare_account_and_template_groups (const gchar *account_group, const gchar *template_group)
{
  const gchar *account_id;
  const gchar *template_id;

  g_return_val_if_fail (g_str_has_prefix (account_group, "Account "), 0);
  g_return_val_if_fail (g_str_has_prefix (template_group, "Template "), 0);

  account_id = account_group + sizeof "Account " - 1;
  template_id = template_group + sizeof "Template " - 1;

  return g_strcmp0 (account_id, template_id);
}

static void
process_template_entries (GoaDaemon  *self,
                          GHashTable *group_name_to_key_file_data)
{
  GError *error;
  GHashTable *key_files_to_update = NULL;
  GHashTableIter iter;
  GKeyFile *home_conf_key_file = NULL;
  GKeyFile *key_file;
  KeyFileData *key_file_data;
  const gchar *group;
  const gchar *key_file_path;
  GList *config_object_groups = NULL;
  GList *config_template_groups = NULL;
  GList *added;
  GList *removed;
  GList *unchanged;
  GList *l;

  key_files_to_update = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify) g_key_file_unref);

  g_hash_table_iter_init (&iter, group_name_to_key_file_data);
  while (g_hash_table_iter_next (&iter, (gpointer *) &group, (gpointer *) &key_file_data))
    {
      if (home_conf_key_file == NULL && g_strcmp0 (key_file_data->path, self->home_conf_file_path) == 0)
        home_conf_key_file = g_key_file_ref (key_file_data->key_file);

      if (g_str_has_prefix (group, "Account "))
        config_object_groups = g_list_prepend (config_object_groups, g_strdup (group));
      else if (g_str_has_prefix (group, "Template "))
        config_template_groups = g_list_prepend (config_template_groups, g_strdup (group));
    }

  if (home_conf_key_file == NULL)
    home_conf_key_file = g_key_file_new ();

  config_object_groups = g_list_sort (config_object_groups, (GCompareFunc) g_strcmp0);
  config_template_groups = g_list_sort (config_template_groups, (GCompareFunc) g_strcmp0);
  diff_sorted_lists (config_object_groups,
                     config_template_groups,
                     (GCompareFunc) compare_account_and_template_groups,
                     &added,
                     &removed,
                     &unchanged);

  for (l = added; l != NULL; l = l->next)
    {
      gboolean needs_update;
      const gchar *id;
      const gchar *template_group = l->data;
      gchar *object_group = NULL;

      key_file_data = g_hash_table_lookup (group_name_to_key_file_data, template_group);
      g_assert_nonnull (key_file_data);

      if (goa_utils_keyfile_get_boolean (key_file_data->key_file, template_group, "ForceRemove"))
        continue;

      g_debug ("Adding from template %s", template_group);

      id = template_group_to_id (template_group);
      object_group = g_strdup_printf ("Account %s", id);
      g_warn_if_fail (!g_key_file_has_group (home_conf_key_file, object_group));

      needs_update = goa_utils_keyfile_copy_group (key_file_data->key_file,
                                                   template_group,
                                                   home_conf_key_file,
                                                   object_group);

      if (needs_update)
        {
          g_key_file_set_boolean (home_conf_key_file, object_group, "IsLocked", TRUE);
          g_hash_table_insert (key_files_to_update,
                               g_strdup (self->home_conf_file_path),
                               g_key_file_ref (home_conf_key_file));
        }

      g_free (object_group);
    }

  for (l = unchanged; l != NULL; l = l->next)
    {
      KeyFileData *object_key_file_data;
      KeyFileData *template_key_file_data;
      gboolean needs_update;
      const gchar *id;
      const gchar *object_group = l->data;
      gchar *template_group = NULL;

      object_key_file_data = g_hash_table_lookup (group_name_to_key_file_data, object_group);
      g_assert_nonnull (object_key_file_data);

      g_warn_if_fail (g_key_file_has_group (object_key_file_data->key_file, object_group));

      id = account_group_to_id (object_group);
      template_group = g_strdup_printf ("Template %s", id);

      template_key_file_data = g_hash_table_lookup (group_name_to_key_file_data, template_group);
      g_assert_nonnull (template_key_file_data);
      g_assert_true (g_key_file_has_group (template_key_file_data->key_file, template_group));

      if (goa_utils_keyfile_get_boolean (template_key_file_data->key_file, template_group, "ForceRemove"))
        {
          gboolean removed;

          g_debug ("Template %s specifies ForceRemove, removing %s", template_group, object_group);

          error = NULL;
          needs_update = g_key_file_remove_group (object_key_file_data->key_file, object_group, &error);
          if (error != NULL)
            {
              g_warning ("Error removing group %s from %s: %s (%s, %d)",
                         object_group,
                         key_file_data->path,
                         error->message,
                         g_quark_to_string (error->domain),
                         error->code);
              g_error_free (error);
            }

          if (needs_update)
            {
              g_hash_table_insert (key_files_to_update,
                                   g_strdup (object_key_file_data->path),
                                   g_key_file_ref (object_key_file_data->key_file));
            }

          removed = g_hash_table_remove (group_name_to_key_file_data, object_group);
          g_warn_if_fail (removed);
        }
      else
        {
          g_debug ("Updating %s from template %s", object_group, template_group);

          needs_update = goa_utils_keyfile_copy_group (template_key_file_data->key_file,
                                                       template_group,
                                                       object_key_file_data->key_file,
                                                       object_group);

          if (needs_update)
            {
              g_key_file_set_boolean (home_conf_key_file, object_group, "IsLocked", TRUE);
              g_hash_table_insert (key_files_to_update,
                                   g_strdup (object_key_file_data->path),
                                   g_key_file_ref (object_key_file_data->key_file));
            }
        }

      g_free (template_group);
    }

  g_hash_table_iter_init (&iter, key_files_to_update);
  while (g_hash_table_iter_next (&iter, (gpointer *) &key_file_path, (gpointer *) &key_file))
    {
      error = NULL;
      if (!g_key_file_save_to_file (key_file, key_file_path, &error))
        {
          g_prefix_error (&error, "Error writing key-value-file %s: ", key_file_path);
          g_warning ("%s (%s, %d)", error->message, g_quark_to_string (error->domain), error->code);
          g_error_free (error);
        }
    }

  g_hash_table_unref (key_files_to_update);
  g_key_file_unref (home_conf_key_file);
  g_list_free (removed);
  g_list_free (added);
  g_list_free (unchanged);
  g_list_free_full (config_object_groups, g_free);
  g_list_free_full (config_template_groups, g_free);
}

/* ---------------------------------------------------------------------------------------------------- */

/* <internal>
 * goa_daemon_reload_configuration:
 * @self: A #GoaDaemon
 *
 * Updates the accounts_objects member from stored configuration -
 * typically called at startup or when a change on the configuration
 * files has been detected.
 */
static void
goa_daemon_reload_configuration (GoaDaemon *self)
{
  GHashTable *group_name_to_key_file_data;

  group_name_to_key_file_data = g_hash_table_new_full (g_str_hash,
                                                       g_str_equal,
                                                       g_free,
                                                       (GDestroyNotify) key_file_data_free);

  /* Read the main user config file at $HOME/.config/goa-1.0/accounts.conf */
  add_config_file (self, self->home_conf_file_path, group_name_to_key_file_data);

  if (GOA_TEMPLATE_FILE != NULL && GOA_TEMPLATE_FILE[0] != '\0')
    add_config_file (self, GOA_TEMPLATE_FILE, group_name_to_key_file_data);

  process_template_entries (self, group_name_to_key_file_data);

  /* now process the group_name_to_key_file_data hash table */
  process_config_entries (self, group_name_to_key_file_data);

  g_hash_table_unref (group_name_to_key_file_data);
}

/* ---------------------------------------------------------------------------------------------------- */

static gchar *
generate_new_id (GoaDaemon *self)
{
  static guint counter = 0;
  GDateTime *dt;
  gchar *ret;

  dt = g_date_time_new_now_local ();
  ret = g_strdup_printf ("account_%" G_GINT64_FORMAT "_%u",
                         g_date_time_to_unix (dt), /* seconds since Epoch */
                         counter); /* avoids collisions */
  g_date_time_unref (dt);

  counter++;
  return ret;
}

typedef struct
{
  GoaDaemon *daemon;
  GoaManager *manager;
  GDBusMethodInvocation *invocation;
  gchar *provider_type;
  gchar *identity;
  gchar *presentation_identity;
  GVariant *credentials;
  GVariant *details;
} AddAccountData;

static void
get_all_providers_cb (GObject      *source,
                      GAsyncResult *res,
                      gpointer      user_data)
{
  AddAccountData *data = user_data;
  GoaProvider *provider = NULL;
  GKeyFile *key_file = NULL;
  GError *error;
  GList *providers = NULL;
  GList *l;
  gchar *id = NULL;
  gchar *group = NULL;
  gchar *key_file_data = NULL;
  gsize length;
  gsize n_credentials;
  gchar *object_path = NULL;
  GVariantIter iter;
  const gchar *key;
  const gchar *value;

  /* TODO: could check for @type */

  if (!goa_provider_get_all_finish (&providers, res, NULL))
    goto out;

  for (l = providers; l != NULL; l = l->next)
    {
      GoaProvider *p;
      const gchar *type;

      p = GOA_PROVIDER (l->data);
      type = goa_provider_get_provider_type (p);
      if (g_strcmp0 (type, data->provider_type) == 0)
        {
          provider = p;
          break;
        }
    }

  if (provider == NULL)
    {
      error= NULL;
      g_set_error (&error,
                   GOA_ERROR,
                   GOA_ERROR_FAILED, /* TODO: more specific */
                   _("Failed to find a provider for: %s"),
                   data->provider_type);
      g_dbus_method_invocation_take_error (data->invocation, error);
      goto out;
    }

  key_file = g_key_file_new ();

  error = NULL;
  if (!g_file_get_contents (data->daemon->home_conf_file_path,
                            &key_file_data,
                            &length,
                            &error))
    {
      if (error->domain == G_FILE_ERROR && error->code == G_FILE_ERROR_NOENT)
        {
          g_error_free (error);
        }
      else
        {
          g_prefix_error (&error, "Error loading file %s: ", data->daemon->home_conf_file_path);
          g_dbus_method_invocation_take_error (data->invocation, error);
          goto out;
        }
    }
  else
    {
      if (length > 0)
        {
          error = NULL;
          if (!g_key_file_load_from_data (key_file, key_file_data, length, G_KEY_FILE_KEEP_COMMENTS, &error))
            {
              g_prefix_error (&error, "Error parsing key-value-file %s: ", data->daemon->home_conf_file_path);
              g_dbus_method_invocation_take_error (data->invocation, error);
              goto out;
            }
        }
    }

  if (!g_variant_lookup (data->details, "Id", "s", &id))
    id = generate_new_id (data->daemon);

  group = g_strdup_printf ("Account %s", id);
  g_key_file_set_string (key_file, group, "Provider", data->provider_type);
  g_key_file_set_string (key_file, group, "Identity", data->identity);
  g_key_file_set_string (key_file, group, "PresentationIdentity", data->presentation_identity);

  g_variant_iter_init (&iter, data->details);
  while (g_variant_iter_next (&iter, "{&s&s}", &key, &value))
    {
      /* We treat IsTemporary special.  If it's true we add in
       * the current session guid, so it can be ignored after
       * the session is over.
       */
      if (g_strcmp0 (key, "IsTemporary") == 0)
        {
          if (g_strcmp0 (value, "true") == 0)
            {
              const char *guid;

              guid = g_dbus_connection_get_guid (data->daemon->connection);
              g_key_file_set_string (key_file, group, "SessionId", guid);
            }
        }

      /* Skip Id since we already handled it above. */
      if (g_strcmp0 (key, "Id") == 0)
        continue;

      g_key_file_set_string (key_file, group, key, value);
    }

  error = NULL;
  if (!g_key_file_save_to_file (key_file, data->daemon->home_conf_file_path, &error))
    {
      g_prefix_error (&error, "Error writing key-value-file %s: ", data->daemon->home_conf_file_path);
      g_dbus_method_invocation_take_error (data->invocation, error);
      goto out;
    }

  n_credentials = g_variant_n_children (data->credentials);
  if (n_credentials > 0)
    {
      /* We don't want to fail AddAccount if we could not store the
       * credentials in the keyring.
       */
      goa_utils_store_credentials_for_id_sync (provider,
                                               id,
                                               data->credentials,
                                               NULL, /* GCancellable */
                                               NULL);
    }

  goa_daemon_reload_configuration (data->daemon);

  object_path = g_strdup_printf ("/org/gnome/OnlineAccounts/Accounts/%s", id);
  goa_manager_complete_add_account (data->manager, data->invocation, object_path);

 out:
  g_free (object_path);
  g_list_free_full (providers, g_object_unref);
  g_free (key_file_data);
  g_free (group);
  g_free (id);
  g_clear_pointer (&key_file, (GDestroyNotify) g_key_file_unref);
  g_object_unref (data->daemon);
  g_object_unref (data->manager);
  g_object_unref (data->invocation);
  g_free (data->provider_type);
  g_free (data->identity);
  g_free (data->presentation_identity);
  g_variant_unref (data->credentials);
  g_variant_unref (data->details);
  g_slice_free (AddAccountData, data);
}

static gboolean
on_manager_handle_add_account (GoaManager             *manager,
                               GDBusMethodInvocation  *invocation,
                               const gchar            *provider_type,
                               const gchar            *identity,
                               const gchar            *presentation_identity,
                               GVariant               *credentials,
                               GVariant               *details,
                               gpointer                user_data)
{
  GoaDaemon *self = GOA_DAEMON (user_data);
  AddAccountData *data;

  data = g_slice_new0 (AddAccountData);
  data->daemon = g_object_ref (self);
  data->manager = g_object_ref (manager);
  data->invocation = g_object_ref (invocation);
  data->provider_type = g_strdup (provider_type);
  data->identity = g_strdup (identity);
  data->presentation_identity = g_strdup (presentation_identity);
  data->credentials = g_variant_ref (credentials);
  data->details = g_variant_ref (details);

  goa_provider_get_all (get_all_providers_cb, data);

  return TRUE; /* invocation was handled */
}

/* ---------------------------------------------------------------------------------------------------- */

typedef struct
{
  GoaObject *object;
  GList *invocations;
} ObjectInvocationData;

static ObjectInvocationData *
object_invocation_data_new (GoaObject             *object,
                 GDBusMethodInvocation *invocation)
{
  ObjectInvocationData *data;
  data = g_slice_new0 (ObjectInvocationData);
  data->object = g_object_ref (object);
  data->invocations = g_list_prepend (data->invocations, invocation);
  return data;
}

static void
object_invocation_data_unref (ObjectInvocationData *data)
{
  g_list_free (data->invocations);
  g_object_unref (data->object);
  g_slice_free (ObjectInvocationData, data);
}

/* ---------------------------------------------------------------------------------------------------- */

static void
remove_account_cb (GObject *source_object, GAsyncResult *res, gpointer user_data)
{
  GTask *task = G_TASK (user_data);
  GoaDaemon *self;
  GDBusMethodInvocation *invocation;
  GError *error;
  GoaAccount *account;
  GoaProvider *provider = GOA_PROVIDER (source_object);
  ObjectInvocationData *data;

  self = GOA_DAEMON (g_task_get_source_object (task));
  data = g_task_get_task_data (task);

  error= NULL;
  if (!goa_provider_remove_account_finish (provider, res, &error))
    {
      g_warning ("goa_provider_remove_account() failed: %s (%s, %d)",
                 error->message,
                 g_quark_to_string (error->domain),
                 error->code);
      g_error_free (error);
    }

  goa_daemon_reload_configuration (self);

  account = goa_object_peek_account (data->object);
  invocation = G_DBUS_METHOD_INVOCATION (data->invocations->data);
  goa_account_complete_remove (account, invocation);

  g_object_unref (task);
}

static gboolean
on_account_handle_remove (GoaAccount            *account,
                          GDBusMethodInvocation *invocation,
                          gpointer               user_data)
{
  GoaDaemon *self = GOA_DAEMON (user_data);
  GoaObject *object;
  GoaProvider *provider = NULL;
  GKeyFile *key_file = NULL;
  GTask *task = NULL;
  ObjectInvocationData *data;
  const gchar *provider_type = NULL;
  gchar *group = NULL;
  GError *error;

  if (goa_account_get_is_locked (account))
    {
      error = NULL;
      g_set_error_literal (&error,
                           GOA_ERROR,
                           GOA_ERROR_NOT_SUPPORTED,
                           _("IsLocked property is set for account"));
      g_dbus_method_invocation_take_error (invocation, error);
      goto out;
    }

  /* update key-file - right now we only support removing the account
   * if the entry is in ~/.config/goa-1.0/accounts.conf
   */

  key_file = g_key_file_new ();

  error = NULL;
  if (!g_key_file_load_from_file (key_file,
                                  self->home_conf_file_path,
                                  G_KEY_FILE_KEEP_COMMENTS,
                                  &error))
    {
      g_dbus_method_invocation_take_error (invocation, error);
      goto out;
    }

  group = g_strdup_printf ("Account %s", goa_account_get_id (account));

  error = NULL;
  if (!g_key_file_remove_group (key_file, group, &error))
    {
      g_dbus_method_invocation_take_error (invocation, error);
      goto out;
    }

  error = NULL;
  if (!g_key_file_save_to_file (key_file, self->home_conf_file_path, &error))
    {
      g_prefix_error (&error, "Error writing key-value-file %s: ", self->home_conf_file_path);
      g_dbus_method_invocation_take_error (invocation, error);
      goto out;
    }

  provider_type = goa_account_get_provider_type (account);
  if (provider_type == NULL)
    {
      error = NULL;
      g_set_error_literal (&error,
                           GOA_ERROR,
                           GOA_ERROR_FAILED, /* TODO: more specific */
                           _("ProviderType property is not set for account"));
      g_dbus_method_invocation_take_error (invocation, error);
      goto out;
    }

  provider = goa_provider_get_for_provider_type (provider_type);
  if (provider == NULL)
    {
      error = NULL;
      g_set_error (&error,
                   GOA_ERROR,
                   GOA_ERROR_FAILED, /* TODO: more specific */
                   _("Failed to find a provider for: %s"),
                   provider_type);
      g_dbus_method_invocation_take_error (invocation, error);
      goto out;
    }

  error = NULL;
  if (!goa_utils_delete_credentials_for_account_sync (provider, account, NULL, &error))
    {
      g_dbus_method_invocation_take_error (invocation, error);
      goto out;
    }

  object = GOA_OBJECT (g_dbus_interface_get_object (G_DBUS_INTERFACE (account)));
  data = object_invocation_data_new (object, invocation);

  task = g_task_new (self, NULL, NULL, NULL);
  g_task_set_task_data (task, data, (GDestroyNotify) object_invocation_data_unref);

  goa_provider_remove_account (provider, object, NULL, remove_account_cb, g_object_ref (task));

 out:
  g_clear_object (&provider);
  g_clear_object (&task);
  g_clear_pointer (&key_file, (GDestroyNotify) g_key_file_unref);
  g_free (group);
  return TRUE; /* invocation was handled */
}

/* ---------------------------------------------------------------------------------------------------- */

static gboolean
is_authorization_error (GError *error)
{
  gboolean ret;

  g_return_val_if_fail (error != NULL, FALSE);

  ret = FALSE;
  if (error->domain == REST_PROXY_ERROR || error->domain == SOUP_HTTP_ERROR)
    {
      if (SOUP_STATUS_IS_CLIENT_ERROR (error->code))
        ret = TRUE;
    }
  else if (error->domain == GOA_ERROR)
    {
      if (error->code == GOA_ERROR_NOT_AUTHORIZED)
        ret = TRUE;
    }
  return ret;
}

static void
ensure_credentials_queue_complete (GList *invocations, GoaAccount *account, gint expires_in, GError *error)
{
  GList *l;
  const gchar *id;
  const gchar *provider_type;
  gint64 timestamp;

  for (l = invocations; l != NULL; l = l->next)
    {
      GDBusMethodInvocation *invocation = G_DBUS_METHOD_INVOCATION (l->data);

      if (invocation == NULL)
        continue;

      if (error == NULL)
        goa_account_complete_ensure_credentials (account, invocation, expires_in);
      else
        g_dbus_method_invocation_return_gerror (invocation, error);
    }

  id = goa_account_get_id (account);
  provider_type = goa_account_get_provider_type (account);
  timestamp = g_get_monotonic_time ();
  g_debug ("%" G_GINT64_FORMAT ": Handled EnsureCredentials (%s, %s)", timestamp, provider_type, id);
}

static void
ensure_credentials_queue_collector (GObject *source_object, GAsyncResult *res, gpointer user_data)
{
  GTask *task = G_TASK (user_data);
  GTask *task_queued;
  GoaDaemon *self;
  GoaAccount *account;
  GoaProvider *provider = GOA_PROVIDER (source_object);
  GError *error;
  ObjectInvocationData *data;
  gint expires_in;

  self = GOA_DAEMON (g_task_get_source_object (task));

  task_queued = G_TASK (g_queue_pop_head (self->ensure_credentials_queue));
  g_assert (task == task_queued);

  data = g_task_get_task_data (task);
  account = goa_object_peek_account (data->object);

  error= NULL;
  if (!goa_provider_ensure_credentials_finish (provider, &expires_in, res, &error))
    {
      /* Set AttentionNeeded only if the error is an authorization error */
      if (is_authorization_error (error))
        {
          if (!goa_account_get_attention_needed (account))
            {
              goa_account_set_attention_needed (account, TRUE);
              g_dbus_interface_skeleton_flush (G_DBUS_INTERFACE_SKELETON (account));
              g_message ("%s: Setting AttentionNeeded to TRUE because EnsureCredentials() failed with: %s (%s, %d)",
                         g_dbus_object_get_object_path (G_DBUS_OBJECT (data->object)),
                         error->message, g_quark_to_string (error->domain), error->code);
            }
        }

      ensure_credentials_queue_complete (data->invocations, account, 0, error);
      g_error_free (error);
    }
  else
    {
      /* Clear AttentionNeeded flag if set */
      if (goa_account_get_attention_needed (account))
        {
          goa_account_set_attention_needed (account, FALSE);
          g_dbus_interface_skeleton_flush (G_DBUS_INTERFACE_SKELETON (account));
          g_message ("%s: Setting AttentionNeeded to FALSE because EnsureCredentials() succeded\n",
                     g_dbus_object_get_object_path (G_DBUS_OBJECT (data->object)));
        }

      ensure_credentials_queue_complete (data->invocations, account, expires_in, NULL);
    }

  self->ensure_credentials_running = FALSE;
  ensure_credentials_queue_check (self);
  g_object_unref (task);
}

static gint
ensure_credentials_queue_sort (gconstpointer a, gconstpointer b, gpointer user_data)
{
  GTask *task_a = G_TASK (a);
  GTask *task_b = G_TASK (b);
  gint priority_a;
  gint priority_b;

  priority_a = g_task_get_priority (task_a);
  priority_b = g_task_get_priority (task_b);

  return priority_a - priority_b;
}

static void
ensure_credentials_queue_check (GoaDaemon *self)
{
  GoaAccount *account;
  GoaProvider *provider = NULL;
  GTask *task;
  ObjectInvocationData *data;
  const gchar *id;
  const gchar *provider_type;
  gint64 timestamp;

  if (self->ensure_credentials_running)
    goto out;

  if (self->ensure_credentials_queue->length == 0)
    goto out;

  g_queue_sort (self->ensure_credentials_queue, ensure_credentials_queue_sort, NULL);

  task = G_TASK (g_queue_peek_head (self->ensure_credentials_queue));
  self->ensure_credentials_running = TRUE;

  data = (ObjectInvocationData *) g_task_get_task_data (task);
  account = goa_object_peek_account (data->object);

  id = goa_account_get_id (account);
  provider_type = goa_account_get_provider_type (account);
  timestamp = g_get_monotonic_time ();
  g_debug ("%" G_GINT64_FORMAT ": Handling EnsureCredentials (%s, %s)", timestamp, provider_type, id);

  provider = goa_provider_get_for_provider_type (provider_type);
  g_assert_nonnull (provider);

  goa_provider_ensure_credentials (provider,
                                   data->object,
                                   NULL, /* GCancellable */
                                   ensure_credentials_queue_collector,
                                   task);

 out:
  g_clear_object (&provider);
}

static gboolean
ensure_credentials_queue_coalesce (GoaDaemon *self, GoaObject *object, GDBusMethodInvocation *invocation)
{
  GList *l;
  GoaAccount *account;
  const gchar *id;
  gboolean ret = FALSE;
  gint priority;

  account = goa_object_peek_account (object);
  id = goa_account_get_id (account);

  priority = (invocation == NULL) ? G_PRIORITY_LOW : G_PRIORITY_HIGH;

  for (l = self->ensure_credentials_queue->head; l != NULL; l = l->next)
    {
      GoaAccount *account_queued;
      GTask *task = G_TASK (l->data);
      ObjectInvocationData *data;
      const gchar *id_queued;

      data = g_task_get_task_data (task);
      account_queued = goa_object_peek_account (data->object);
      id_queued = goa_account_get_id (account_queued);
      if (g_strcmp0 (id, id_queued) == 0)
        {
          gint priority_queued;

          priority_queued = g_task_get_priority (task);
          if (priority < priority_queued)
            g_task_set_priority (task, priority);

          data->invocations = g_list_prepend (data->invocations, invocation);

          ret = TRUE;
          break;
        }
    }

  return ret;
}

static gboolean
on_account_handle_ensure_credentials (GoaAccount            *account,
                                      GDBusMethodInvocation *invocation,
                                      gpointer               user_data)
{
  GoaDaemon *self = GOA_DAEMON (user_data);
  GoaObject *object;
  GoaProvider *provider = NULL;
  GTask *task = NULL;
  ObjectInvocationData *data;
  const gchar *id;
  const gchar *method_name;
  const gchar *provider_type;
  gint64 timestamp;

  id = goa_account_get_id (account);
  provider_type = goa_account_get_provider_type (account);
  method_name = g_dbus_method_invocation_get_method_name (invocation);
  timestamp = g_get_monotonic_time ();
  g_debug ("%" G_GINT64_FORMAT ": Received %s (%s, %s)", timestamp, method_name, provider_type, id);

  provider = goa_provider_get_for_provider_type (provider_type);
  if (provider == NULL)
    {
      g_dbus_method_invocation_return_error (invocation,
                                             GOA_ERROR,
                                             GOA_ERROR_FAILED,
                                             "Unsupported account type %s for id %s (no provider)",
                                             provider_type,
                                             id);
      goto out;
    }

  object = GOA_OBJECT (g_dbus_interface_get_object (G_DBUS_INTERFACE (account)));
  if (ensure_credentials_queue_coalesce (self, object, invocation))
    {
      timestamp = g_get_monotonic_time ();
      g_debug ("%" G_GINT64_FORMAT ": Coalesced %s (%s, %s)",
               timestamp,
               method_name,
               provider_type,
               id);
      goto out;
    }

  data = object_invocation_data_new (object, invocation);

  task = g_task_new (self, NULL, NULL, NULL);
  g_task_set_priority (task, G_PRIORITY_HIGH);
  g_task_set_task_data (task, data, (GDestroyNotify) object_invocation_data_unref);
  g_queue_push_tail (self->ensure_credentials_queue, g_object_ref (task));

  ensure_credentials_queue_check (self);

 out:
  g_clear_object (&provider);
  g_clear_object (&task);
  return TRUE; /* invocation was handled */
}

/* <internal>
 * goa_daemon_check_credentials:
 * @self: A #GoaDaemon
 *
 * Checks whether credentials are valid and tries to refresh them if
 * not. It also reports whether accounts are usable with the current
 * network.
 */
static void
goa_daemon_check_credentials (GoaDaemon *self)
{
  GList *l;
  GList *objects;

  objects = g_dbus_object_manager_get_objects (G_DBUS_OBJECT_MANAGER (self->object_manager));
  for (l = objects; l != NULL; l = l->next)
    {
      GoaAccount *account;
      GoaObject *object = GOA_OBJECT (l->data);
      GoaProvider *provider = NULL;
      GTask *task = NULL;
      ObjectInvocationData *data;
      const gchar *id;
      const gchar *provider_type;
      gint64 timestamp;

      account = goa_object_peek_account (object);
      if (account == NULL)
        goto cleanup_and_continue;

      provider_type = goa_account_get_provider_type (account);
      provider = goa_provider_get_for_provider_type (provider_type);
      if (provider == NULL)
        goto cleanup_and_continue;

      id = goa_account_get_id (account);
      provider_type = goa_account_get_provider_type (account);
      timestamp = g_get_monotonic_time ();
      g_debug ("%" G_GINT64_FORMAT ": Calling EnsureCredentials (%s, %s)",
               timestamp,
               provider_type,
               id);

      if (ensure_credentials_queue_coalesce (self, object, NULL))
        {
          timestamp = g_get_monotonic_time ();
          g_debug ("%" G_GINT64_FORMAT ": Coalesced EnsureCredentials (%s, %s)",
                   timestamp,
                   provider_type,
                   id);
          goto cleanup_and_continue;
        }

      data = object_invocation_data_new (object, NULL);

      task = g_task_new (self, NULL, NULL, NULL);
      g_task_set_priority (task, G_PRIORITY_LOW);
      g_task_set_task_data (task, data, (GDestroyNotify) object_invocation_data_unref);
      g_queue_push_tail (self->ensure_credentials_queue, g_object_ref (task));

    cleanup_and_continue:
      g_clear_object (&provider);
      g_clear_object (&task);
    }

  ensure_credentials_queue_check (self);
  g_list_free_full (objects, g_object_unref);
}
