/* -*- 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 <libmalcontent-timer/extension-agent-object.h>
#include <libmalcontent-timer/timer-store.h>

#include "extension-agent-iface.h"
#include "extension-agent-request-iface.h"
#include "enums.h"
#include "operation-counter-private.h"
#include "properties-iface.h"


static void mct_extension_agent_object_constructed (GObject *object);
static void mct_extension_agent_object_dispose (GObject *object);
static void mct_extension_agent_object_get_property (GObject      *object,
                                                     unsigned int  property_id,
                                                     GValue       *value,
                                                     GParamSpec   *pspec);
static void mct_extension_agent_object_set_property (GObject      *object,
                                                     unsigned int  property_id,
                                                     const GValue *value,
                                                     GParamSpec   *pspec);

static char **mct_extension_agent_object_request_enumerate (GDBusConnection *connection,
                                                            const char      *sender,
                                                            const char      *object_path,
                                                            void            *user_data);
static GDBusInterfaceInfo **mct_extension_agent_object_request_introspect (GDBusConnection *connection,
                                                                           const char      *sender,
                                                                           const char      *object_path,
                                                                           const char      *node,
                                                                           void            *user_data);
static const GDBusInterfaceVTable *mct_extension_agent_object_request_dispatch (GDBusConnection  *connection,
                                                                                const char       *sender,
                                                                                const char       *object_path,
                                                                                const char       *interface_name,
                                                                                const char       *node,
                                                                                void            **out_user_data,
                                                                                void             *user_data);

static void mct_extension_agent_object_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_extension_agent_object_properties_get (MctExtensionAgentObject *self,
                                                       GDBusConnection         *connection,
                                                       const char              *sender,
                                                       GVariant                *parameters,
                                                       GDBusMethodInvocation   *invocation);
static void mct_extension_agent_object_properties_set (MctExtensionAgentObject *self,
                                                       GDBusConnection         *connection,
                                                       const char              *sender,
                                                       GVariant                *parameters,
                                                       GDBusMethodInvocation   *invocation);
static void mct_extension_agent_object_properties_get_all (MctExtensionAgentObject *self,
                                                           GDBusConnection         *connection,
                                                           const char              *sender,
                                                           GVariant                *parameters,
                                                           GDBusMethodInvocation   *invocation);

static void mct_extension_agent_object_request_extension (MctExtensionAgentObject *self,
                                                          GDBusConnection         *connection,
                                                          const char              *sender,
                                                          GVariant                *parameters,
                                                          GDBusMethodInvocation   *invocation);

/* These errors do go over the bus, and are registered in mct_extension_agent_object_class_init(). */
G_DEFINE_QUARK (MctExtensionAgentObjectError, mct_extension_agent_object_error)

static const char *extension_agent_object_errors[] =
{
  "org.freedesktop.MalcontentTimer1.ExtensionAgent.Error.Failed",
  "org.freedesktop.MalcontentTimer1.ExtensionAgent.Error.IdentifyingUser",
  "org.freedesktop.MalcontentTimer1.ExtensionAgent.Error.Cancelled",
  "org.freedesktop.MalcontentTimer1.ExtensionAgent.Error.InvalidQuery",
};
static const GDBusErrorEntry extension_agent_object_error_map[] =
  {
    { MCT_EXTENSION_AGENT_OBJECT_ERROR_FAILED, "org.freedesktop.MalcontentTimer1.ExtensionAgent.Error.Failed" },
    { MCT_EXTENSION_AGENT_OBJECT_ERROR_IDENTIFYING_USER, "org.freedesktop.MalcontentTimer1.ExtensionAgent.Error.IdentifyingUser" },
    { MCT_EXTENSION_AGENT_OBJECT_ERROR_CANCELLED, "org.freedesktop.MalcontentTimer1.ExtensionAgent.Error.Cancelled" },
    { MCT_EXTENSION_AGENT_OBJECT_ERROR_INVALID_QUERY, "org.freedesktop.MalcontentTimer1.ExtensionAgent.Error.InvalidQuery" },
  };
G_STATIC_ASSERT (G_N_ELEMENTS (extension_agent_object_error_map) == MCT_EXTENSION_AGENT_OBJECT_N_ERRORS);
G_STATIC_ASSERT (G_N_ELEMENTS (extension_agent_object_error_map) == G_N_ELEMENTS (extension_agent_object_errors));

/* Small struct which tracks a pending request, and which maps directly to a
 * /Request D-Bus object on the bus. While this struct exists (and is registered
 * in MctExtensionAgentObject.requests) it’s exported on the bus. */
