/* -*- 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 <glib.h>
#include <glib/gi18n-lib.h>
#include <gio/gio.h>
#include <libgsystemservice/peer-manager.h>
#include <libmalcontent/user-manager.h>
#include <libmalcontent/user.h>
#include <libmalcontent-timer/parent-timer-service.h>
#include <libmalcontent-timer/time-span.h>
#include <libmalcontent-timer/timer-store.h>

#include "parent-iface.h"
#include "enums.h"
#include "operation-counter-private.h"


static void mct_parent_timer_service_constructed (GObject *object);
static void mct_parent_timer_service_dispose (GObject *object);
static void mct_parent_timer_service_get_property (GObject      *object,
                                                   unsigned int  property_id,
                                                   GValue       *value,
                                                   GParamSpec   *pspec);
static void mct_parent_timer_service_set_property (GObject      *object,
                                                   unsigned int  property_id,
                                                   const GValue *value,
                                                   GParamSpec   *pspec);

static void mct_parent_timer_service_method_call (GDBusConnection       *connection,
                                                  const char            *sender,
                                                  const char            *object_path,
                                                  const char            *interface_name,
                                                  const char            *method_name,
                                                  GVariant              *parameters,
                                                  GDBusMethodInvocation *invocation,
                                                  void                  *user_data);
static void mct_parent_timer_service_properties_get (MctParentTimerService *self,
                                                     GDBusConnection       *connection,
                                                     const char            *sender,
                                                     GVariant              *parameters,
                                                     GDBusMethodInvocation *invocation);
static void mct_parent_timer_service_properties_set (MctParentTimerService *self,
                                                     GDBusConnection       *connection,
                                                     const char            *sender,
                                                     GVariant              *parameters,
                                                     GDBusMethodInvocation *invocation);
static void mct_parent_timer_service_properties_get_all (MctParentTimerService *self,
                                                         GDBusConnection       *connection,
                                                         const char            *sender,
                                                         GVariant              *parameters,
                                                         GDBusMethodInvocation *invocation);

static void timer_store_estimated_end_times_changed_cb (MctTimerStore *timer_store,
                                                        const char    *username,
                                                        void          *user_data);
static void mct_parent_timer_service_query_usage (MctParentTimerService *self,
                                                  GDBusConnection       *connection,
                                                  const char            *sender,
                                                  GVariant              *parameters,
                                                  GDBusMethodInvocation *invocation);

/* These errors do go over the bus, and are registered in mct_parent_timer_service_class_init(). */
G_DEFINE_QUARK (MctParentTimerServiceError, mct_parent_timer_service_error)

static const char *parent_timer_service_errors[] =
{
  "org.freedesktop.MalcontentTimer1.Parent.Error.InvalidQuery",
  "org.freedesktop.MalcontentTimer1.Parent.Error.StorageError",
  "org.freedesktop.MalcontentTimer1.Parent.Error.Busy",
  "org.freedesktop.MalcontentTimer1.Parent.Error.IdentifyingUser",
  "org.freedesktop.MalcontentTimer1.Parent.Error.PermissionDenied",
};
static const GDBusErrorEntry parent_timer_service_error_map[] =
  {
    { MCT_PARENT_TIMER_SERVICE_ERROR_INVALID_QUERY, "org.freedesktop.MalcontentTimer1.Parent.Error.InvalidQuery" },
    { MCT_PARENT_TIMER_SERVICE_ERROR_STORAGE_ERROR, "org.freedesktop.MalcontentTimer1.Parent.Error.StorageError" },
    { MCT_PARENT_TIMER_SERVICE_ERROR_BUSY, "org.freedesktop.MalcontentTimer1.Parent.Error.Busy" },
    { MCT_PARENT_TIMER_SERVICE_ERROR_IDENTIFYING_USER, "org.freedesktop.MalcontentTimer1.Parent.Error.IdentifyingUser" },
    { MCT_PARENT_TIMER_SERVICE_ERROR_PERMISSION_DENIED, "org.freedesktop.MalcontentTimer1.Parent.Error.PermissionDenied" },
  };
G_STATIC_ASSERT (G_N_ELEMENTS (parent_timer_service_error_map) == MCT_PARENT_TIMER_SERVICE_N_ERRORS);
G_STATIC_ASSERT (G_N_ELEMENTS (parent_timer_service_error_map) == G_N_ELEMENTS (parent_timer_service_errors));

