/* -*- 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-object.h>
#include <glib-unix.h>
#include <glib/gi18n-lib.h>
#include <gio/gio.h>
#include <locale.h>
#include <libgsystemservice/peer-manager.h>
#include <libgsystemservice/peer-manager-dbus.h>
#include <libgsystemservice/service.h>
#include <libmalcontent-web/filtering-dbus-service.h>
#include <libmalcontent-web/web-service.h>


static void mct_web_service_dispose (GObject *object);
static void mct_web_service_finalize (GObject *object);

static void mct_web_service_startup_async (GssService          *service,
                                           GCancellable        *cancellable,
                                           GAsyncReadyCallback  callback,
                                           gpointer             user_data);
static void mct_web_service_startup_finish (GssService    *service,
                                            GAsyncResult  *result,
                                            GError       **error);
static void mct_web_service_shutdown (GssService *service);

static void notify_busy_cb (GObject    *obj,
                            GParamSpec *pspec,
                            gpointer    user_data);

/**
 * MctWebService:
 *
 * The core implementation of the web filtering daemon, which exposes its D-Bus
 * API on the bus.
 *
 * Since: 0.14.0
 */
struct _MctWebService
{
  GssService parent;

  char *state_directory_path;  /* (owned) (nullable) */
  char *cache_directory_path;  /* (owned) (nullable) */
  MctFilteringDBusService *filtering_dbus_service;  /* (owned) */
  unsigned long filtering_dbus_service_notify_busy_id;

  gboolean busy;
};

G_DEFINE_TYPE (MctWebService, mct_web_service, GSS_TYPE_SERVICE)

static void
mct_web_service_class_init (MctWebServiceClass *klass)
{
  GObjectClass *object_class = (GObjectClass *) klass;
  GssServiceClass *service_class = (GssServiceClass *) klass;

  object_class->dispose = mct_web_service_dispose;
  object_class->finalize = mct_web_service_finalize;

  service_class->startup_async = mct_web_service_startup_async;
  service_class->startup_finish = mct_web_service_startup_finish;
  service_class->shutdown = mct_web_service_shutdown;
}

static void
mct_web_service_init (MctWebService *self)
{
  g_autoptr(GOptionGroup) group = NULL;
  const GOptionEntry options[] =
    {
      { "state-directory", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_FILENAME, &self->state_directory_path,
        N_("Directory to store data in (default: $STATE_DIRECTORY)"), N_("DIR") },
      { "cache-directory", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_FILENAME, &self->cache_directory_path,
        N_("Directory to cache data in (default: $CACHE_DIRECTORY)"), N_("DIR") },
      { NULL}
    };

  /* Add an additional command line option for the state directory. */
  group = g_option_group_new ("service", _("Service Options:"), _("Show service help options"), NULL, NULL);
  g_option_group_add_entries (group, options);
  gss_service_add_option_group (GSS_SERVICE (self), group);
}