typedef struct
{
  char *object_path;  /* full path of this Request object on the bus */
  const char *object_subpath;  /* subpath of the Request object, i.e. `Request123` */
  char *owner;  /* unique bus name of peer who originally called RequestExtension() */
  GCancellable *cancellable;  /* (owned) */
  unsigned int owner_name_watch_id;
} MctExtensionAgentRequest;

static void
mct_extension_agent_request_free (MctExtensionAgentRequest *request)
{
  g_free (request->object_path);
  g_free (request->owner);
  g_clear_object (&request->cancellable);
  if (request->owner_name_watch_id != 0)
    g_bus_unwatch_name (request->owner_name_watch_id);
  request->owner_name_watch_id = 0;
  g_free (request);
}

G_DEFINE_AUTOPTR_CLEANUP_FUNC (MctExtensionAgentRequest, mct_extension_agent_request_free)

static void mct_extension_agent_request_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_extension_agent_request_properties_get (MctExtensionAgentObject  *self,
                                                        MctExtensionAgentRequest *request,
                                                        GDBusConnection          *connection,
                                                        const char               *sender,
                                                        GVariant                 *parameters,
                                                        GDBusMethodInvocation    *invocation);
static void mct_extension_agent_request_properties_set (MctExtensionAgentObject  *self,
                                                        MctExtensionAgentRequest *request,
                                                        GDBusConnection          *connection,
                                                        const char               *sender,
                                                        GVariant                 *parameters,
                                                        GDBusMethodInvocation    *invocation);
static void mct_extension_agent_request_properties_get_all (MctExtensionAgentObject  *self,
                                                            MctExtensionAgentRequest *request,
                                                            GDBusConnection          *connection,
                                                            const char               *sender,
                                                            GVariant                 *parameters,
                                                            GDBusMethodInvocation    *invocation);
static void mct_extension_agent_request_close (MctExtensionAgentObject  *self,
                                               MctExtensionAgentRequest *request,
                                               GDBusConnection          *connection,
                                               const char               *sender,
                                               GVariant                 *parameters,
                                               GDBusMethodInvocation    *invocation);

/**
 * MctExtensionAgentObject:
 *
 * An implementation of the `org.freedesktop.MalcontentTimer1.ExtensionAgent`
 * D-Bus interface, allowing a trusted component on the system (the
 * `malcontent-timerd` daemon) to ask for a decision about whether to allow a
 * screen time limit extension request from a child user.
 *
 * This will expose all the necessary objects on the bus for `malcontent-timerd`
 * to interact with them, but delegates the logic of whether to allow each
 * extension request to the
 * [vfunc@Malcontent.ExtensionAgentObject.request_extension_async] virtual
 * method, which must be implemented by a subclass. This allows for different
 * implementations of the agent to share the common D-Bus framework but use
 * different UIs for asking the parent whether to allow the extension.
 *
 * Since: 0.14.0
 */
typedef struct
{
  GDBusConnection *connection;  /* (owned) */
  char *object_path;  /* (owned) */
  unsigned int request_subtree_id;

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

  unsigned int n_pending_operations;

  unsigned int request_counter;
  /* Map from D-Bus object path to MctExtensionAgentRequest struct: */
  GHashTable *requests;  /* (element-type utf8 MctExtensionAgentRequest) (owned) */
} MctExtensionAgentObjectPrivate;

typedef enum
{
  PROP_CONNECTION = 1,
  PROP_OBJECT_PATH,
  PROP_BUSY,
} MctExtensionAgentObjectProperty;

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

G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (MctExtensionAgentObject, mct_extension_agent_object, G_TYPE_OBJECT)

static void
mct_extension_agent_object_class_init (MctExtensionAgentObjectClass *klass)
{
  GObjectClass *object_class = (GObjectClass *) klass;

  object_class->constructed = mct_extension_agent_object_constructed;
  object_class->dispose = mct_extension_agent_object_dispose;
  object_class->get_property = mct_extension_agent_object_get_property;
  object_class->set_property = mct_extension_agent_object_set_property;

  /**
   * MctExtensionAgentObject: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);

  /**
   * MctExtensionAgentObject: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);

  /**
   * MctExtensionAgentObject: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_extension_agent_object_error_quark(), to avoid spreading the
   * D-Bus code outside this file. */
  for (size_t i = 0; i < G_N_ELEMENTS (extension_agent_object_error_map); i++)
    g_dbus_error_register_error (MCT_EXTENSION_AGENT_OBJECT_ERROR,
                                 extension_agent_object_error_map[i].error_code,
                                 extension_agent_object_error_map[i].dbus_error_name);
}

static void cancelled_cb (GCancellable *cancellable,
                          void         *user_data);