/**
 * MctParentTimerService:
 *
 * An implementation of the `org.freedesktop.MalcontentTimer1.Parent` D-Bus
 * interface, allowing a trusted component in a parent user account’s session to
 * record screen time and app usage periods for that account.
 *
 * This will expose all the necessary objects on the bus for peers to interact
 * with them, and hooks them up to internal state management using
 * [property@Malcontent.ParentTimerService:timer-store].
 *
 * Since: 0.14.0
 */
struct _MctParentTimerService
{
  GObject parent;

  GDBusConnection *connection;  /* (owned) */
  char *object_path;  /* (owned) */
  unsigned int object_id;

  /* Used to cancel any pending operations when the object is unregistered. */
  GCancellable *cancellable;  /* (owned) */

  MctTimerStore *timer_store;  /* (owned) */
  unsigned long timer_store_estimated_end_times_changed_id;
  MctUserManager *user_manager;  /* (owned) */
  GssPeerManager *peer_manager;  /* (owned) */
  unsigned int n_pending_operations;
};

typedef enum
{
  PROP_CONNECTION = 1,
  PROP_OBJECT_PATH,
  PROP_TIMER_STORE,
  PROP_USER_MANAGER,
  PROP_PEER_MANAGER,
  PROP_BUSY,
} MctParentTimerServiceProperty;

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

G_DEFINE_TYPE (MctParentTimerService, mct_parent_timer_service, G_TYPE_OBJECT)

static void
mct_parent_timer_service_class_init (MctParentTimerServiceClass *klass)
{
  GObjectClass *object_class = (GObjectClass *) klass;

  object_class->constructed = mct_parent_timer_service_constructed;
  object_class->dispose = mct_parent_timer_service_dispose;
  object_class->get_property = mct_parent_timer_service_get_property;
  object_class->set_property = mct_parent_timer_service_set_property;

  /**
   * MctParentTimerService:connection:
   *
   * D-Bus connection to export objects on.
   *
   * 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);

  /**
   * MctParentTimerService:object-path:
   *
   * Object path to root all exported objects at. If this does not end in a
   * slash, one will be added.
   *
   * Since: 0.14.0
   */
  props[PROP_OBJECT_PATH] =
      g_param_spec_string ("object-path", NULL, NULL,
                           "/",
                           G_PARAM_READWRITE |
                           G_PARAM_CONSTRUCT_ONLY |
                           G_PARAM_STATIC_STRINGS);

  /**
   * MctParentTimerService:timer-store:
   *
   * Store for timer data.
   *
   * Since: 0.14.0
   */
  props[PROP_TIMER_STORE] =
      g_param_spec_object ("timer-store", NULL, NULL,
                           MCT_TYPE_TIMER_STORE,
                           G_PARAM_READWRITE |
                           G_PARAM_CONSTRUCT_ONLY |
                           G_PARAM_STATIC_STRINGS);

  /**
   * MctParentTimerService:user-manager:
   *
   * User manager for querying family relationships.
   *
   * Since: 0.14.0
   */
  props[PROP_USER_MANAGER] =
      g_param_spec_object ("user-manager", NULL, NULL,
                           MCT_TYPE_USER_MANAGER,
                           G_PARAM_READWRITE |
                           G_PARAM_CONSTRUCT_ONLY |
                           G_PARAM_STATIC_STRINGS);

  /**
   * MctParentTimerService:peer-manager:
   *
   * Peer manager to identify incoming method calls.
   *
   * Since: 0.14.0
   */
  props[PROP_PEER_MANAGER] =
      g_param_spec_object ("peer-manager", NULL, NULL,
                           GSS_TYPE_PEER_MANAGER,
                           G_PARAM_READWRITE |
                           G_PARAM_CONSTRUCT_ONLY |
                           G_PARAM_STATIC_STRINGS);

  /**
   * MctParentTimerService:busy:
   *
   * True if the D-Bus API is busy.
   *
   * For example, if there are any outstanding method calls which haven’t been
   * replied to yet.
   *
   * Since: 0.14.0
   */
  props[PROP_BUSY] =
      g_param_spec_boolean ("busy", NULL, NULL,
                            FALSE,
                            G_PARAM_READABLE |
                            G_PARAM_STATIC_STRINGS);

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

  /* Error domain registration for D-Bus. We do this here, rather than in a
   * #GOnce section in mct_parent_timer_service_error_quark(), to avoid spreading the
   * D-Bus code outside this file. */
  for (size_t i = 0; i < G_N_ELEMENTS (parent_timer_service_error_map); i++)
    g_dbus_error_register_error (MCT_PARENT_TIMER_SERVICE_ERROR,
                                 parent_timer_service_error_map[i].error_code,
                                 parent_timer_service_error_map[i].dbus_error_name);
}