static void
mct_web_service_dispose (GObject *object)
{
  MctWebService *self = MCT_WEB_SERVICE (object);

  g_clear_signal_handler (&self->filtering_dbus_service_notify_busy_id, self->filtering_dbus_service);

  g_clear_object (&self->filtering_dbus_service);

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

static void
mct_web_service_finalize (GObject *object)
{
  MctWebService *self = MCT_WEB_SERVICE (object);

  g_clear_pointer (&self->state_directory_path, g_free);
  g_clear_pointer (&self->cache_directory_path, g_free);

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

static GFile *
get_system_directory (const char *environment_variable_name,
                      const char *command_line_argument)
{
  const char *env = g_getenv (environment_variable_name);
  if (command_line_argument != NULL)
    return g_file_new_for_commandline_arg (command_line_argument);
  else if (env != NULL)
    return g_file_new_for_commandline_arg (env);
  else
    return NULL;
}

typedef struct
{
  GFile *state_directory;
  GFile *cache_directory;
  MctManager *policy_manager;
  MctUserManager *user_manager;
} StartupData;

static void
startup_data_free (StartupData *data)
{
  g_clear_object (&data->user_manager);
  g_clear_object (&data->policy_manager);
  g_clear_object (&data->cache_directory);
  g_clear_object (&data->state_directory);
  g_free (data);
}

G_DEFINE_AUTOPTR_CLEANUP_FUNC (StartupData, startup_data_free)

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

static void
mct_web_service_startup_async (GssService          *service,
                               GCancellable        *cancellable,
                               GAsyncReadyCallback  callback,
                               gpointer             user_data)
{
  MctWebService *self = MCT_WEB_SERVICE (service);
  g_autoptr(GTask) task = NULL;
  g_autoptr(StartupData) data_owned = NULL;
  StartupData *data;
  GDBusConnection *connection = gss_service_get_dbus_connection (service);
  g_autoptr(GError) local_error = NULL;

  task = g_task_new (service, cancellable, callback, user_data);
  g_task_set_source_tag (task, mct_web_service_startup_async);

  data = data_owned = g_new0 (StartupData, 1);
  g_task_set_task_data (task, g_steal_pointer (&data_owned), (GDestroyNotify) startup_data_free);

  /* Work out where the state directory is. If running under systemd, it’ll be
   * passed in as `STATE_DIRECTORY` due to our use of the `StateDirectory=`
   * option (see systemd.exec(5)). We prefer the command line option, if
   * specified, as that’s used for debugging. */
  data->state_directory = get_system_directory ("STATE_DIRECTORY", self->state_directory_path);
  if (data->state_directory == NULL)
    {
      g_task_return_new_error (task, GSS_SERVICE_ERROR, GSS_SERVICE_ERROR_INVALID_ENVIRONMENT,
                               _("No state directory specified using $STATE_DIRECTORY or --state-directory"));
      return;
    }

  /* Similarly work out the cache directory. */
  data->cache_directory = get_system_directory ("CACHE_DIRECTORY", self->cache_directory_path);
  if (data->cache_directory == NULL)
    {
      g_task_return_new_error (task, GSS_SERVICE_ERROR, GSS_SERVICE_ERROR_INVALID_ENVIRONMENT,
                               _("No cache directory specified using $CACHE_DIRECTORY or --cache-directory"));
      return;
    }

  data->policy_manager = mct_manager_new (connection);
  data->user_manager = mct_user_manager_new (connection);

  /* Fill the user data cache */
  mct_user_manager_load_async (data->user_manager, cancellable,
                               user_manager_loaded_cb, g_steal_pointer (&task));
}

static void
user_manager_loaded_cb (GObject      *object,
                        GAsyncResult *result,
                        void         *user_data)
{
  MctUserManager *user_manager = MCT_USER_MANAGER (object);
  g_autoptr(GTask) task = G_TASK (user_data);
  MctWebService *self = MCT_WEB_SERVICE (g_task_get_source_object (task));
  GDBusConnection *connection = gss_service_get_dbus_connection (GSS_SERVICE (self));
  StartupData *data = g_task_get_task_data (task);
  g_autoptr(MctFilterUpdater) filter_updater = NULL;
  g_autoptr(GError) local_error = NULL;

  if (!mct_user_manager_load_finish (user_manager, result, &local_error))
    {
      g_task_return_error (task, g_steal_pointer (&local_error));
      return;
    }

  filter_updater = mct_filter_updater_new (data->policy_manager, data->user_manager,
                                           data->state_directory, data->cache_directory);
  self->filtering_dbus_service =
      mct_filtering_dbus_service_new (connection,
                                      "/org/freedesktop/MalcontentWeb1",
                                      filter_updater);
  self->filtering_dbus_service_notify_busy_id =
      g_signal_connect (self->filtering_dbus_service, "notify::busy",
                        (GCallback) notify_busy_cb, self);
  notify_busy_cb (G_OBJECT (self->filtering_dbus_service), NULL, self);

  if (!mct_filtering_dbus_service_register (self->filtering_dbus_service, &local_error))
    g_task_return_error (task, g_steal_pointer (&local_error));
  else
    g_task_return_boolean (task, TRUE);
}

static void
mct_web_service_startup_finish (GssService    *service,
                                  GAsyncResult  *result,
                                  GError       **error)
{
  g_task_propagate_boolean (G_TASK (result), error);
}

static void
notify_busy_cb (GObject    *obj,
                GParamSpec *pspec,
                gpointer    user_data)
{
  MctWebService *self = MCT_WEB_SERVICE (user_data);

  gboolean was_busy = self->busy;
  gboolean now_busy = mct_filtering_dbus_service_get_busy (self->filtering_dbus_service);

  g_debug ("%s: was_busy: %s, now_busy: %s",
           G_STRFUNC, was_busy ? "yes" : "no", now_busy ? "yes" : "no");

  if (was_busy && !now_busy)
    gss_service_release (GSS_SERVICE (self));
  else if (!was_busy && now_busy)
    gss_service_hold (GSS_SERVICE (self));

  self->busy = now_busy;
}

static void
mct_web_service_shutdown (GssService *service)
{
  MctWebService *self = MCT_WEB_SERVICE (service);

  mct_filtering_dbus_service_unregister (self->filtering_dbus_service);
}

/**
 * mct_web_service_new:
 *
 * Create a new [class@Malcontent.WebService].
 *
 * Returns: (transfer full): a new [class@Malcontent.WebService]
 * Since: 0.14.0
 */
MctWebService *
mct_web_service_new (void)
{
  return g_object_new (MCT_TYPE_WEB_SERVICE,
                       "bus-type", G_BUS_TYPE_SYSTEM,
                       "service-id", "org.freedesktop.MalcontentWeb1",
                       "inactivity-timeout", 30000  /* ms */,
                       "translation-domain", GETTEXT_PACKAGE,
                       "parameter-string", _("— manage web filter lists for parental controls"),
                       "summary", _("Manage web filter lists to enforce "
                                    "parental controls on web access."),
                       NULL);
}