static void
mct_extension_agent_object_init (MctExtensionAgentObject *self)
{
  MctExtensionAgentObjectPrivate *priv = mct_extension_agent_object_get_instance_private (self);

  priv->cancellable = g_cancellable_new ();
  priv->cancelled_id = g_cancellable_connect (priv->cancellable,
                                              G_CALLBACK (cancelled_cb), self, NULL);
  priv->requests = g_hash_table_new_full (g_str_hash, g_str_equal,
                                          NULL, (GDestroyNotify) mct_extension_agent_request_free);
}

static void
cancelled_cb (GCancellable *cancellable,
              void         *user_data)
{
  MctExtensionAgentObject *self = MCT_EXTENSION_AGENT_OBJECT (user_data);
  MctExtensionAgentObjectPrivate *priv = mct_extension_agent_object_get_instance_private (self);
  GHashTableIter iter;
  void *value;

  /* Propagate cancellation to all our pending requests. */
  g_hash_table_iter_init (&iter, priv->requests);
  while (g_hash_table_iter_next (&iter, NULL, &value))
    {
      MctExtensionAgentRequest *request = value;

      g_cancellable_cancel (request->cancellable);
    }
}

static void
mct_extension_agent_object_constructed (GObject *object)
{
  MctExtensionAgentObject *self = MCT_EXTENSION_AGENT_OBJECT (object);
  MctExtensionAgentObjectPrivate *priv = mct_extension_agent_object_get_instance_private (self);

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

  /* Check our construct properties. */
  g_assert (G_IS_DBUS_CONNECTION (priv->connection));
  g_assert (g_variant_is_object_path (priv->object_path));
}

static void
mct_extension_agent_object_dispose (GObject *object)
{
  MctExtensionAgentObject *self = MCT_EXTENSION_AGENT_OBJECT (object);
  MctExtensionAgentObjectPrivate *priv = mct_extension_agent_object_get_instance_private (self);

  g_assert (priv->request_subtree_id == 0);
  g_assert (priv->n_pending_operations == 0);
  g_assert (g_hash_table_size (priv->requests) == 0);

  g_clear_pointer (&priv->requests, g_hash_table_unref);

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

  if (priv->cancelled_id != 0)
    {
      g_cancellable_disconnect (priv->cancellable, priv->cancelled_id);
      priv->cancelled_id = 0;
    }
  g_clear_object (&priv->cancellable);

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

static void
mct_extension_agent_object_get_property (GObject      *object,
                                         unsigned int  property_id,
                                         GValue       *value,
                                         GParamSpec   *pspec)
{
  MctExtensionAgentObject *self = MCT_EXTENSION_AGENT_OBJECT (object);
  MctExtensionAgentObjectPrivate *priv = mct_extension_agent_object_get_instance_private (self);

  switch ((MctExtensionAgentObjectProperty) property_id)
    {
    case PROP_CONNECTION:
      g_value_set_object (value, priv->connection);
      break;
    case PROP_OBJECT_PATH:
      g_value_set_string (value, priv->object_path);
      break;
    case PROP_BUSY:
      g_value_set_boolean (value, mct_extension_agent_object_get_busy (self));
      break;
    default:
      g_assert_not_reached ();
    }
}

static void
mct_extension_agent_object_set_property (GObject      *object,
                                         unsigned int  property_id,
                                         const GValue *value,
                                         GParamSpec   *pspec)
{
  MctExtensionAgentObject *self = MCT_EXTENSION_AGENT_OBJECT (object);
  MctExtensionAgentObjectPrivate *priv = mct_extension_agent_object_get_instance_private (self);

  switch ((MctExtensionAgentObjectProperty) property_id)
    {
    case PROP_CONNECTION:
      /* Construct only. */
      g_assert (priv->connection == NULL);
      priv->connection = g_value_dup_object (value);
      break;
    case PROP_OBJECT_PATH:
      /* Construct only. */
      g_assert (priv->object_path == NULL);
      g_assert (g_variant_is_object_path (g_value_get_string (value)));
      priv->object_path = g_value_dup_string (value);
      break;
    case PROP_BUSY:
      /* Read only. Fall through. */
      G_GNUC_FALLTHROUGH;
    default:
      g_assert_not_reached ();
    }
}

/**
 * mct_extension_agent_object_register:
 * @self: an extension agent object
 * @error: return location for a [type@GLib.Error]
 *
 * Register the agent service objects on D-Bus using the connection details
 * given in [property@Malcontent.ExtensionAgentObject.connection] and
 * [property@Malcontent.ExtensionAgentObject.object-path].
 *
 * Use [method@Malcontent.ExtensionAgentObject.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_extension_agent_object_register (MctExtensionAgentObject  *self,
                                     GError                  **error)
{
  MctExtensionAgentObjectPrivate *priv = mct_extension_agent_object_get_instance_private (self);

  g_return_val_if_fail (MCT_IS_EXTENSION_AGENT_OBJECT (self), FALSE);
  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

  const GDBusSubtreeVTable subtree_vtable =
    {
      mct_extension_agent_object_request_enumerate,
      mct_extension_agent_object_request_introspect,
      mct_extension_agent_object_request_dispatch,
      { NULL, }  /* padding */
    };

  unsigned int id = g_dbus_connection_register_subtree (priv->connection,
                                                        priv->object_path,
                                                        &subtree_vtable,
                                                        G_DBUS_SUBTREE_FLAGS_NONE,
                                                        g_object_ref (self),
                                                        g_object_unref,
                                                        error);

  if (id == 0)
    return FALSE;

  priv->request_subtree_id = id;

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

  return TRUE;
}