static void
mct_parent_timer_service_init (MctParentTimerService *self)
{
  self->cancellable = g_cancellable_new ();
}

static void
mct_parent_timer_service_constructed (GObject *object)
{
  MctParentTimerService *self = MCT_PARENT_TIMER_SERVICE (object);

  /* Chain up. */
  G_OBJECT_CLASS (mct_parent_timer_service_parent_class)->constructed (object);

  /* Check our construct properties. */
  g_assert (G_IS_DBUS_CONNECTION (self->connection));
  g_assert (g_variant_is_object_path (self->object_path));
  g_assert (MCT_IS_TIMER_STORE (self->timer_store));
  g_assert (MCT_IS_USER_MANAGER (self->user_manager));
  g_assert (GSS_IS_PEER_MANAGER (self->peer_manager));

  /* Connect to signals on the timer store. */
  self->timer_store_estimated_end_times_changed_id =
      g_signal_connect (self->timer_store, "estimated-end-times-changed",
                        G_CALLBACK (timer_store_estimated_end_times_changed_cb),
                        self);
}

static void
mct_parent_timer_service_dispose (GObject *object)
{
  MctParentTimerService *self = MCT_PARENT_TIMER_SERVICE (object);

  g_assert (self->object_id == 0);
  g_assert (self->n_pending_operations == 0);

  g_clear_object (&self->peer_manager);
  g_clear_object (&self->user_manager);
  g_clear_signal_handler (&self->timer_store_estimated_end_times_changed_id, self->timer_store);
  g_clear_object (&self->timer_store);

  g_clear_object (&self->connection);
  g_clear_pointer (&self->object_path, g_free);
  g_clear_object (&self->cancellable);

  /* Chain up to the parent class */
  G_OBJECT_CLASS (mct_parent_timer_service_parent_class)->dispose (object);
}

static void
mct_parent_timer_service_get_property (GObject      *object,
                                       unsigned int  property_id,
                                       GValue       *value,
                                       GParamSpec   *pspec)
{
  MctParentTimerService *self = MCT_PARENT_TIMER_SERVICE (object);

  switch ((MctParentTimerServiceProperty) property_id)
    {
    case PROP_CONNECTION:
      g_value_set_object (value, self->connection);
      break;
    case PROP_OBJECT_PATH:
      g_value_set_string (value, self->object_path);
      break;
    case PROP_TIMER_STORE:
      g_value_set_object (value, self->timer_store);
      break;
    case PROP_USER_MANAGER:
      g_value_set_object (value, self->user_manager);
      break;
    case PROP_PEER_MANAGER:
      g_value_set_object (value, self->peer_manager);
      break;
    case PROP_BUSY:
      g_value_set_boolean (value, mct_parent_timer_service_get_busy (self));
      break;
    default:
      g_assert_not_reached ();
    }
}

static void
mct_parent_timer_service_set_property (GObject      *object,
                                       unsigned int  property_id,
                                       const GValue *value,
                                       GParamSpec   *pspec)
{
  MctParentTimerService *self = MCT_PARENT_TIMER_SERVICE (object);

  switch ((MctParentTimerServiceProperty) property_id)
    {
    case PROP_CONNECTION:
      /* Construct only. */
      g_assert (self->connection == NULL);
      self->connection = g_value_dup_object (value);
      break;
    case PROP_OBJECT_PATH:
      /* Construct only. */
      g_assert (self->object_path == NULL);
      g_assert (g_variant_is_object_path (g_value_get_string (value)));
      self->object_path = g_value_dup_string (value);
      break;
    case PROP_TIMER_STORE:
      /* Construct only. */
      g_assert (self->timer_store == NULL);
      self->timer_store = g_value_dup_object (value);
      break;
    case PROP_USER_MANAGER:
      /* Construct only */
      g_assert (self->user_manager == NULL);
      self->user_manager = g_value_dup_object (value);
      break;
    case PROP_PEER_MANAGER:
      /* Construct only. */
      g_assert (self->peer_manager == NULL);
      self->peer_manager = g_value_dup_object (value);
      break;
    case PROP_BUSY:
      /* Read only. Fall through. */
      G_GNUC_FALLTHROUGH;
    default:
      g_assert_not_reached ();
    }
}

static void
timer_store_estimated_end_times_changed_cb (MctTimerStore *timer_store,
                                            const char    *username,
                                            void          *user_data)
{
  MctParentTimerService *self = MCT_PARENT_TIMER_SERVICE (user_data);

  /* Ignore errors from emitting the signal; it can only fail if the parameters
   * are invalid (not possible) or if the connection has been closed. */
  g_dbus_connection_emit_signal (self->connection,
                                 NULL,  /* destination bus name */
                                 self->object_path,
                                 "org.freedesktop.MalcontentTimer1.Parent",
                                 "UsageChanged",
                                 NULL,
                                 NULL);
}

/**
 * mct_parent_timer_service_register:
 * @self: a parent service
 * @error: return location for a [type@GLib.Error]
 *
 * Register the parent timer service objects on D-Bus using the connection
 * details given in [property@Malcontent.ParentTimerService.connection] and
 * [property@Malcontent.ParentTimerService.object-path].
 *
 * Use [method@Malcontent.ParentTimerService.unregister] to unregister them.
 * Calls to these two functions must be well paired.
 *
 * Returns: true on success, false otherwise
 * Since: 0.14.0
 */
gboolean
mct_parent_timer_service_register (MctParentTimerService  *self,
                                   GError                **error)
{
  g_return_val_if_fail (MCT_IS_PARENT_TIMER_SERVICE (self), FALSE);
  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

  const GDBusInterfaceVTable interface_vtable =
    {
      mct_parent_timer_service_method_call,
      NULL,  /* handled in mct_parent_timer_service_method_call() */
      NULL,  /* handled in mct_parent_timer_service_method_call() */
      { NULL, },  /* padding */
    };

  unsigned int id = g_dbus_connection_register_object (self->connection,
                                                       self->object_path,
                                                       (GDBusInterfaceInfo *) &org_freedesktop_malcontent_timer1_parent_interface,
                                                       &interface_vtable,
                                                       g_object_ref (self),
                                                       g_object_unref,
                                                       error);

  if (id == 0)
    return FALSE;

  self->object_id = id;

  /* This has potentially changed. */
  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_BUSY]);

  return TRUE;
}

/**
 * mct_parent_timer_service_unregister:
 * @self: a parent service
 *
 * Unregister objects from D-Bus which were previously registered using
 * [method@Malcontent.ParentTimerService.register].
 *
 * Calls to these two functions must be well paired.
 *
 * Since: 0.14.0
 */
void
mct_parent_timer_service_unregister (MctParentTimerService *self)
{
  g_return_if_fail (MCT_IS_PARENT_TIMER_SERVICE (self));

  g_dbus_connection_unregister_object (self->connection, self->object_id);
  self->object_id = 0;

  /* This has potentially changed. */
  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_BUSY]);
}

static gboolean
validate_dbus_interface_name (GDBusMethodInvocation *invocation,
                              const char            *interface_name)
{
  if (!g_dbus_is_interface_name (interface_name))
    {
      g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR,
                                             G_DBUS_ERROR_UNKNOWN_INTERFACE,
                                             _("Invalid interface name ‘%s’."),
                                             interface_name);
      return FALSE;
    }

  return TRUE;
}

static gboolean
validate_record_type_and_identifier (GDBusMethodInvocation *invocation,
                                     const char            *record_type,
                                     const char            *identifier)
{
  g_autoptr(GError) local_error = NULL;

  if (!mct_timer_store_record_type_validate_string (record_type, &local_error) ||
      !mct_timer_store_record_type_validate_identifier (mct_timer_store_record_type_from_string (record_type), identifier, &local_error))
    {
      g_dbus_method_invocation_return_error (invocation, MCT_PARENT_TIMER_SERVICE_ERROR,
                                             MCT_PARENT_TIMER_SERVICE_ERROR_INVALID_QUERY,
                                             _("Invalid query parameters: %s"),
                                             local_error->message);
      return FALSE;
    }

  return TRUE;
}