/**
 * mct_extension_agent_object_unregister:
 * @self: an extension agent object
 *
 * Unregister objects from D-Bus which were previously registered using
 * [method@Malcontent.ExtensionAgentObject.register].
 *
 * Calls to these two functions must be well paired.
 *
 * Since: 0.14.0
 */
void
mct_extension_agent_object_unregister (MctExtensionAgentObject *self)
{
  MctExtensionAgentObjectPrivate *priv = mct_extension_agent_object_get_instance_private (self);

  g_return_if_fail (MCT_IS_EXTENSION_AGENT_OBJECT (self));

  g_cancellable_cancel (priv->cancellable);

  g_dbus_connection_unregister_subtree (priv->connection, priv->request_subtree_id);
  priv->request_subtree_id = 0;

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

static char **
mct_extension_agent_object_request_enumerate (GDBusConnection *connection,
                                              const char      *sender,
                                              const char      *object_path,
                                              void            *user_data)
{
  MctExtensionAgentObject *self = MCT_EXTENSION_AGENT_OBJECT (user_data);
  MctExtensionAgentObjectPrivate *priv = mct_extension_agent_object_get_instance_private (self);
  g_autoptr(GPtrArray) paths = NULL;  /* (element-type utf8) */
  GHashTableIter iter;
  void *key;

  /* Don’t implement any permissions checks here, as they should be specific to
   * the APIs being called and objects being accessed. Just output a list of
   * paths to request objects. */
  paths = g_ptr_array_new_null_terminated (g_hash_table_size (priv->requests), g_free, TRUE);

  g_hash_table_iter_init (&iter, priv->requests);
  while (g_hash_table_iter_next (&iter, &key, NULL))
    {
      const char *request_object_subpath = key;
      g_ptr_array_add (paths, g_strdup (request_object_subpath));
    }

  return (char **) g_ptr_array_free (g_steal_pointer (&paths), FALSE);
}

/* @object_subpath should be something like `Request1` or `Request513` */
static MctExtensionAgentRequest *
object_subpath_to_request (MctExtensionAgentObject *self,
                           const char              *object_subpath)
{
  MctExtensionAgentObjectPrivate *priv = mct_extension_agent_object_get_instance_private (self);

  g_return_val_if_fail (object_subpath != NULL, NULL);

  return g_hash_table_lookup (priv->requests, object_subpath);
}

static GDBusInterfaceInfo **
mct_extension_agent_object_request_introspect (GDBusConnection *connection,
                                               const char      *sender,
                                               const char      *object_path,
                                               const char      *node,
                                               void            *user_data)
{
  MctExtensionAgentObject *self = MCT_EXTENSION_AGENT_OBJECT (user_data);
  g_autofree GDBusInterfaceInfo **interfaces = NULL;

  /* Don’t implement any permissions checks here, as they should be specific to
   * the APIs being called and objects being accessed. */

  if (node == NULL)
    {
      /* The root node implements ExtensionAgent only. */
      interfaces = g_new0 (GDBusInterfaceInfo *, 3);
      interfaces[0] = (GDBusInterfaceInfo *) &org_freedesktop_malcontent_timer1_extension_agent_interface;
      interfaces[1] = (GDBusInterfaceInfo *) &org_freedesktop_dbus_properties_interface;
      interfaces[2] = NULL;
    }
  else if (object_subpath_to_request (self, node) != NULL)
    {
      /* Request objects */
      interfaces = g_new0 (GDBusInterfaceInfo *, 3);
      interfaces[0] = (GDBusInterfaceInfo *) &org_freedesktop_malcontent_timer1_extension_agent_request_interface;
      interfaces[1] = (GDBusInterfaceInfo *) &org_freedesktop_dbus_properties_interface;
      interfaces[2] = NULL;
    }

  return g_steal_pointer (&interfaces);
}

static const GDBusInterfaceVTable *
mct_extension_agent_object_request_dispatch (GDBusConnection  *connection,
                                             const char       *sender,
                                             const char       *object_path,
                                             const char       *interface_name,
                                             const char       *node,
                                             void            **out_user_data,
                                             void             *user_data)
{
  MctExtensionAgentObject *self = MCT_EXTENSION_AGENT_OBJECT (user_data);
  static const GDBusInterfaceVTable extension_agent_interface_vtable =
    {
      mct_extension_agent_object_method_call,
      NULL,  /* handled in mct_extension_agent_object_method_call() */
      NULL,  /* handled in mct_extension_agent_object_method_call() */
      { NULL, }  /* padding */
    };
  static const GDBusInterfaceVTable extension_agent_request_interface_vtable =
    {
      mct_extension_agent_request_method_call,
      NULL,  /* handled in mct_extension_agent_request_method_call() */
      NULL,  /* handled in mct_extension_agent_request_method_call() */
      { NULL, }  /* padding */
    };

  /* Don’t implement any permissions checks here, as they should be specific to
   * the APIs being called and objects being accessed. */

  /* Agent is implemented on the root of the tree. */
  if (node == NULL &&
      (g_str_equal (interface_name, "org.freedesktop.MalcontentTimer1.ExtensionAgent") ||
       g_str_equal (interface_name, "org.freedesktop.DBus.Properties")))
    {
      *out_user_data = user_data;
      return &extension_agent_interface_vtable;
    }
  else if (node == NULL)
    {
      return NULL;
    }

  /* We only handle the Request interface on other objects. */
  if (!g_str_equal (interface_name, "org.freedesktop.MalcontentTimer1.ExtensionAgent.Request") &&
      !g_str_equal (interface_name, "org.freedesktop.DBus.Properties"))
    return NULL;

  /* Find the request. */
  MctExtensionAgentRequest *request = object_subpath_to_request (self, node);

  if (request == NULL)
    return NULL;

  *out_user_data = user_data;
  return &extension_agent_request_interface_vtable;
}

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;
}

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