typedef void (*ParentMethodCallFunc) (MctParentTimerService *self,
                                      GDBusConnection       *connection,
                                      const char            *sender,
                                      GVariant              *parameters,
                                      GDBusMethodInvocation *invocation);

static const struct
  {
    const char *interface_name;
    const char *method_name;
    ParentMethodCallFunc func;
  }
parent_methods[] =
  {
    /* Handle properties. */
    { "org.freedesktop.DBus.Properties", "Get",
      mct_parent_timer_service_properties_get },
    { "org.freedesktop.DBus.Properties", "Set",
      mct_parent_timer_service_properties_set },
    { "org.freedesktop.DBus.Properties", "GetAll",
      mct_parent_timer_service_properties_get_all },

    /* Parent methods. */
    { "org.freedesktop.MalcontentTimer1.Parent", "QueryUsage",
      mct_parent_timer_service_query_usage },
  };

static void
mct_parent_timer_service_method_call (GDBusConnection       *connection,
                                      const char            *sender,
                                      const char            *object_path,
                                      const char            *interface_name,
                                      const char            *method_name,
                                      GVariant              *parameters,
                                      GDBusMethodInvocation *invocation,
                                      void                  *user_data)
{
  MctParentTimerService *self = MCT_PARENT_TIMER_SERVICE (user_data);

  /* Check we’ve implemented all the methods. Unfortunately this can’t be a
   * compile time check because the method array is declared in a separate
   * compilation unit. */
  size_t n_parent_interface_methods = 0;
  for (size_t i = 0; org_freedesktop_malcontent_timer1_parent_interface.methods[i] != NULL; i++)
    n_parent_interface_methods++;

  g_assert (G_N_ELEMENTS (parent_methods) ==
            n_parent_interface_methods +
            3  /* o.fdo.DBus.Properties */);

  /* Remove the service prefix from the path. */
  g_assert (g_str_equal (object_path, self->object_path));

  /* Work out which method to call. */
  for (size_t i = 0; i < G_N_ELEMENTS (parent_methods); i++)
    {
      if (g_str_equal (parent_methods[i].interface_name, interface_name) &&
          g_str_equal (parent_methods[i].method_name, method_name))
        {
          parent_methods[i].func (self, connection, sender, parameters, invocation);
          return;
        }
    }

  /* Make sure we actually called a method implementation. GIO guarantees that
   * this function is only called with methods we’ve declared in the interface
   * info, so this should never fail. */
  g_assert_not_reached ();
}

static void
mct_parent_timer_service_properties_get (MctParentTimerService *self,
                                         GDBusConnection       *connection,
                                         const char            *sender,
                                         GVariant              *parameters,
                                         GDBusMethodInvocation *invocation)
{
  const char *interface_name, *property_name;
  g_variant_get (parameters, "(&s&s)", &interface_name, &property_name);

  /* D-Bus property names can be anything. */
  if (!validate_dbus_interface_name (invocation, interface_name))
    return;

  /* No properties exposed. */
  g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR,
                                         G_DBUS_ERROR_UNKNOWN_PROPERTY,
                                         _("Unknown property ‘%s.%s’."),
                                         interface_name, property_name);
  }

static void
mct_parent_timer_service_properties_set (MctParentTimerService *self,
                                         GDBusConnection       *connection,
                                         const char            *sender,
                                         GVariant              *parameters,
                                         GDBusMethodInvocation *invocation)
{
  const char *interface_name, *property_name;
  g_variant_get (parameters, "(&s&sv)", &interface_name, &property_name, NULL);

  /* D-Bus property names can be anything. */
  if (!validate_dbus_interface_name (invocation, interface_name))
    return;

  /* No properties exposed. */
  g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR,
                                         G_DBUS_ERROR_UNKNOWN_PROPERTY,
                                         _("Unknown property ‘%s.%s’."),
                                         interface_name, property_name);
}

static void
mct_parent_timer_service_properties_get_all (MctParentTimerService *self,
                                             GDBusConnection       *connection,
                                             const char            *sender,
                                             GVariant              *parameters,
                                             GDBusMethodInvocation *invocation)
{
  const char *interface_name;
  g_variant_get (parameters, "(&s)", &interface_name);

  if (!validate_dbus_interface_name (invocation, interface_name))
    return;

  /* Try the interface. */
  if (g_str_equal (interface_name, "org.freedesktop.MalcontentTimer1.Parent"))
    g_dbus_method_invocation_return_value (invocation,
                                           g_variant_new_parsed ("(@a{sv} {},)"));
  else
    g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR,
                                           G_DBUS_ERROR_UNKNOWN_INTERFACE,
                                           _("Unknown interface ‘%s’."),
                                           interface_name);
}

typedef struct
{
  MctParentTimerService *parent_timer_service;  /* (owned) */
  uid_t uid;
  MctTimerStoreRecordType record_type;
  char *identifier;  /* (owned) */
  GDBusMethodInvocation *invocation;  /* (owned) */
  MctUser *child_user;  /* (nullable) (owned) */
  MctUser *parent_user;  /* (nullable) (owned) */
  MctOperationCounter operation_counter;
  unsigned int n_open_retries;
} QueryUsageData;

static void
query_usage_data_free (QueryUsageData *data)
{
  g_clear_pointer (&data->identifier, g_free);
  g_clear_object (&data->invocation);
  g_clear_object (&data->parent_user);
  g_clear_object (&data->child_user);
  g_clear_object (&data->parent_timer_service);
  mct_operation_counter_release_and_clear (&data->operation_counter);
  g_free (data);
}

G_DEFINE_AUTOPTR_CLEANUP_FUNC (QueryUsageData, query_usage_data_free)

static void query_usage_ensure_credentials_cb (GObject      *object,
                                               GAsyncResult *result,
                                               void         *user_data);
static void query_usage_get_parent_user_cb (GObject      *object,
                                            GAsyncResult *result,
                                            void         *user_data);
static void query_usage_get_child_user_cb (GObject      *object,
                                           GAsyncResult *result,
                                           void         *user_data);
static void query_usage_open_username_cb (GObject      *object,
                                          GAsyncResult *result,
                                          void         *user_data);

static void
mct_parent_timer_service_query_usage (MctParentTimerService *self,
                                      GDBusConnection       *connection,
                                      const char            *sender,
                                      GVariant              *parameters,
                                      GDBusMethodInvocation *invocation)
{
  g_autoptr(GError) local_error = NULL;
  uid_t uid;
  const char *record_type, *identifier;
  g_autoptr(QueryUsageData) data = NULL;

  /* Validate the parameters. */
  g_variant_get (parameters, "(u&s&s)", &uid, &record_type, &identifier);

  if (!validate_record_type_and_identifier (invocation, record_type, identifier))
    return;

  /* Load the peer’s credentials so we know which user is querying the usage
   * entries, and whether they’re allowed to do that for @uid. */
  data = g_new0 (QueryUsageData, 1);
  data->parent_timer_service = g_object_ref (self);
  data->uid = uid;
  data->record_type = mct_timer_store_record_type_from_string (record_type);
  data->identifier = g_strdup (identifier);
  data->invocation = g_object_ref (invocation);
  mct_operation_counter_init_and_hold (&data->operation_counter,
                                       &self->n_pending_operations,
                                       G_OBJECT (self), props[PROP_BUSY]);

  gss_peer_manager_ensure_peer_credentials_async (self->peer_manager,
                                                  sender,
                                                  self->cancellable,
                                                  query_usage_ensure_credentials_cb,
                                                  g_steal_pointer (&data));
}

static void
query_usage_ensure_credentials_cb (GObject      *object,
                                   GAsyncResult *result,
                                   void         *user_data)
{
  GssPeerManager *peer_manager = GSS_PEER_MANAGER (object);
  g_autoptr(QueryUsageData) data = g_steal_pointer (&user_data);
  MctParentTimerService *self = data->parent_timer_service;
  GDBusMethodInvocation *invocation = data->invocation;
  uid_t peer_uid;
  g_autoptr(MctUser) parent_user = NULL, child_user = NULL;
  g_autoptr(GError) local_error = NULL;

  /* Finish looking up the sender. */
  if (!gss_peer_manager_ensure_peer_credentials_finish (peer_manager,
                                                        result, &local_error))
    {
      g_dbus_method_invocation_return_error (invocation, MCT_PARENT_TIMER_SERVICE_ERROR,
                                             MCT_PARENT_TIMER_SERVICE_ERROR_IDENTIFYING_USER,
                                             _("Error identifying user: %s"),
                                             local_error->message);
      return;
    }

  /* Load details of the family relationship of the two users. */
  g_assert (mct_user_manager_get_is_loaded (self->user_manager));

  peer_uid = gss_peer_manager_get_peer_uid (peer_manager, g_dbus_method_invocation_get_sender (invocation));
  if (peer_uid == 0 || peer_uid == (uid_t) -1)
    {
      g_dbus_method_invocation_return_error (invocation, MCT_PARENT_TIMER_SERVICE_ERROR,
                                             MCT_PARENT_TIMER_SERVICE_ERROR_IDENTIFYING_USER,
                                             _("Error identifying user: %s"),
                                             _("Invalid or unknown user"));
      return;
    }

  mct_user_manager_get_user_by_uid_async (self->user_manager, peer_uid, self->cancellable,
                                          query_usage_get_parent_user_cb, g_steal_pointer (&data));
}