static const struct
  {
    const char *interface_name;
    const char *method_name;
    ExtensionAgentMethodCallFunc func;
  }
extension_agent_methods[] =
  {
    /* Handle properties. */
    { "org.freedesktop.DBus.Properties", "Get",
      mct_extension_agent_object_properties_get },
    { "org.freedesktop.DBus.Properties", "Set",
      mct_extension_agent_object_properties_set },
    { "org.freedesktop.DBus.Properties", "GetAll",
      mct_extension_agent_object_properties_get_all },

    /* Extension agent methods. */
    { "org.freedesktop.MalcontentTimer1.ExtensionAgent", "RequestExtension",
      mct_extension_agent_object_request_extension },
  };

static void
mct_extension_agent_object_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)
{
  MctExtensionAgentObject *self = MCT_EXTENSION_AGENT_OBJECT (user_data);
  MctExtensionAgentObjectPrivate *priv = mct_extension_agent_object_get_instance_private (self);

  /* 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_extension_agent_interface_methods = 0;
  for (size_t i = 0; org_freedesktop_malcontent_timer1_extension_agent_interface.methods[i] != NULL; i++)
    n_extension_agent_interface_methods++;

  g_assert (G_N_ELEMENTS (extension_agent_methods) ==
            n_extension_agent_interface_methods +
            3  /* o.fdo.DBus.Properties */);

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

  /* Work out which method to call. */
  for (size_t i = 0; i < G_N_ELEMENTS (extension_agent_methods); i++)
    {
      if (g_str_equal (extension_agent_methods[i].interface_name, interface_name) &&
          g_str_equal (extension_agent_methods[i].method_name, method_name))
        {
          extension_agent_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_extension_agent_object_properties_get (MctExtensionAgentObject *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_extension_agent_object_properties_set (MctExtensionAgentObject *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_extension_agent_object_properties_get_all (MctExtensionAgentObject *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.ExtensionAgent"))
    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);
}

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_EXTENSION_AGENT_OBJECT_ERROR,
                                             MCT_EXTENSION_AGENT_OBJECT_ERROR_INVALID_QUERY,
                                             _("Invalid query parameters: %s"),
                                             local_error->message);
      return FALSE;
    }

  return TRUE;
}