static void
query_usage_get_parent_user_cb (GObject      *object,
                                GAsyncResult *result,
                                void         *user_data)
{
  MctUserManager *user_manager = MCT_USER_MANAGER (object);
  g_autoptr(QueryUsageData) data = g_steal_pointer (&user_data);
  MctParentTimerService *self = data->parent_timer_service;
  GDBusMethodInvocation *invocation = data->invocation;
  uid_t uid;
  g_autoptr(GError) local_error = NULL;

  data->parent_user = mct_user_manager_get_user_by_uid_finish (user_manager, result, &local_error);

  if (data->parent_user == NULL)
    {
      g_dbus_method_invocation_return_error (invocation, MCT_PARENT_TIMER_SERVICE_ERROR,
                                             MCT_PARENT_TIMER_SERVICE_ERROR_IDENTIFYING_USER,
                                             _("Error identifying user: %s"),
                                             _("Invalid or unknown user"));
      return;
    }

  uid = data->uid;
  mct_user_manager_get_user_by_uid_async (user_manager, uid, self->cancellable,
                                          query_usage_get_child_user_cb, g_steal_pointer (&data));
}

static void
query_usage_get_child_user_cb (GObject      *object,
                               GAsyncResult *result,
                               void         *user_data)
{
  MctUserManager *user_manager = MCT_USER_MANAGER (object);
  g_autoptr(QueryUsageData) data = g_steal_pointer (&user_data);
  MctParentTimerService *self = data->parent_timer_service;
  GDBusMethodInvocation *invocation = data->invocation;
  MctUser *child_user;
  g_autoptr(GError) local_error = NULL;

  child_user = data->child_user = mct_user_manager_get_user_by_uid_finish (user_manager, result, &local_error);

  if (child_user == NULL)
    {
      g_dbus_method_invocation_return_error (invocation, MCT_PARENT_TIMER_SERVICE_ERROR,
                                             MCT_PARENT_TIMER_SERVICE_ERROR_IDENTIFYING_USER,
                                             _("Error identifying user: %s"),
                                             _("Invalid or unknown user"));
      return;
    }

  /* Check that the sender is actually a parent of @data->uid, or that the two
   * users are the same (i.e. a user, potentially a child, is querying their own
   * records). */
  if (!mct_user_is_parent_of (data->parent_user, child_user) &&
      !mct_user_equal (data->parent_user, child_user))
    {
      g_dbus_method_invocation_return_error (invocation, MCT_PARENT_TIMER_SERVICE_ERROR,
                                             MCT_PARENT_TIMER_SERVICE_ERROR_PERMISSION_DENIED,
                                             _("Permission denied to query user usage"));
      return;
    }

  /* Proceed to open the database for the child user so we can query their records. */
  mct_timer_store_open_username_async (self->timer_store,
                                       mct_user_get_username (child_user),
                                       self->cancellable,
                                       query_usage_open_username_cb,
                                       g_steal_pointer (&data));
}