/* https://www.freedesktop.org/software/polkit/docs/latest/eggdbus-interface-org.freedesktop.PolicyKit1.Authority.html#eggdbus-struct-Subject */
static gboolean
validate_subject (GDBusMethodInvocation *invocation,
                  GVariant              *subject)
{
  const char *subject_kind;
  g_autoptr(GVariant) subject_details = NULL;

  /* For the moment we only support unix-process subjects, using the pidfd details. */
  g_assert (g_variant_is_of_type (subject, G_VARIANT_TYPE ("(sa{sv})")));

  g_variant_get (subject, "(&s@a{sv})", &subject_kind, &subject_details);

  if (g_str_equal (subject_kind, "unix-process"))
    {
      if (!g_variant_lookup (subject_details, "pidfd", "h", NULL) ||
          !g_variant_lookup (subject_details, "uid", "i", NULL))
        {
          g_dbus_method_invocation_return_error (invocation, MCT_EXTENSION_AGENT_OBJECT_ERROR,
                                                 MCT_EXTENSION_AGENT_OBJECT_ERROR_INVALID_QUERY,
                                                 _("Invalid query parameters: %s"),
                                                 _("Missing subject UID"));
          return FALSE;
        }

      return TRUE;
    }
  else
    {
      g_dbus_method_invocation_return_error (invocation, MCT_EXTENSION_AGENT_OBJECT_ERROR,
                                             MCT_EXTENSION_AGENT_OBJECT_ERROR_INVALID_QUERY,
                                             _("Invalid query parameters: %s"),
                                             _("Unsupported subject kind"));
      return FALSE;
    }
}

typedef struct
{
  char *request_owner;  /* (owned) */
  char *request_object_path;  /* (owned) */
  MctOperationCounter operation_counter;
} RequestExtensionData;

static void
request_extension_data_free (RequestExtensionData *data)
{
  g_clear_pointer (&data->request_owner, g_free);
  g_clear_pointer (&data->request_object_path, g_free);
  mct_operation_counter_release_and_clear (&data->operation_counter);
  g_free (data);
}

G_DEFINE_AUTOPTR_CLEANUP_FUNC (RequestExtensionData, request_extension_data_free)

static void request_sender_vanished_cb (GDBusConnection *connection,
                                        const char      *name,
                                        void            *user_data);
static void request_extension_vfunc_cb (GObject      *object,
                                        GAsyncResult *result,
                                        void         *user_data);

static void
mct_extension_agent_object_request_extension (MctExtensionAgentObject *self,
                                              GDBusConnection         *connection,
                                              const char              *sender,
                                              GVariant                *parameters,
                                              GDBusMethodInvocation   *invocation)
{
  MctExtensionAgentObjectPrivate *priv = mct_extension_agent_object_get_instance_private (self);
  g_autoptr(GError) local_error = NULL;
  g_autofree char *record_type = NULL;
  g_autofree char *identifier = NULL;
  uint64_t duration_secs;
  g_autoptr(GVariant) subject = NULL;
  GUnixFDList *subject_fd_list;
  g_autoptr(GVariant) extra_data = NULL;
  g_autoptr(RequestExtensionData) data = NULL;
  MctExtensionAgentObjectClass *klass;
  g_autoptr(MctExtensionAgentRequest) request_owned = NULL;
  MctExtensionAgentRequest *request;

  /* Validate the parameters. */
  g_variant_get (parameters, "(sst@r@a{sv})",
                 &record_type, &identifier, &duration_secs, &subject, &extra_data);

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

  /* Only malcontent-timerd is allowed to call the agent, but we enforce that
   * using a D-Bus access control policy, rather than in code. */

  subject_fd_list = g_dbus_message_get_unix_fd_list (g_dbus_method_invocation_get_message (invocation));

  data = g_new0 (RequestExtensionData, 1);
  mct_operation_counter_init_and_hold (&data->operation_counter,
                                       &priv->n_pending_operations,
                                       G_OBJECT (self), props[PROP_BUSY]);

  /* Create an ExtensionAgent.Request object and immediately return it, so the
   * client can track the request. We don’t need to explicitly register the
   * object on the bus because once it’s in priv->requests, a call to
   * mct_extension_agent_object_request_enumerate() will return it. */
  if (priv->request_counter == G_MAXUINT)
    {
      g_dbus_method_invocation_return_error (invocation, MCT_EXTENSION_AGENT_OBJECT_ERROR,
                                             MCT_EXTENSION_AGENT_OBJECT_ERROR_FAILED,
                                             _("Error requesting extension: %s"),
                                             _("Too many requests"));
      return;
    }

  request = request_owned = g_new0 (MctExtensionAgentRequest, 1);
  request->object_path = g_strdup_printf ("/org/freedesktop/MalcontentTimer1/ExtensionAgent/Request%u",
                                          ++priv->request_counter);
  request->object_subpath = request->object_path + strlen ("/org/freedesktop/MalcontentTimer1/ExtensionAgent/");
  request->cancellable = g_cancellable_new ();
  request->owner = g_strdup (g_dbus_method_invocation_get_sender (invocation));

  data->request_owner = g_strdup (request->owner);
  data->request_object_path = g_strdup (request->object_path);
  g_hash_table_insert (priv->requests, (void *) request->object_subpath,
                       g_steal_pointer (&request_owned));

  g_dbus_method_invocation_return_value (invocation,
                                         g_variant_new ("(o)", request->object_path));

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

  /* Watch the sender so we can cancel the request if they disappear off the bus. */
  request->owner_name_watch_id = g_bus_watch_name_on_connection (priv->connection,
                                                                 sender,
                                                                 G_BUS_NAME_WATCHER_FLAGS_NONE,
                                                                 NULL,
                                                                 request_sender_vanished_cb,
                                                                 self,
                                                                 NULL);

  /* Now start the actual asynchronous extension request. It’ll be controlled
   * and monitored from the request object. */
  klass = MCT_EXTENSION_AGENT_OBJECT_GET_CLASS (self);
  g_assert (klass->request_extension_async != NULL);
  klass->request_extension_async (self, record_type, identifier, duration_secs, extra_data, subject, subject_fd_list,
                                  invocation, request->cancellable,
                                  request_extension_vfunc_cb, g_steal_pointer (&data));
}