static void
query_usage_open_username_cb (GObject      *object,
                              GAsyncResult *result,
                              void         *user_data)
{
  MctTimerStore *timer_store = MCT_TIMER_STORE (object);
  g_autoptr(QueryUsageData) data = g_steal_pointer (&user_data);
  MctParentTimerService *self = data->parent_timer_service;
  MctUser *child_user = data->child_user;
  GDBusMethodInvocation *invocation = data->invocation;
  const MctTimerStoreTransaction *transaction;
  g_auto(GVariantBuilder) builder = G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE ("a(tt)"));
  const MctTimeSpan * const *time_spans = NULL;
  size_t n_time_spans = 0;
  g_autoptr(GError) local_error = NULL;

  transaction = mct_timer_store_open_username_finish (timer_store, result, &local_error);

  if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_BUSY) && data->n_open_retries >= 10)
    {
      g_dbus_method_invocation_return_error (invocation, MCT_PARENT_TIMER_SERVICE_ERROR,
                                             MCT_PARENT_TIMER_SERVICE_ERROR_BUSY,
                                             _("Error opening user file: %s"),
                                             local_error->message);
      return;
    }
  else if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_BUSY))
    {
      /* Try again */
      data->n_open_retries++;
      mct_timer_store_open_username_async (timer_store,
                                           mct_user_get_username (child_user),
                                           self->cancellable,
                                           query_usage_open_username_cb,
                                           g_steal_pointer (&data));
      return;
    }
  else if (local_error != NULL)  /* likely a GFileError */
    {
      g_dbus_method_invocation_return_error (invocation, MCT_PARENT_TIMER_SERVICE_ERROR,
                                             MCT_PARENT_TIMER_SERVICE_ERROR_STORAGE_ERROR,
                                             _("Error opening user file: %s"),
                                             local_error->message);
      return;
    }

  /* Query the timer store for all the matching records */
  time_spans = mct_timer_store_query_time_spans (timer_store, transaction,
                                                 data->record_type,
                                                 data->identifier,
                                                 &n_time_spans);

  for (size_t i = 0; i < n_time_spans; i++)
    {
      const MctTimeSpan *time_span = time_spans[i];

      g_variant_builder_add (&builder, "(tt)",
                             mct_time_span_get_start_time_secs (time_span),
                             mct_time_span_get_end_time_secs (time_span));
    }

  /* Close the file again. */
  mct_timer_store_roll_back_transaction (timer_store, transaction);

  /* Return the results */
  g_dbus_method_invocation_return_value (invocation,
                                         g_variant_new ("(@a(tt))",
                                                        g_variant_builder_end (&builder)));
}

/**
 * mct_parent_timer_service_new:
 * @connection: (transfer none): D-Bus connection to export objects on
 * @object_path: root path to export objects below; must be a valid D-Bus object
 *    path
 * @timer_store: (transfer none): store to use for timer data
 * @user_manager: (transfer none): user manager for querying family relationships
 * @peer_manager: (transfer none): peer manager for querying D-Bus peers
 *
 * Create a new [class@Malcontent.ParentTimerService] instance which is set up
 * to run as a service.
 *
 * Returns: (transfer full): a new [class@Malcontent.ParentTimerService]
 * Since: 0.14.0
 */
MctParentTimerService *
mct_parent_timer_service_new (GDBusConnection *connection,
                              const char      *object_path,
                              MctTimerStore   *timer_store,
                              MctUserManager  *user_manager,
                              GssPeerManager  *peer_manager)
{
  g_return_val_if_fail (G_IS_DBUS_CONNECTION (connection), NULL);
  g_return_val_if_fail (g_variant_is_object_path (object_path), NULL);
  g_return_val_if_fail (MCT_IS_TIMER_STORE (timer_store), NULL);
  g_return_val_if_fail (MCT_IS_USER_MANAGER (user_manager), NULL);
  g_return_val_if_fail (GSS_IS_PEER_MANAGER (peer_manager), NULL);

  return g_object_new (MCT_TYPE_PARENT_TIMER_SERVICE,
                       "connection", connection,
                       "object-path", object_path,
                       "timer-store", timer_store,
                       "user-manager", user_manager,
                       "peer-manager", peer_manager,
                       NULL);
}

/**
 * mct_parent_timer_service_get_busy:
 * @self: a parent service
 *
 * Get the value of [property@Malcontent.ParentTimerService.busy].
 *
 * Returns: true if the service is busy, false otherwise
 * Since: 0.14.0
 */
gboolean
mct_parent_timer_service_get_busy (MctParentTimerService *self)
{
  g_return_val_if_fail (MCT_IS_PARENT_TIMER_SERVICE (self), FALSE);

  return (self->object_id != 0 && self->n_pending_operations > 0);
}