static void
request_extension_vfunc_cb (GObject      *object,
                            GAsyncResult *result,
                            void         *user_data)
{
  MctExtensionAgentObject *self = MCT_EXTENSION_AGENT_OBJECT (object);
  MctExtensionAgentObjectPrivate *priv = mct_extension_agent_object_get_instance_private (self);
  g_autoptr(RequestExtensionData) data = g_steal_pointer (&user_data);
  MctExtensionAgentObjectClass *klass;
  gboolean granted = FALSE;
  g_autoptr(GVariant) extra_data = NULL;
  g_autoptr(GError) local_error = NULL;

  klass = MCT_EXTENSION_AGENT_OBJECT_GET_CLASS (self);
  g_assert (klass->request_extension_finish != NULL);
  if (!klass->request_extension_finish (self, result, &granted, &extra_data, &local_error))
    {
      g_autofree char *error_name = NULL;

      g_assert (local_error->domain == MCT_EXTENSION_AGENT_OBJECT_ERROR);

      granted = FALSE;
      g_clear_pointer (&extra_data, g_variant_unref);

      error_name = g_dbus_error_encode_gerror (local_error);

      g_auto(GVariantBuilder) extra_data_builder = G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE ("a{sv}"));
      if (error_name != NULL)
        g_variant_builder_add (&extra_data_builder, "{sv}", "error-name", g_variant_new_string (error_name));
      extra_data = g_variant_ref_sink (g_variant_builder_end (&extra_data_builder));
    }

  /* Emit the response signal on the request object, to notify the client about
   * the decision. This call can only fail if the parameters are invalid, or the
   * D-Bus connection has been closed, so we don’t bother with error reporting. */
  g_dbus_connection_emit_signal (priv->connection,
                                 data->request_owner,
                                 data->request_object_path,
                                 "org.freedesktop.MalcontentTimer1.ExtensionAgent.Request",
                                 "Response",
                                 g_variant_new ("(b@a{sv})",
                                                granted,
                                                (extra_data != NULL) ? extra_data : g_variant_new_parsed ("@a{sv} {}")),
                                 NULL);

  /* Keep the Request object about until the client calls Close() on it, but
   * otherwise this async chain is complete. */
}

static void
request_sender_vanished_cb (GDBusConnection *connection,
                            const char      *name,
                            void            *user_data)
{
  MctExtensionAgentObject *self = MCT_EXTENSION_AGENT_OBJECT (user_data);
  MctExtensionAgentObjectPrivate *priv = mct_extension_agent_object_get_instance_private (self);
  GHashTableIter iter;
  void *value;

  /* Search for all the sender’s requests. */
  g_hash_table_iter_init (&iter, priv->requests);
  while (g_hash_table_iter_next (&iter, NULL, &value))
    {
      MctExtensionAgentRequest *request = value;

      if (!g_str_equal (request->owner, name))
        continue;

      /* Cancel any pending async calls associated with the request. */
      g_cancellable_cancel (request->cancellable);

      /* Remove the request. */
      g_hash_table_iter_remove (&iter);
    }

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

typedef void (*ExtensionAgentRequestMethodCallFunc) (MctExtensionAgentObject  *self,
                                                     MctExtensionAgentRequest *request,
                                                     GDBusConnection          *connection,
                                                     const char               *sender,
                                                     GVariant                 *parameters,
                                                     GDBusMethodInvocation    *invocation);

static const struct
  {
    const char *interface_name;
    const char *method_name;
    ExtensionAgentRequestMethodCallFunc func;
  }
extension_agent_request_methods[] =
  {
    /* Handle properties. */
    { "org.freedesktop.DBus.Properties", "Get",
      mct_extension_agent_request_properties_get },
    { "org.freedesktop.DBus.Properties", "Set",
      mct_extension_agent_request_properties_set },
    { "org.freedesktop.DBus.Properties", "GetAll",
      mct_extension_agent_request_properties_get_all },

    /* Request methods. */
    { "org.freedesktop.MalcontentTimer1.ExtensionAgent.Request", "Close",
      mct_extension_agent_request_close },
  };

static void
mct_extension_agent_request_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)
{
  MctExtensionAgentObject *self = MCT_EXTENSION_AGENT_OBJECT (user_data);
  MctExtensionAgentObjectPrivate *priv = mct_extension_agent_object_get_instance_private (self);
  MctExtensionAgentRequest *request;

  /* 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_request_interface_methods = 0;
  for (size_t i = 0; org_freedesktop_malcontent_timer1_extension_agent_request_interface.methods[i] != NULL; i++)
    n_request_interface_methods++;

  g_assert (G_N_ELEMENTS (extension_agent_request_methods) ==
            n_request_interface_methods +
            3  /* o.fdo.DBus.Properties */);

  /* Remove the service prefix from the path. */
  g_assert (g_str_has_prefix (object_path, priv->object_path));
  g_assert (object_path[strlen (priv->object_path)] == '/');

  request = object_subpath_to_request (self,
                                       object_path + strlen (priv->object_path) + 1);
  g_assert (request != NULL);

  /* Check the @sender is the owner of @request. */
  if (!g_str_equal (request->owner, sender))
    {
      g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR,
                                             G_DBUS_ERROR_UNKNOWN_OBJECT,
                                             _("Unknown object ‘%s’."), object_path);
      return;
    }

  /* Work out which method to call. */
  for (size_t i = 0; i < G_N_ELEMENTS (extension_agent_request_methods); i++)
    {
      if (g_str_equal (extension_agent_request_methods[i].interface_name, interface_name) &&
          g_str_equal (extension_agent_request_methods[i].method_name, method_name))
        {
          extension_agent_request_methods[i].func (self, request, 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_extension_agent_request_properties_get (MctExtensionAgentObject  *self,
                                            MctExtensionAgentRequest *request,
                                            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_extension_agent_request_properties_set (MctExtensionAgentObject  *self,
                                            MctExtensionAgentRequest *request,
                                            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_extension_agent_request_properties_get_all (MctExtensionAgentObject  *self,
                                                MctExtensionAgentRequest *request,
                                                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.ExtensionAgent.Request"))
    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);
}

static void
mct_extension_agent_request_close (MctExtensionAgentObject  *self,
                                   MctExtensionAgentRequest *request,
                                   GDBusConnection          *connection,
                                   const char               *sender,
                                   GVariant                 *parameters,
                                   GDBusMethodInvocation    *invocation)
{
  MctExtensionAgentObjectPrivate *priv = mct_extension_agent_object_get_instance_private (self);

  /* We don’t need to check whether the @sender has permission, as
   * mct_extension_agent_request_method_call() has already done that. */

  /* Cancel any pending async calls associated with the request. */
  g_cancellable_cancel (request->cancellable);

  /* Remove the request. Note: @request may be potentially freed after this point. */
  g_hash_table_remove (priv->requests, request->object_subpath);

  g_dbus_method_invocation_return_value (invocation, NULL);

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

/**
 * mct_extension_agent_object_get_busy:
 * @self: an extension agent object
 *
 * Get the value of [property@Malcontent.ExtensionAgentObject.busy].
 *
 * Returns: true if the object is busy, false otherwise
 * Since: 0.14.0
 */
gboolean
mct_extension_agent_object_get_busy (MctExtensionAgentObject *self)
{
  MctExtensionAgentObjectPrivate *priv = mct_extension_agent_object_get_instance_private (self);

  g_return_val_if_fail (MCT_IS_EXTENSION_AGENT_OBJECT (self), FALSE);

  return (priv->request_subtree_id != 0 &&
          (priv->n_pending_operations > 0 || g_hash_table_size (priv->requests) > 0));
}

/**
 * mct_extension_agent_object_get_connection:
 * @self: an extension agent object
 *
 * Get the value of [property@Malcontent.ExtensionAgentObject.connection].
 *
 * Returns: D-Bus connection used by the agent object
 * Since: 0.14.0
 */
GDBusConnection *
mct_extension_agent_object_get_connection (MctExtensionAgentObject *self)
{
  MctExtensionAgentObjectPrivate *priv = mct_extension_agent_object_get_instance_private (self);

  g_return_val_if_fail (MCT_IS_EXTENSION_AGENT_OBJECT (self), NULL);

  return priv->connection;
}
